Skip to content

Commit aa8b815

Browse files
committed
Look at task tree sibling location for conf files after 2.1
Fixes #944
1 parent b2da4d9 commit aa8b815

File tree

6 files changed

+44
-12
lines changed

6 files changed

+44
-12
lines changed

invoke/loader.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33
from importlib.machinery import ModuleSpec
44
from importlib.util import module_from_spec, spec_from_file_location
5+
from pathlib import Path
56
from types import ModuleType
67
from typing import Any, Optional, Tuple
78

@@ -68,18 +69,28 @@ def load(self, name: Optional[str] = None) -> Tuple[ModuleType, str]:
6869
name = self.config.tasks.collection_name
6970
spec = self.find(name)
7071
if spec and spec.loader and spec.origin:
71-
path = spec.origin
72-
# Ensure containing directory is on sys.path in case the module
73-
# being imported is trying to load local-to-it names.
74-
if os.path.isfile(spec.origin):
75-
path = os.path.dirname(spec.origin)
76-
if path not in sys.path:
77-
sys.path.insert(0, path)
72+
# Typically either tasks.py or tasks/__init__.py
73+
source_file = Path(spec.origin)
74+
# Will be 'the dir tasks.py is in', or 'tasks/', in both cases this
75+
# is what wants to be in sys.path for "from . import sibling"
76+
enclosing_dir = source_file.parent
77+
# Will be "the directory above the spot that 'import tasks' found",
78+
# namely the parent of "your task tree", i.e. "where project level
79+
# config files are looked for". So, same as enclosing_dir for
80+
# tasks.py, but one more level up for tasks/__init__.py...
81+
module_parent = enclosing_dir
82+
if spec.parent: # it's a package, so we have to go up again
83+
module_parent = module_parent.parent
84+
# Get the enclosing dir on the path
85+
enclosing_str = str(enclosing_dir)
86+
if enclosing_str not in sys.path:
87+
sys.path.insert(0, enclosing_str)
7888
# Actual import
7989
module = module_from_spec(spec)
8090
sys.modules[spec.name] = module # so 'from . import xxx' works
8191
spec.loader.exec_module(module)
82-
return module, os.path.dirname(spec.origin)
92+
# Return the module and the folder it was found in
93+
return module, str(module_parent)
8394
msg = "ImportError loading {!r}, raising ImportError"
8495
debug(msg.format(name))
8596
raise ImportError

sites/www/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changelog
33
=========
44

5+
- :bug:`944` After the release of 2.1, package-style task modules started
6+
looking in the wrong place for project-level config files (inside one's eg
7+
``tasks/`` dir, instead of *next to* that dir) due to a subtlety in the new
8+
import/discovery mechanism used. This has been fixed. Thanks to Arnaud V. and
9+
Hunter Kelly for the reports and to Jesse P. Johnson for initial
10+
debugging/diagnosis.
511
- :release:`2.1.2 <2023-05-15>`
612
- :support:`936 backported` Make sure ``py.typed`` is in our packaging
713
manifest; without it, users working from a regular installation
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
outer:
2+
inner:
3+
hooray: "package"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from invoke import task
2+
3+
4+
@task
5+
def mytask(c):
6+
assert c.outer.inner.hooray == "package"

tests/loader.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ def doesnt_duplicate_parent_dir_addition(self):
6161
def can_load_package(self):
6262
loader = _BasicLoader()
6363
# Load itself doesn't explode (tests 'from . import xxx' internally)
64-
mod, loc = loader.load("package")
64+
mod, enclosing_dir = loader.load("package")
6565
# Properties of returned values look as expected
66-
package = Path(support) / "package"
67-
assert loc == str(package)
68-
assert mod.__file__ == str(package / "__init__.py")
66+
# (enclosing dir is always the one above the module-or-package)
67+
assert enclosing_dir == support
68+
assert mod.__file__ == str(Path(support) / "package" / "__init__.py")
6969

7070
def load_name_defaults_to_config_tasks_collection_name(self):
7171
"load() name defaults to config.tasks.collection_name"

tests/program.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import sys
44
from io import BytesIO
5+
from pathlib import Path
56

67
from invoke.util import Lexicon
78
from unittest.mock import patch, Mock, ANY
@@ -35,6 +36,7 @@
3536
skip_if_windows,
3637
support_file,
3738
support_path,
39+
support,
3840
)
3941

4042

@@ -241,6 +243,10 @@ def uses_loader_class_given(self):
241243
Program(loader_class=klass).run("myapp --help foo", exit=False)
242244
klass.assert_called_with(start=ANY, config=ANY)
243245

246+
def config_location_correct_for_package_type_task_trees(self):
247+
with cd(Path(support) / "configs" / "package"):
248+
expect("mytask") # will assert if config not loaded right
249+
244250
class execute:
245251
def uses_executor_class_given(self):
246252
klass = Mock()

0 commit comments

Comments
 (0)