Skip to content

Pyreverse: print package import stats #8974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 31, 2023
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/8973.user_action
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.

Closes #8973
16 changes: 11 additions & 5 deletions pylint/pyreverse/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@
from pylint import constants
from pylint.pyreverse import utils

_WrapperFuncT = Callable[[Callable[[str], nodes.Module], str], Optional[nodes.Module]]
_WrapperFuncT = Callable[
[Callable[[str], nodes.Module], str, bool], Optional[nodes.Module]
]


def _astroid_wrapper(
func: Callable[[str], nodes.Module], modname: str
func: Callable[[str], nodes.Module],
modname: str,
verbose: bool = False,
) -> nodes.Module | None:
print(f"parsing {modname}...")
if verbose:
print(f"parsing {modname}...")
try:
return func(modname)
except astroid.exceptions.AstroidBuildingException as exc:
Expand Down Expand Up @@ -344,6 +349,7 @@ def project_from_files(
func_wrapper: _WrapperFuncT = _astroid_wrapper,
project_name: str = "no name",
black_list: tuple[str, ...] = constants.DEFAULT_IGNORE_LIST,
verbose: bool = False,
) -> Project:
"""Return a Project from a list of files or modules."""
# build the project representation
Expand All @@ -356,7 +362,7 @@ def project_from_files(
fpath = os.path.join(something, "__init__.py")
else:
fpath = something
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
ast = func_wrapper(astroid_manager.ast_from_file, fpath, verbose)
if ast is None:
continue
project.path = project.path or ast.file
Expand All @@ -368,7 +374,7 @@ def project_from_files(
for fpath in astroid.modutils.get_module_files(
os.path.dirname(ast.file), black_list
):
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
ast = func_wrapper(astroid_manager.ast_from_file, fpath, verbose)
if ast is None or ast.name == base_name:
continue
project.add_module(ast)
Expand Down
9 changes: 9 additions & 0 deletions pylint/pyreverse/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@
"used to determine a package namespace for modules located under the source root.",
},
),
(
"verbose",
{
"action": "store_true",
"default": False,
"help": "Makes pyreverse more verbose/talkative. Mostly useful for debugging.",
},
),
)


Expand Down Expand Up @@ -301,6 +309,7 @@ def run(self, args: list[str]) -> int:
args,
project_name=self.config.project,
black_list=self.config.ignore_list,
verbose=self.config.verbose,
)
linker = Linker(project, tag=True)
handler = DiadefsHandler(self.config)
Expand Down
34 changes: 30 additions & 4 deletions pylint/pyreverse/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ def write(self, diadefs: Iterable[ClassDiagram | PackageDiagram]) -> None:

def write_packages(self, diagram: PackageDiagram) -> None:
"""Write a package diagram."""
module_info: dict[str, dict[str, int]] = {}

# sorted to get predictable (hence testable) results
for module in sorted(diagram.modules(), key=lambda x: x.title):
module.fig_id = module.node.qname()

if self.config.no_standalone and not any(
module in (rel.from_object, rel.to_object)
for rel in diagram.get_relationships("depends")
Expand All @@ -68,21 +71,44 @@ def write_packages(self, diagram: PackageDiagram) -> None:
type_=NodeType.PACKAGE,
properties=self.get_package_properties(module),
)

module_info[module.fig_id] = {
"imports": 0,
"imported": 0,
}

# package dependencies
for rel in diagram.get_relationships("depends"):
from_id = rel.from_object.fig_id
to_id = rel.to_object.fig_id

self.printer.emit_edge(
rel.from_object.fig_id,
rel.to_object.fig_id,
from_id,
to_id,
type_=EdgeType.USES,
)

module_info[from_id]["imports"] += 1
module_info[to_id]["imported"] += 1

for rel in diagram.get_relationships("type_depends"):
from_id = rel.from_object.fig_id
to_id = rel.to_object.fig_id

self.printer.emit_edge(
rel.from_object.fig_id,
rel.to_object.fig_id,
from_id,
to_id,
type_=EdgeType.TYPE_DEPENDENCY,
)

module_info[from_id]["imports"] += 1
module_info[to_id]["imported"] += 1

print(
f"Analysed {len(module_info)} modules with a total "
f"of {sum(mod['imports'] for mod in module_info.values())} imports"
)

def write_classes(self, diagram: ClassDiagram) -> None:
"""Write a class diagram."""
# sorted to get predictable (hence testable) results
Expand Down
4 changes: 3 additions & 1 deletion tests/pyreverse/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ def get_project() -> GetProjectCallable:
def _get_project(module: str, name: str | None = "No Name") -> Project:
"""Return an astroid project representation."""

def _astroid_wrapper(func: Callable[[str], Module], modname: str) -> Module:
def _astroid_wrapper(
func: Callable[[str], Module], modname: str, _verbose: bool = False
) -> Module:
return func(modname)

with augmented_sys_path([discover_package_path(module, [])]):
Expand Down
12 changes: 12 additions & 0 deletions tests/pyreverse/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> None:
assert wrapped_sysexit.value.code == 32


@mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.writer")
@pytest.mark.usefixtures("mock_graphviz")
def test_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None:
"""Test the --verbose flag."""
with pytest.raises(SystemExit):
# we have to catch the SystemExit so the test execution does not stop
main.Run(["--verbose", TEST_DATA_DIR])
assert "parsing" in capsys.readouterr().out


@pytest.mark.parametrize(
("arg", "expected_default"),
[
Expand Down