Skip to content

Commit ffe1e71

Browse files
authored
Pyreverse: Use dashed lines for type-checking imports (#8824)
1 parent 0beb2b6 commit ffe1e71

File tree

18 files changed

+118
-4
lines changed

18 files changed

+118
-4
lines changed

doc/whatsnew/fragments/8112.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In Pyreverse package dependency diagrams, show when a module imports another only for type-checking.
2+
3+
Closes #8112

pylint/pyreverse/diagrams.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import astroid
1313
from astroid import nodes, util
1414

15-
from pylint.checkers.utils import decorated_with_property
15+
from pylint.checkers.utils import decorated_with_property, in_type_checking_block
1616
from pylint.pyreverse.utils import FilterMixIn
1717

1818

@@ -281,9 +281,15 @@ def get_module(self, name: str, node: nodes.Module) -> PackageEntity:
281281
def add_from_depend(self, node: nodes.ImportFrom, from_module: str) -> None:
282282
"""Add dependencies created by from-imports."""
283283
mod_name = node.root().name
284-
obj = self.module(mod_name)
285-
if from_module not in obj.node.depends:
286-
obj.node.depends.append(from_module)
284+
package = self.module(mod_name).node
285+
286+
if from_module in package.depends:
287+
return
288+
289+
if not in_type_checking_block(node):
290+
package.depends.append(from_module)
291+
elif from_module not in package.type_depends:
292+
package.type_depends.append(from_module)
287293

288294
def extract_relationships(self) -> None:
289295
"""Extract relationships between nodes in the diagram."""
@@ -304,3 +310,10 @@ def extract_relationships(self) -> None:
304310
except KeyError:
305311
continue
306312
self.add_relationship(package_obj, dep, "depends")
313+
314+
for dep_name in package_obj.node.type_depends:
315+
try:
316+
dep = self.get_module(dep_name, package_obj.node)
317+
except KeyError: # pragma: no cover
318+
continue
319+
self.add_relationship(package_obj, dep, "type_depends")

pylint/pyreverse/dot_printer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class HTMLLabels(Enum):
4343
"style": "solid",
4444
},
4545
EdgeType.USES: {"arrowtail": "none", "arrowhead": "open"},
46+
EdgeType.TYPE_DEPENDENCY: {
47+
"arrowtail": "none",
48+
"arrowhead": "open",
49+
"style": "dashed",
50+
},
4651
}
4752

4853

pylint/pyreverse/inspector.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def visit_module(self, node: nodes.Module) -> None:
139139
return
140140
node.locals_type = collections.defaultdict(list)
141141
node.depends = []
142+
node.type_depends = []
142143
if self.tag:
143144
node.uid = self.generate_id()
144145

pylint/pyreverse/mermaidjs_printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class MermaidJSPrinter(Printer):
2424
EdgeType.ASSOCIATION: "--*",
2525
EdgeType.AGGREGATION: "--o",
2626
EdgeType.USES: "-->",
27+
EdgeType.TYPE_DEPENDENCY: "-.->",
2728
}
2829

2930
def _open_graph(self) -> None:

pylint/pyreverse/plantuml_printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class PlantUmlPrinter(Printer):
2424
EdgeType.ASSOCIATION: "--*",
2525
EdgeType.AGGREGATION: "--o",
2626
EdgeType.USES: "-->",
27+
EdgeType.TYPE_DEPENDENCY: "..>",
2728
}
2829

2930
def _open_graph(self) -> None:

pylint/pyreverse/printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class EdgeType(Enum):
2525
ASSOCIATION = "association"
2626
AGGREGATION = "aggregation"
2727
USES = "uses"
28+
TYPE_DEPENDENCY = "type_dependency"
2829

2930

3031
class Layout(Enum):

pylint/pyreverse/writer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def write_packages(self, diagram: PackageDiagram) -> None:
7676
type_=EdgeType.USES,
7777
)
7878

