Skip to content

Commit

Permalink
Fix editable install finder handling of nested packages (#4020)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed Aug 18, 2023
2 parents 71b355b + dcee086 commit 4d6f44b
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 21 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@
html_static_path = ['images'] # should contain the folder with icons

# Add support for nice Not Found 404 pages
extensions += ['notfound.extension']
# extensions += ['notfound.extension'] # readthedocs/sphinx-notfound-page#219

# List of dicts with <link> HTML attributes
# static-file points to files in the html_static_path (href is computed)
Expand Down
3 changes: 3 additions & 0 deletions newsfragments/4020.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix editable install finder handling of nested packages, by only handling 1
level of nesting and relying on ``importlib.machinery`` to find the remaining
modules based on the parent package path.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ testing-integration =

docs =
# upstream
sphinx >= 3.5
sphinx >= 3.5,<=7.1.2 # workaround, see comments in pypa/setuptools#4020
jaraco.packaging >= 9.3
rst.linker >= 1.9
furo
Expand Down
28 changes: 9 additions & 19 deletions setuptools/command/editable_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,11 +782,15 @@ def find_spec(cls, fullname, path=None, target=None):
pkg_path = MAPPING[fullname]
return cls._find_spec(fullname, Path(pkg_path))
# Nested modules (apparently required for namespaces to work)
for pkg, pkg_path in reversed(list(MAPPING.items())):
if fullname.startswith(f"{{pkg}}."):
return cls._find_nested_spec(fullname, pkg, pkg_path)
# Handle immediate children modules (required for namespaces to work)
# To avoid problems with case sensitivity in the file system we delegate
# to the importlib.machinery implementation.
parent, _, child = fullname.rpartition(".")
if parent and parent in MAPPING:
return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
# Other levels of nesting should be handled automatically by importlib
# using the parent path.
return None
@classmethod
Expand All @@ -797,20 +801,6 @@ def _find_spec(cls, fullname, candidate_path):
if candidate.exists():
return spec_from_file_location(fullname, candidate)
@classmethod
def _find_nested_spec(cls, fullname, parent, parent_path):
'''
To avoid problems with case sensitivity in the file system we delegate to the
importlib.machinery implementation.
'''
rest = fullname.replace(parent, "", 1).strip(".")
nested = PathFinder.find_spec(rest, path=[parent_path])
return nested and spec_from_file_location(
fullname,
nested.origin,
submodule_search_locations=nested.submodule_search_locations
)
class _EditableNamespaceFinder: # PathEntryFinder
@classmethod
Expand Down
44 changes: 44 additions & 0 deletions setuptools/tests/test_editable_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,50 @@ def test_namespace_case_sensitivity(self, tmp_path):
with pytest.raises(ImportError, match="\'ns\\.othername\\.foo\\.BAR\\'"):
import_module("ns.othername.foo.BAR")

def test_intermediate_packages(self, tmp_path):
"""
The finder should not import ``fullname`` if the intermediate segments
don't exist (see pypa/setuptools#4019).
"""
files = {
"src": {
"mypkg": {
"__init__.py": "",
"config.py": "a = 13",
"helloworld.py": "b = 13",
"components": {
"config.py": "a = 37",
},
},
}
}
jaraco.path.build(files, prefix=tmp_path)

mapping = {"mypkg": str(tmp_path / "src/mypkg")}
template = _finder_template(str(uuid4()), mapping, {})

with contexts.save_paths(), contexts.save_sys_modules():
for mod in (
"mypkg",
"mypkg.config",
"mypkg.helloworld",
"mypkg.components",
"mypkg.components.config",
"mypkg.components.helloworld",
):
sys.modules.pop(mod, None)

self.install_finder(template)

config = import_module("mypkg.components.config")
assert config.a == 37

helloworld = import_module("mypkg.helloworld")
assert helloworld.b == 13

with pytest.raises(ImportError):
import_module("mypkg.components.helloworld")


def test_pkg_roots(tmp_path):
"""This test focus in getting a particular implementation detail right.
Expand Down

0 comments on commit 4d6f44b

Please sign in to comment.