Skip to content

Commit dcebc74

Browse files
Pyreverse: print package import stats (#8974)
* Add --verbose flag to Pyreverse Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6f3030e commit dcebc74

File tree

6 files changed

+68
-10
lines changed

6 files changed

+68
-10
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Package stats are now printed when running Pyreverse and a ``--verbose`` flag was added to get the original output with parsed modules. You might need to activate the verbose option if you want to keep the old output.
2+
3+
Closes #8973

pylint/pyreverse/inspector.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
from pylint import constants
2222
from pylint.pyreverse import utils
2323

24-
_WrapperFuncT = Callable[[Callable[[str], nodes.Module], str], Optional[nodes.Module]]
24+
_WrapperFuncT = Callable[
25+
[Callable[[str], nodes.Module], str, bool], Optional[nodes.Module]
26+
]
2527

2628

2729
def _astroid_wrapper(
28-
func: Callable[[str], nodes.Module], modname: str
30+
func: Callable[[str], nodes.Module],
31+
modname: str,
32+
verbose: bool = False,
2933
) -> nodes.Module | None:
30-
print(f"parsing {modname}...")
34+
if verbose:
35+
print(f"parsing {modname}...")
3136
try:
3237
return func(modname)
3338
except astroid.exceptions.AstroidBuildingException as exc:
@@ -344,6 +349,7 @@ def project_from_files(
344349
func_wrapper: _WrapperFuncT = _astroid_wrapper,
345350
project_name: str = "no name",
346351
black_list: tuple[str, ...] = constants.DEFAULT_IGNORE_LIST,
352+
verbose: bool = False,
347353
) -> Project:
348354
"""Return a Project from a list of files or modules."""
349355
# build the project representation
@@ -356,7 +362,7 @@ def project_from_files(
356362
fpath = os.path.join(something, "__init__.py")
357363
else:
358364
fpath = something
359-
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
365+
ast = func_wrapper(astroid_manager.ast_from_file, fpath, verbose)
360366
if ast is None:
361367
continue
362368
project.path = project.path or ast.file
@@ -368,7 +374,7 @@ def project_from_files(
368374
for fpath in astroid.modutils.get_module_files(
369375
os.path.dirname(ast.file), black_list
370376
):
371-
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
377+
ast = func_wrapper(astroid_manager.ast_from_file, fpath, verbose)
372378
if ast is None or ast.name == base_name:
373379
continue
374380
project.add_module(ast)

pylint/pyreverse/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@
255255
"used to determine a package namespace for modules located under the source root.",
256256
},
257257
),
258+
(
259+
"verbose",
260+
{
261+
"action": "store_true",
262+
"default": False,
263+
"help": "Makes pyreverse more verbose/talkative. Mostly useful for debugging.",
264+
},
265+
),
258266
)
259267

260268

@@ -301,6 +309,7 @@ def run(self, args: list[str]) -> int:
301309
args,
302310
project_name=self.config.project,
303311
black_list=self.config.ignore_list,
312+
verbose=self.config.verbose,
304313
)
305314
linker = Linker(project, tag=True)
306315
handler = DiadefsHandler(self.config)

pylint/pyreverse/writer.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@ def write(self, diadefs: Iterable[ClassDiagram | PackageDiagram]) -> None:
5454

5555
def write_packages(self, diagram: PackageDiagram) -> None:
5656
"""Write a package diagram."""
57+
module_info: dict[str, dict[str, int]] = {}
58+
5759
# sorted to get predictable (hence testable) results
5860
for module in sorted(diagram.modules(), key=lambda x: x.title):
5961
module.fig_id = module.node.qname()
62+
6063
if self.config.no_standalone and not any(
6164
module in (rel.from_object, rel.to_object)
6265
for rel in diagram.get_relationships("depends")
@@ -68,21 +71,44 @@ def write_packages(self, diagram: PackageDiagram) -> None:
6871
type_=NodeType.PACKAGE,
6972
properties=self.get_package_properties(module),
7073
)
74+
75+
module_info[module.fig_id] = {
76+
"imports": 0,
77+
"imported": 0,
78+
}
79+
7180
# package dependencies
7281
for rel in diagram.get_relationships("depends"):
82+
from_id = rel.from_object.fig_id
83+
to_id = rel.to_object.fig_id
84+
7385
self.printer.emit_edge(
74-
rel.from_object.fig_id,
75-
rel.to_object.fig_id,
86+
from_id,
87+
to_id,
7688
type_=EdgeType.USES,
7789
)
7890

91+
module_info[from_id]["imports"] += 1
92+
module_info[to_id]["imported"] += 1
93+
7994
for rel in diagram.get_relationships("type_depends"):
95+
from_id = rel.from_object.fig_id
96+
to_id = rel.to_object.fig_id
97+
8098
self.printer.emit_edge(
81-
rel.from_object.fig_id,
82-
rel.to_object.fig_id,
99+
from_id,
100+
to_id,
83101
type_=EdgeType.TYPE_DEPENDENCY,
84102
)
85103

104+
module_info[from_id]["imports"] += 1
105+
module_info[to_id]["imported"] += 1
106+
107+
print(
108+
f"Analysed {len(module_info)} modules with a total "
109+
f"of {sum(mod['imports'] for mod in module_info.values())} imports"
110+
)
111+
86112
def write_classes(self, diagram: ClassDiagram) -> None:
87113
"""Write a class diagram."""
88114
# sorted to get predictable (hence testable) results

tests/pyreverse/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ def get_project() -> GetProjectCallable:
7272
def _get_project(module: str, name: str | None = "No Name") -> Project:
7373
"""Return an astroid project representation."""
7474

75-
def _astroid_wrapper(func: Callable[[str], Module], modname: str) -> Module:
75+
def _astroid_wrapper(
76+
func: Callable[[str], Module], modname: str, _verbose: bool = False
77+
) -> Module:
7678
return func(modname)
7779

7880
with augmented_sys_path([discover_package_path(module, [])]):

tests/pyreverse/test_main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> None:
128128
assert wrapped_sysexit.value.code == 32
129129

130130

131+
@mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
132+
@mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock())
133+
@mock.patch("pylint.pyreverse.main.writer")
134+
@pytest.mark.usefixtures("mock_graphviz")
135+
def test_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None:
136+
"""Test the --verbose flag."""
137+
with pytest.raises(SystemExit):
138+
# we have to catch the SystemExit so the test execution does not stop
139+
main.Run(["--verbose", TEST_DATA_DIR])
140+
assert "parsing" in capsys.readouterr().out
141+
142+
131143
@pytest.mark.parametrize(
132144
("arg", "expected_default"),
133145
[

0 commit comments

Comments
 (0)