79+
for rel in diagram.get_relationships("type_depends"):
80+
self.printer.emit_edge(
81+
rel.from_object.fig_id,
82+
rel.to_object.fig_id,
83+
type_=EdgeType.TYPE_DEPENDENCY,
84+
)
85+
7986
def write_classes(self, diagram: ClassDiagram) -> None:
8087
"""Write a class diagram."""
8188
# sorted to get predictable (hence testable) results
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
digraph "classes_type_check_imports" {
2+
rankdir=BT
3+
charset="utf-8"
4+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
digraph "packages_type_check_imports" {
2+
rankdir=BT
3+
charset="utf-8"
4+
"package_diagrams" [color="black", label=<package_diagrams>, shape="box", style="solid"];
5+
"package_diagrams.type_check_imports" [color="black", label=<package_diagrams.type_check_imports>, shape="box", style="solid"];
6+
"package_diagrams.type_check_imports.mod_a" [color="black", label=<package_diagrams.type_check_imports.mod_a>, shape="box", style="solid"];
7+
"package_diagrams.type_check_imports.mod_b" [color="black", label=<package_diagrams.type_check_imports.mod_b>, shape="box", style="solid"];
8+
"package_diagrams.type_check_imports.mod_c" [color="black", label=<package_diagrams.type_check_imports.mod_c>, shape="box", style="solid"];
9+
"package_diagrams.type_check_imports.mod_d" [color="black", label=<package_diagrams.type_check_imports.mod_d>, shape="box", style="solid"];
10+
"package_diagrams.type_check_imports.mod_b" -> "package_diagrams.type_check_imports.mod_a" [arrowhead="open", arrowtail="none"];
11+
"package_diagrams.type_check_imports.mod_d" -> "package_diagrams.type_check_imports.mod_a" [arrowhead="open", arrowtail="none"];
12+
"package_diagrams.type_check_imports.mod_c" -> "package_diagrams.type_check_imports.mod_a" [arrowhead="open", arrowtail="none", style="dashed"];
13+
}

tests/pyreverse/functional/package_diagrams/__init__.py

Whitespace-only changes.

tests/pyreverse/functional/package_diagrams/type_check_imports/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Int = int
2+
3+
List = list
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import Any
2+
3+
from mod_a import Int
4+
5+
6+
def is_int(x) -> bool:
7+
return isinstance(x, Int)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import TYPE_CHECKING
2+
3+
if TYPE_CHECKING:
4+
from mod_a import Int
5+
6+
def some_int() -> Int:
7+
return 5
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import TYPE_CHECKING
2+
3+
from mod_a import Int
4+
5+
if TYPE_CHECKING:
6+
from typing import Any
7+
8+
from mod_a import List
9+
10+
def list_int(x: Any) -> List[Int]:
11+
return [x] if isinstance(x, Int) else []
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
classDiagram
2+
class type_check_imports {
3+
}
4+
class mod_a {
5+
}
6+
class mod_b {
7+
}
8+
class mod_c {
9+
}
10+
class mod_d {
11+
}
12+
mod_b --> mod_a
13+
mod_d --> mod_a
14+
mod_c -.-> mod_a

tests/pyreverse/test_writer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
MMD_FILES = ["packages_No_Name.mmd", "classes_No_Name.mmd"]
5050
HTML_FILES = ["packages_No_Name.html", "classes_No_Name.html"]
5151
NO_STANDALONE_FILES = ["classes_no_standalone.dot", "packages_no_standalone.dot"]
52+
TYPE_CHECK_IMPORTS_FILES = [
53+
"packages_type_check_imports.dot",
54+
"classes_type_check_imports.dot",
55+
]
5256

5357

5458
class Config:
@@ -105,6 +109,19 @@ def setup_no_standalone_dot(
105109
yield from _setup(project, no_standalone_dot_config, writer)
106110

107111

112+
@pytest.fixture()
113+
def setup_type_check_imports_dot(
114+
default_config: PyreverseConfig, get_project: GetProjectCallable
115+
) -> Iterator[None]:
116+
writer = DiagramWriter(default_config)
117+
project = get_project(
118+
os.path.join(os.path.dirname(__file__), "functional", "package_diagrams"),
119+
name="type_check_imports",
120+
)
121+
122+
yield from _setup(project, default_config, writer)
123+
124+
108125
@pytest.fixture()
109126
def setup_puml(
110127
puml_config: PyreverseConfig, get_project: GetProjectCallable
@@ -173,6 +190,12 @@ def test_no_standalone_dot_files(generated_file: str) -> None:
173190
_assert_files_are_equal(generated_file)
174191

175192

193+
@pytest.mark.usefixtures("setup_type_check_imports_dot")
194+
@pytest.mark.parametrize("generated_file", TYPE_CHECK_IMPORTS_FILES)
195+
def test_type_check_imports_dot_files(generated_file: str) -> None:
196+
_assert_files_are_equal(generated_file)
197+
198+
176199
@pytest.mark.usefixtures("setup_puml")
177200
@pytest.mark.parametrize("generated_file", PUML_FILES)
178201
def test_puml_files(generated_file: str) -> None:

0 commit comments

Comments
 (0)