diff --git a/src/invoke_toolkit/collections.py b/src/invoke_toolkit/collections.py index 44b64fd..39782e0 100644 --- a/src/invoke_toolkit/collections.py +++ b/src/invoke_toolkit/collections.py @@ -250,6 +250,13 @@ def load_local_tasks(self, search_path: str | Path | None = None) -> None: if not local_tasks_file.exists(): debug(f"No local_tasks.py found at {local_tasks_file}") return + if "local" in self.tasks: + logger.warning( + "Skipping local_tasks.py at %s because tasks.py already defines " + "the 'local' task namespace", + local_tasks_file, + ) + return debug(f"Loading local tasks from {local_tasks_file}") diff --git a/tests/test_collection.py b/tests/test_collection.py index 92851f8..ba7ebb6 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -1,10 +1,13 @@ import ast +import logging import os import subprocess import sys from pathlib import Path from textwrap import dedent +from typing import Any, cast +from invoke_toolkit import Task, task from invoke_toolkit.collections import ToolkitCollection @@ -216,6 +219,27 @@ def standalone_task(ctx): assert "standalone-task" in local_col.tasks +def test_load_local_tasks_preserves_conflicting_main_task(tmp_path: Path, caplog): + """A main task named local takes precedence over local_tasks.py.""" + + def main_task(ctx): + pass + + decorated_main_task = cast(Task[Any], task()(main_task)) + ns = ToolkitCollection() + ns.add_task(decorated_main_task, aliases=("local",)) + (tmp_path / "local_tasks.py").write_text( + "from invoke_toolkit import task\n\n@task()\ndef local_task(ctx):\n pass\n" + ) + + with caplog.at_level(logging.WARNING, logger="invoke"): + ns.load_local_tasks(search_path=tmp_path) + + assert ns.tasks["local"] is decorated_main_task + assert "local" not in ns.collections + assert "Skipping local_tasks.py" in caplog.text + + def test_load_local_tasks_from_multiple_directories_without_module_collision( tmp_path: Path, ):