Skip to content

Commit 0adb305

Browse files
committed
Handle accidental virtual namespaces in editable install (#3512)
2 parents 8f2cc1f + ce980c1 commit 0adb305

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

changelog.d/3512.misc.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added capability of handling namespace packages created
2+
accidentally/purposefully via discovery configuration during editable installs.
3+
This should emulate the behaviour of a non-editable installation.

setuptools/command/editable_wheel.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,14 @@ def _absolute_root(path: _Path) -> str:
628628
def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
629629
"""By carefully designing ``package_dir``, it is possible to implement the logical
630630
structure of PEP 420 in a package without the corresponding directories.
631-
This function will try to find this kind of namespaces.
631+
632+
Moreover a parent package can be purposefully/accidentally skipped in the discovery
633+
phase (e.g. ``find_packages(include=["mypkg.*"])``, when ``mypkg.foo`` is included
634+
by ``mypkg`` itself is not).
635+
We consider this case to also be a virtual namespace (ignoring the original
636+
directory) to emulate a non-editable installation.
637+
638+
This function will try to find these kinds of namespaces.
632639
"""
633640
for pkg in pkg_roots:
634641
if "." not in pkg:
@@ -637,7 +644,8 @@ def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
637644
for i in range(len(parts) - 1, 0, -1):
638645
partial_name = ".".join(parts[:i])
639646
path = Path(find_package_path(partial_name, pkg_roots, ""))
640-
if not path.exists():
647+
if not path.exists() or partial_name not in pkg_roots:
648+
# partial_name not in pkg_roots ==> purposefully/accidentally skipped
641649
yield partial_name
642650

643651

setuptools/tests/test_editable_install.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,54 @@ def test_namespace_created_via_package_dir(self, venv, tmp_path, editable_opts):
269269
venv.run(["python", "-m", "pip", "install", "-e", str(pkg_C), *opts])
270270
venv.run(["python", "-c", "from myns.n import pkgA, pkgB, pkgC"])
271271

272+
def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path):
273+
"""Sometimes users might specify an ``include`` pattern that ignores parent
274+
packages. In a normal installation this would ignore all modules inside the
275+
parent packages, and make them namespaces (reported in issue #3504),
276+
so the editable mode should preserve this behaviour.
277+
"""
278+
files = {
279+
"pkgA": {
280+
"pyproject.toml": dedent("""\
281+
[build-system]
282+
requires = ["setuptools", "wheel"]
283+
build-backend = "setuptools.build_meta"
284+
285+
[project]
286+
name = "pkgA"
287+
version = "3.14159"
288+
289+
[tool.setuptools]
290+
packages.find.include = ["mypkg.*"]
291+
"""),
292+
"mypkg": {
293+
"__init__.py": "",
294+
"other.py": "b = 1",
295+
"n": {
296+
"__init__.py": "",
297+
"pkgA.py": "a = 1",
298+
},
299+
},
300+
"MANIFEST.in": EXAMPLE["MANIFEST.in"],
301+
},
302+
}
303+
jaraco.path.build(files, prefix=tmp_path)
304+
pkg_A = tmp_path / "pkgA"
305+
306+
# use pip to install to the target directory
307+
opts = ["--no-build-isolation"] # force current version of setuptools
308+
venv.run(["python", "-m", "pip", "-v", "install", "-e", str(pkg_A), *opts])
309+
out = venv.run(["python", "-c", "from mypkg.n import pkgA; print(pkgA.a)"])
310+
assert str(out, "utf-8").strip() == "1"
311+
cmd = """\
312+
try:
313+
import mypkg.other
314+
except ImportError:
315+
print("mypkg.other not defined")
316+
"""
317+
out = venv.run(["python", "-c", dedent(cmd)])
318+
assert "mypkg.other not defined" in str(out, "utf-8")
319+
272320

273321
# Moved here from test_develop:
274322
@pytest.mark.xfail(
@@ -490,7 +538,7 @@ def test_pkg_roots(tmp_path):
490538
assert ns == {"f", "f.g"}
491539

492540
ns = set(_find_virtual_namespaces(roots))
493-
assert ns == {"a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"}
541+
assert ns == {"a.b", "a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"}
494542

495543

496544
class TestOverallBehaviour:

0 commit comments

Comments
 (0)