1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
|
"""Tests for class inheritance."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable
import pytest
from griffe import ModulesCollection, temporary_inspected_module, temporary_visited_module
if TYPE_CHECKING:
from griffe import Class
def _mro_paths(cls: Class) -> list[str]:
return [base.path for base in cls.mro()]
@pytest.mark.parametrize("agent1", [temporary_visited_module, temporary_inspected_module])
@pytest.mark.parametrize("agent2", [temporary_visited_module, temporary_inspected_module])
def test_loading_inherited_members(agent1: Callable, agent2: Callable) -> None:
"""Test basic class inheritance.
Parameters:
agent1: A parametrized agent to load a module.
agent2: A parametrized agent to load a module.
"""
code1 = """
class A:
attr_from_a = 0
def method_from_a(self): ...
class B(A):
attr_from_a = 1
attr_from_b = 1
def method_from_b(self): ...
"""
code2 = """
from module1 import B
class C(B):
attr_from_c = 2
def method_from_c(self): ...
class D(C):
attr_from_a = 3
attr_from_d = 3
def method_from_d(self): ...
"""
inspection_options = {}
collection = ModulesCollection()
with agent1(code1, module_name="module1", modules_collection=collection) as module1:
if agent2 is temporary_inspected_module:
inspection_options["import_paths"] = [module1.filepath.parent]
with agent2(code2, module_name="module2", modules_collection=collection, **inspection_options) as module2:
classa = module1["A"]
classb = module1["B"]
classc = module2["C"]
classd = module2["D"]
assert classa in classb.resolved_bases
assert classb in classc.resolved_bases
assert classc in classd.resolved_bases
classd_mro = classd.mro()
assert classa in classd_mro
assert classb in classd_mro
assert classc in classd_mro
inherited_members = classd.inherited_members
assert "attr_from_a" not in inherited_members # Overwritten.
assert "attr_from_b" in inherited_members
assert "attr_from_c" in inherited_members
assert "attr_from_d" not in inherited_members # Own-declared.
assert "method_from_a" in inherited_members
assert "method_from_b" in inherited_members
assert "method_from_c" in inherited_members
assert "method_from_d" not in inherited_members # Own-declared.
assert "attr_from_b" in classd.all_members
@pytest.mark.parametrize("agent", [temporary_visited_module, temporary_inspected_module])
def test_nested_class_inheritance(agent: Callable) -> None:
"""Test nested class inheritance.
Parameters:
agent: A parametrized agent to load a module.
"""
code = """
class A:
class B:
attr_from_b = 0
class C(A.B):
attr_from_c = 1
"""
with agent(code) as module:
assert "attr_from_b" in module["C"].inherited_members
code = """
class OuterA:
class Inner: ...
class OuterB(OuterA):
class Inner(OuterA.Inner): ...
class OuterC(OuterA):
class Inner(OuterA.Inner): ...
class OuterD(OuterC):
class Inner(OuterC.Inner, OuterB.Inner): ...
"""
with temporary_visited_module(code) as module:
assert _mro_paths(module["OuterD.Inner"]) == [
"module.OuterC.Inner",
"module.OuterB.Inner",
"module.OuterA.Inner",
]
@pytest.mark.parametrize(
("classes", "cls", "expected_mro"),
[
(["A", "B(A)"], "B", ["A"]),
(["A", "B(A)", "C(A)", "D(B, C)"], "D", ["B", "C", "A"]),
(["A", "B(A)", "C(A)", "D(C, B)"], "D", ["C", "B", "A"]),
(["A(Z)"], "A", []),
(["A(str)"], "A", []),
(["A", "B(A)", "C(B)", "D(C)"], "D", ["C", "B", "A"]),
(["A", "B(A)", "C(B)", "D(C)", "E(A)", "F(B)", "G(F, E)", "H(G, D)"], "H", ["G", "F", "D", "C", "B", "E", "A"]),
(["A", "B(A[T])", "C(B[T])"], "C", ["B", "A"]),
],
)
def test_computing_mro(classes: list[str], cls: str, expected_mro: list[str]) -> None:
"""Test computing MRO.
Parameters:
classes: A list of classes inheriting from each other.
cls: The class to compute the MRO of.
expected_mro: The expected computed MRO.
"""
code = "class " + ": ...\nclass ".join(classes) + ": ..."
with temporary_visited_module(code) as module:
assert _mro_paths(module[cls]) == [f"module.{base}" for base in expected_mro]
@pytest.mark.parametrize(
("classes", "cls"),
[
(["A", "B(A, A)"], "B"),
(["A(D)", "B", "C(A, B)", "D(C)"], "D"),
],
)
def test_uncomputable_mro(classes: list[str], cls: str) -> None:
"""Test uncomputable MRO.
Parameters:
classes: A list of classes inheriting from each other.
cls: The class to compute the MRO of.
"""
code = "class " + ": ...\nclass ".join(classes) + ": ..."
with temporary_visited_module(code) as module, pytest.raises(ValueError, match="Cannot compute C3 linearization"):
_mro_paths(module[cls])
def test_dynamic_base_classes() -> None:
"""Test dynamic base classes."""
code = """
from collections import namedtuple
class A(namedtuple("B", "attrb")):
attra = 0
"""
with temporary_visited_module(code) as module:
assert _mro_paths(module["A"]) == [] # Not supported.
with temporary_inspected_module(code) as module:
assert _mro_paths(module["A"]) == [] # Not supported either.
|