Skip to content

Commit

Permalink
Look at task tree sibling location for conf files after 2.1
Browse files Browse the repository at this point in the history
Fixes #944
  • Loading branch information
bitprophet committed Jun 14, 2023
1 parent b2da4d9 commit aa8b815
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 12 deletions.
27 changes: 19 additions & 8 deletions invoke/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from importlib.machinery import ModuleSpec
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from types import ModuleType
from typing import Any, Optional, Tuple

Expand Down Expand Up @@ -68,18 +69,28 @@ def load(self, name: Optional[str] = None) -> Tuple[ModuleType, str]:
name = self.config.tasks.collection_name
spec = self.find(name)
if spec and spec.loader and spec.origin:
path = spec.origin
# Ensure containing directory is on sys.path in case the module
# being imported is trying to load local-to-it names.
if os.path.isfile(spec.origin):
path = os.path.dirname(spec.origin)
if path not in sys.path:
sys.path.insert(0, path)
# Typically either tasks.py or tasks/__init__.py
source_file = Path(spec.origin)
# Will be 'the dir tasks.py is in', or 'tasks/', in both cases this
# is what wants to be in sys.path for "from . import sibling"
enclosing_dir = source_file.parent
# Will be "the directory above the spot that 'import tasks' found",
# namely the parent of "your task tree", i.e. "where project level
# config files are looked for". So, same as enclosing_dir for
# tasks.py, but one more level up for tasks/__init__.py...
module_parent = enclosing_dir
if spec.parent: # it's a package, so we have to go up again
module_parent = module_parent.parent
# Get the enclosing dir on the path
enclosing_str = str(enclosing_dir)
if enclosing_str not in sys.path:
sys.path.insert(0, enclosing_str)
# Actual import
module = module_from_spec(spec)
sys.modules[spec.name] = module # so 'from . import xxx' works
spec.loader.exec_module(module)
return module, os.path.dirname(spec.origin)
# Return the module and the folder it was found in
return module, str(module_parent)
msg = "ImportError loading {!r}, raising ImportError"
debug(msg.format(name))
raise ImportError
Expand Down
6 changes: 6 additions & 0 deletions sites/www/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changelog
=========

- :bug:`944` After the release of 2.1, package-style task modules started
looking in the wrong place for project-level config files (inside one's eg
``tasks/`` dir, instead of *next to* that dir) due to a subtlety in the new
import/discovery mechanism used. This has been fixed. Thanks to Arnaud V. and
Hunter Kelly for the reports and to Jesse P. Johnson for initial
debugging/diagnosis.
- :release:`2.1.2 <2023-05-15>`
- :support:`936 backported` Make sure ``py.typed`` is in our packaging
manifest; without it, users working from a regular installation
Expand Down
3 changes: 3 additions & 0 deletions tests/_support/configs/package/invoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
outer:
inner:
hooray: "package"
6 changes: 6 additions & 0 deletions tests/_support/configs/package/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from invoke import task


@task
def mytask(c):
assert c.outer.inner.hooray == "package"
8 changes: 4 additions & 4 deletions tests/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ def doesnt_duplicate_parent_dir_addition(self):
def can_load_package(self):
loader = _BasicLoader()
# Load itself doesn't explode (tests 'from . import xxx' internally)
mod, loc = loader.load("package")
mod, enclosing_dir = loader.load("package")
# Properties of returned values look as expected
package = Path(support) / "package"
assert loc == str(package)
assert mod.__file__ == str(package / "__init__.py")
# (enclosing dir is always the one above the module-or-package)
assert enclosing_dir == support
assert mod.__file__ == str(Path(support) / "package" / "__init__.py")

def load_name_defaults_to_config_tasks_collection_name(self):
"load() name defaults to config.tasks.collection_name"
Expand Down
6 changes: 6 additions & 0 deletions tests/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import sys
from io import BytesIO
from pathlib import Path

from invoke.util import Lexicon
from unittest.mock import patch, Mock, ANY
Expand Down Expand Up @@ -35,6 +36,7 @@
skip_if_windows,
support_file,
support_path,
support,
)


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

def config_location_correct_for_package_type_task_trees(self):
with cd(Path(support) / "configs" / "package"):
expect("mytask") # will assert if config not loaded right

class execute:
def uses_executor_class_given(self):
klass = Mock()
Expand Down

0 comments on commit aa8b815

Please sign in to comment.