From af982da2b00e58ca0a24ab066f2e8e55970761bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikul=C3=A1=C5=A1=20Poul?= Date: Tue, 1 Oct 2024 10:42:01 +0100 Subject: [PATCH] chore: write tests for printing available commands from modules/submodules --- src/management_commands/core.py | 20 ++++++--- tests/test_core.py | 76 +++++++++++++++++++++++++++++++++ tests/test_management.py | 65 ++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 7 deletions(-) diff --git a/src/management_commands/core.py b/src/management_commands/core.py index ef30f9a..12f2ef6 100644 --- a/src/management_commands/core.py +++ b/src/management_commands/core.py @@ -3,6 +3,7 @@ import contextlib import importlib import pkgutil +import typing from contextlib import suppress from django.apps.registry import apps @@ -17,6 +18,9 @@ CommandTypeError, ) +if typing.TYPE_CHECKING: + from collections.abc import Iterator + def import_command_class(dotted_path: str) -> type[BaseCommand]: try: @@ -30,16 +34,18 @@ def import_command_class(dotted_path: str) -> type[BaseCommand]: return command_class +def iterate_modules(dotted_path: str) -> Iterator[str]: + for _, name, is_pkg in pkgutil.iter_modules( + importlib.import_module(dotted_path).__path__, + ): + if not is_pkg and not name.startswith("_"): + yield name + + def _discover_commands_in_module(module: str) -> list[str]: commands: list[str] = [] try: - files_in_dir = [ - name - for _, name, is_pkg in pkgutil.iter_modules( - importlib.import_module(module).__path__, - ) - if not is_pkg and not name.startswith("_") - ] + files_in_dir = list(iterate_modules(module)) except ImportError: # module doesn't exist return commands diff --git a/tests/test_core.py b/tests/test_core.py index e588057..522a444 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,6 +8,7 @@ from management_commands.core import ( get_command_paths, + get_commands_from_modules_and_submodules, import_command_class, load_command_class, ) @@ -119,6 +120,81 @@ def test_get_command_paths_returns_list_of_all_dotted_paths_to_command_classes_i ] +def test_get_commands_from_modules_and_submodules_returns_dictionary_of_available_commands( + mocker: MockerFixture, +) -> None: + # Configure. + mocker.patch.multiple( + "management_commands.conf.settings", + MODULES=[ + "module_a", + "module_b", # no commands + ], + SUBMODULES=[ + "submodule_a", + "submodule_b", # no commands + ], + ) + + # Arrange. + app_config_a_mock = mocker.Mock() + app_config_a_mock.name = "app_a" + app_config_b_mock = mocker.Mock() # no commands + app_config_b_mock.name = "app_b" + + class CommandA: + pass + + class CommandB(BaseCommand): + pass + + # Mock. + mocker.patch( + "management_commands.core.apps.app_configs", + { + "app_a": app_config_a_mock, + "app_b": app_config_b_mock, + }, + ) + + def import_string_side_effect(dotted_path: str) -> type: + if dotted_path == "module_a.command_a.Command": + return CommandA + if dotted_path == "module_a.command_b.Command": + return CommandB + if dotted_path == "app_a.submodule_a.command_a.Command": + return CommandA + if dotted_path == "app_a.submodule_a.command_b.Command": + return CommandB + + raise ImportError + + mocker.patch( + "management_commands.core.import_string", + side_effect=import_string_side_effect, + ) + + def iterate_modules_side_effect(dotted_path: str) -> list[str]: + if dotted_path == "module_a": + return ["command_a", "command_b"] + if dotted_path == "app_a.submodule_a": + return ["command_a", "command_b"] + raise ImportError + + mocker.patch( + "management_commands.core.iterate_modules", + side_effect=iterate_modules_side_effect, + ) + + # Act. + commands = get_commands_from_modules_and_submodules() + + # Assert. + assert set(commands.keys()) == {"module_a", "app_a"} + assert commands["module_a"] == ["command_b"] + assert commands["app_a"] == ["command_b"] + + def test_get_command_paths_returns_list_of_dotted_paths_to_app_submodules_if_app_label_specified( mocker: MockerFixture, ) -> None: diff --git a/tests/test_management.py b/tests/test_management.py index 0ab3849..270ba92 100644 --- a/tests/test_management.py +++ b/tests/test_management.py @@ -56,6 +56,71 @@ def test_execute_from_command_line_help_displays_paths_and_aliases( ) in captured.out +def test_execute_from_command_line_help_displays_modules_and_submodules( + mocker: MockerFixture, + capsys: pytest.CaptureFixture[str], +) -> None: + # Mock. + mocker.patch.multiple( + "management_commands.management.settings", + MODULES=["module_a"], + SUBMODULES=["submodule_a"], + ) + + # Arrange. + app_config_a_mock = mocker.Mock() + app_config_a_mock.name = "app_a" + + class CommandB(BaseCommand): + pass + + # Mock. + mocker.patch( + "management_commands.core.apps.app_configs", + {"app_a": app_config_a_mock}, + ) + + def import_string_side_effect(dotted_path: str) -> type: + if dotted_path == "module_a.command_b.Command": + return CommandB + if dotted_path == "app_a.submodule_a.command_b.Command": + return CommandB + + raise ImportError + + mocker.patch( + "management_commands.core.import_string", + side_effect=import_string_side_effect, + ) + + def iterate_modules_side_effect(dotted_path: str) -> list[str]: + if dotted_path == "module_a": + return ["command_b"] + if dotted_path == "app_a.submodule_a": + return ["command_b"] + raise ImportError + + mocker.patch( + "management_commands.core.iterate_modules", + side_effect=iterate_modules_side_effect, + ) + + # Act. + + execute_from_command_line(["manage.py", "--help"]) + captured = capsys.readouterr() + + # Assert. + assert ( + "[django-management-commands: module_a]\n" + " command_b\n" + "\n" + "[django-management-commands: app_a]\n" + " command_b\n" + "\n" + ) in captured.out + + def test_execute_from_command_line_falls_back_to_django_management_utility_if_command_name_is_not_passed( mocker: MockerFixture, ) -> None: