Skip to content

Commit 4d6f44b

Browse files
committed
Fix editable install finder handling of nested packages (#4020)
2 parents 71b355b + dcee086 commit 4d6f44b

File tree

5 files changed

+58
-21
lines changed

5 files changed

+58
-21
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@
232232
html_static_path = ['images'] # should contain the folder with icons
233233

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

237237
# List of dicts with <link> HTML attributes
238238
# static-file points to files in the html_static_path (href is computed)

newsfragments/4020.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix editable install finder handling of nested packages, by only handling 1
2+
level of nesting and relying on ``importlib.machinery`` to find the remaining
3+
modules based on the parent package path.

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ testing-integration =
9292

9393
docs =
9494
# upstream
95-
sphinx >= 3.5
95+
sphinx >= 3.5,<=7.1.2 # workaround, see comments in pypa/setuptools#4020
9696
jaraco.packaging >= 9.3
9797
rst.linker >= 1.9
9898
furo

setuptools/command/editable_wheel.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -782,11 +782,15 @@ def find_spec(cls, fullname, path=None, target=None):
782782
pkg_path = MAPPING[fullname]
783783
return cls._find_spec(fullname, Path(pkg_path))
784784
785-
# Nested modules (apparently required for namespaces to work)
786-
for pkg, pkg_path in reversed(list(MAPPING.items())):
787-
if fullname.startswith(f"{{pkg}}."):
788-
return cls._find_nested_spec(fullname, pkg, pkg_path)
789-
785+
# Handle immediate children modules (required for namespaces to work)
786+
# To avoid problems with case sensitivity in the file system we delegate
787+
# to the importlib.machinery implementation.
788+
parent, _, child = fullname.rpartition(".")
789+
if parent and parent in MAPPING:
790+
return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
791+
792+
# Other levels of nesting should be handled automatically by importlib
793+
# using the parent path.
790794
return None
791795
792796
@classmethod
@@ -797,20 +801,6 @@ def _find_spec(cls, fullname, candidate_path):
797801
if candidate.exists():
798802
return spec_from_file_location(fullname, candidate)
799803
800-
@classmethod
801-
def _find_nested_spec(cls, fullname, parent, parent_path):
802-
'''
803-
To avoid problems with case sensitivity in the file system we delegate to the
804-
importlib.machinery implementation.
805-
'''
806-
rest = fullname.replace(parent, "", 1).strip(".")
807-
nested = PathFinder.find_spec(rest, path=[parent_path])
808-
return nested and spec_from_file_location(
809-
fullname,
810-
nested.origin,
811-
submodule_search_locations=nested.submodule_search_locations
812-
)
813-
814804
815805
class _EditableNamespaceFinder: # PathEntryFinder
816806
@classmethod

setuptools/tests/test_editable_install.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,50 @@ def test_namespace_case_sensitivity(self, tmp_path):
660660
with pytest.raises(ImportError, match="\'ns\\.othername\\.foo\\.BAR\\'"):
661661
import_module("ns.othername.foo.BAR")
662662

663+
def test_intermediate_packages(self, tmp_path):
664+
"""
665+
The finder should not import ``fullname`` if the intermediate segments
666+
don't exist (see pypa/setuptools#4019).
667+
"""
668+
files = {
669+
"src": {
670+
"mypkg": {
671+
"__init__.py": "",
672+
"config.py": "a = 13",
673+
"helloworld.py": "b = 13",
674+
"components": {
675+
"config.py": "a = 37",
676+
},
677+
},
678+
}
679+
}
680+
jaraco.path.build(files, prefix=tmp_path)
681+
682+
mapping = {"mypkg": str(tmp_path / "src/mypkg")}
683+
template = _finder_template(str(uuid4()), mapping, {})
684+
685+
with contexts.save_paths(), contexts.save_sys_modules():
686+
for mod in (
687+
"mypkg",
688+
"mypkg.config",
689+
"mypkg.helloworld",
690+
"mypkg.components",
691+
"mypkg.components.config",
692+
"mypkg.components.helloworld",
693+
):
694+
sys.modules.pop(mod, None)
695+
696+
self.install_finder(template)
697+
698+
config = import_module("mypkg.components.config")
699+
assert config.a == 37
700+
701+
helloworld = import_module("mypkg.helloworld")
702+
assert helloworld.b == 13
703+
704+
with pytest.raises(ImportError):
705+
import_module("mypkg.components.helloworld")
706+
663707

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

0 commit comments

Comments
 (0)