diff --git a/docs/conf.py b/docs/conf.py index bd4ffdbadb..29f2c8bb10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 HTML attributes # static-file points to files in the html_static_path (href is computed) diff --git a/newsfragments/4020.bugfix.rst b/newsfragments/4020.bugfix.rst new file mode 100644 index 0000000000..d77b448ecd --- /dev/null +++ b/newsfragments/4020.bugfix.rst @@ -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. diff --git a/setup.cfg b/setup.cfg index 5388a8c873..d8ccd07f03 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 91c215f2aa..7f66f7e3e9 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -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 @@ -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 diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 0265611945..e58168b0cf 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -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.