Skip to content

Commit

Permalink
chore: write tests for printing available commands from modules/submo…
Browse files Browse the repository at this point in the history
…dules
  • Loading branch information
mikicz committed Oct 1, 2024
1 parent da92bb1 commit af982da
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 7 deletions.
20 changes: 13 additions & 7 deletions src/management_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import contextlib
import importlib
import pkgutil
import typing
from contextlib import suppress

from django.apps.registry import apps
Expand All @@ -17,6 +18,9 @@
CommandTypeError,
)

if typing.TYPE_CHECKING:
from collections.abc import Iterator

Check warning on line 22 in src/management_commands/core.py

View check run for this annotation

Codecov / codecov/patch

src/management_commands/core.py#L22

Added line #L22 was not covered by tests


def import_command_class(dotted_path: str) -> type[BaseCommand]:
try:
Expand All @@ -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

Check warning on line 42 in src/management_commands/core.py

View check run for this annotation

Codecov / codecov/patch

src/management_commands/core.py#L42

Added line #L42 was not covered by tests


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

Expand Down
76 changes: 76 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from management_commands.core import (
get_command_paths,
get_commands_from_modules_and_submodules,
import_command_class,
load_command_class,
)
Expand Down Expand Up @@ -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:
Expand Down
65 changes: 65 additions & 0 deletions tests/test_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit af982da

Please sign in to comment.