| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263 |
- from __future__ import annotations
- import os
- import platform
- import stat
- import subprocess
- import sys
- from copy import deepcopy
- from importlib import import_module
- from importlib.machinery import EXTENSION_SUFFIXES
- from pathlib import Path
- from textwrap import dedent
- from typing import Any
- from unittest.mock import Mock
- from uuid import uuid4
- import jaraco.envs
- import jaraco.path
- import pytest
- from path import Path as _Path
- from setuptools._importlib import resources as importlib_resources
- from setuptools.command.editable_wheel import (
- _encode_pth,
- _find_namespaces,
- _find_package_roots,
- _find_virtual_namespaces,
- _finder_template,
- _LinkTree,
- _TopLevelFinder,
- editable_wheel,
- )
- from setuptools.dist import Distribution
- from setuptools.extension import Extension
- from setuptools.warnings import SetuptoolsDeprecationWarning
- from . import contexts, namespaces
- from distutils.core import run_setup
- @pytest.fixture(params=["strict", "lenient"])
- def editable_opts(request):
- if request.param == "strict":
- return ["--config-settings", "editable-mode=strict"]
- return []
- EXAMPLE = {
- 'pyproject.toml': dedent(
- """\
- [build-system]
- requires = ["setuptools"]
- build-backend = "setuptools.build_meta"
- [project]
- name = "mypkg"
- version = "3.14159"
- license = {text = "MIT"}
- description = "This is a Python package"
- dynamic = ["readme"]
- classifiers = [
- "Development Status :: 5 - Production/Stable",
- "Intended Audience :: Developers"
- ]
- urls = {Homepage = "https://github.com"}
- [tool.setuptools]
- package-dir = {"" = "src"}
- packages = {find = {where = ["src"]}}
- license-files = ["LICENSE*"]
- [tool.setuptools.dynamic]
- readme = {file = "README.rst"}
- [tool.distutils.egg_info]
- tag-build = ".post0"
- """
- ),
- "MANIFEST.in": dedent(
- """\
- global-include *.py *.txt
- global-exclude *.py[cod]
- prune dist
- prune build
- """
- ).strip(),
- "README.rst": "This is a ``README``",
- "LICENSE.txt": "---- placeholder MIT license ----",
- "src": {
- "mypkg": {
- "__init__.py": dedent(
- """\
- import sys
- from importlib.metadata import PackageNotFoundError, version
- try:
- __version__ = version(__name__)
- except PackageNotFoundError:
- __version__ = "unknown"
- """
- ),
- "__main__.py": dedent(
- """\
- from importlib.resources import read_text
- from . import __version__, __name__ as parent
- from .mod import x
- data = read_text(parent, "data.txt")
- print(__version__, data, x)
- """
- ),
- "mod.py": "x = ''",
- "data.txt": "Hello World",
- }
- },
- }
- SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
- @pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")
- @pytest.mark.parametrize(
- "files",
- [
- {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB},
- EXAMPLE, # No setup.py script
- ],
- )
- def test_editable_with_pyproject(tmp_path, venv, files, editable_opts):
- project = tmp_path / "mypkg"
- project.mkdir()
- jaraco.path.build(files, prefix=project)
- cmd = [
- "python",
- "-m",
- "pip",
- "install",
- "--no-build-isolation", # required to force current version of setuptools
- "-e",
- str(project),
- *editable_opts,
- ]
- print(venv.run(cmd))
- cmd = ["python", "-m", "mypkg"]
- assert venv.run(cmd).strip() == "3.14159.post0 Hello World"
- (project / "src/mypkg/data.txt").write_text("foobar", encoding="utf-8")
- (project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8")
- assert venv.run(cmd).strip() == "3.14159.post0 foobar 42"
- def test_editable_with_flat_layout(tmp_path, venv, editable_opts):
- files = {
- "mypkg": {
- "pyproject.toml": dedent(
- """\
- [build-system]
- requires = ["setuptools", "wheel"]
- build-backend = "setuptools.build_meta"
- [project]
- name = "mypkg"
- version = "3.14159"
- [tool.setuptools]
- packages = ["pkg"]
- py-modules = ["mod"]
- """
- ),
- "pkg": {"__init__.py": "a = 4"},
- "mod.py": "b = 2",
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- project = tmp_path / "mypkg"
- cmd = [
- "python",
- "-m",
- "pip",
- "install",
- "--no-build-isolation", # required to force current version of setuptools
- "-e",
- str(project),
- *editable_opts,
- ]
- print(venv.run(cmd))
- cmd = ["python", "-c", "import pkg, mod; print(pkg.a, mod.b)"]
- assert venv.run(cmd).strip() == "4 2"
- def test_editable_with_single_module(tmp_path, venv, editable_opts):
- files = {
- "mypkg": {
- "pyproject.toml": dedent(
- """\
- [build-system]
- requires = ["setuptools", "wheel"]
- build-backend = "setuptools.build_meta"
- [project]
- name = "mod"
- version = "3.14159"
- [tool.setuptools]
- py-modules = ["mod"]
- """
- ),
- "mod.py": "b = 2",
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- project = tmp_path / "mypkg"
- cmd = [
- "python",
- "-m",
- "pip",
- "install",
- "--no-build-isolation", # required to force current version of setuptools
- "-e",
- str(project),
- *editable_opts,
- ]
- print(venv.run(cmd))
- cmd = ["python", "-c", "import mod; print(mod.b)"]
- assert venv.run(cmd).strip() == "2"
- class TestLegacyNamespaces:
- # legacy => pkg_resources.declare_namespace(...) + setup(namespace_packages=...)
- def test_nspkg_file_is_unique(self, tmp_path, monkeypatch):
- deprecation = pytest.warns(
- SetuptoolsDeprecationWarning, match=".*namespace_packages parameter.*"
- )
- installation_dir = tmp_path / ".installation_dir"
- installation_dir.mkdir()
- examples = (
- "myns.pkgA",
- "myns.pkgB",
- "myns.n.pkgA",
- "myns.n.pkgB",
- )
- for name in examples:
- pkg = namespaces.build_namespace_package(tmp_path, name, version="42")
- with deprecation, monkeypatch.context() as ctx:
- ctx.chdir(pkg)
- dist = run_setup("setup.py", stop_after="config")
- cmd = editable_wheel(dist)
- cmd.finalize_options()
- editable_name = cmd.get_finalized_command("dist_info").name
- cmd._install_namespaces(installation_dir, editable_name)
- files = list(installation_dir.glob("*-nspkg.pth"))
- assert len(files) == len(examples)
- @pytest.mark.parametrize(
- "impl",
- (
- "pkg_resources",
- # "pkgutil", => does not work
- ),
- )
- @pytest.mark.parametrize("ns", ("myns.n",))
- def test_namespace_package_importable(
- self, venv, tmp_path, ns, impl, editable_opts
- ):
- """
- Installing two packages sharing the same namespace, one installed
- naturally using pip or `--single-version-externally-managed`
- and the other installed in editable mode should leave the namespace
- intact and both packages reachable by import.
- (Ported from test_develop).
- """
- build_system = """\
- [build-system]
- requires = ["setuptools"]
- build-backend = "setuptools.build_meta"
- """
- pkg_A = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgA", impl=impl)
- pkg_B = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgB", impl=impl)
- (pkg_A / "pyproject.toml").write_text(build_system, encoding="utf-8")
- (pkg_B / "pyproject.toml").write_text(build_system, encoding="utf-8")
- # use pip to install to the target directory
- opts = editable_opts[:]
- opts.append("--no-build-isolation") # force current version of setuptools
- venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])
- venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])
- venv.run(["python", "-c", f"import {ns}.pkgA; import {ns}.pkgB"])
- # additionally ensure that pkg_resources import works
- venv.run(["python", "-c", "import pkg_resources"])
- class TestPep420Namespaces:
- def test_namespace_package_importable(self, venv, tmp_path, editable_opts):
- """
- Installing two packages sharing the same namespace, one installed
- normally using pip and the other installed in editable mode
- should allow importing both packages.
- """
- pkg_A = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgA')
- pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB')
- # use pip to install to the target directory
- opts = editable_opts[:]
- opts.append("--no-build-isolation") # force current version of setuptools
- venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])
- venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])
- venv.run(["python", "-c", "import myns.n.pkgA; import myns.n.pkgB"])
- def test_namespace_created_via_package_dir(self, venv, tmp_path, editable_opts):
- """Currently users can create a namespace by tweaking `package_dir`"""
- files = {
- "pkgA": {
- "pyproject.toml": dedent(
- """\
- [build-system]
- requires = ["setuptools", "wheel"]
- build-backend = "setuptools.build_meta"
- [project]
- name = "pkgA"
- version = "3.14159"
- [tool.setuptools]
- package-dir = {"myns.n.pkgA" = "src"}
- """
- ),
- "src": {"__init__.py": "a = 1"},
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- pkg_A = tmp_path / "pkgA"
- pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB')
- pkg_C = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgC')
- # use pip to install to the target directory
- opts = editable_opts[:]
- opts.append("--no-build-isolation") # force current version of setuptools
- venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])
- venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])
- venv.run(["python", "-m", "pip", "install", "-e", str(pkg_C), *opts])
- venv.run(["python", "-c", "from myns.n import pkgA, pkgB, pkgC"])
- def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path):
- """Sometimes users might specify an ``include`` pattern that ignores parent
- packages. In a normal installation this would ignore all modules inside the
- parent packages, and make them namespaces (reported in issue #3504),
- so the editable mode should preserve this behaviour.
- """
- files = {
- "pkgA": {
- "pyproject.toml": dedent(
- """\
- [build-system]
- requires = ["setuptools", "wheel"]
- build-backend = "setuptools.build_meta"
- [project]
- name = "pkgA"
- version = "3.14159"
- [tool.setuptools]
- packages.find.include = ["mypkg.*"]
- """
- ),
- "mypkg": {
- "__init__.py": "",
- "other.py": "b = 1",
- "n": {
- "__init__.py": "",
- "pkgA.py": "a = 1",
- },
- },
- "MANIFEST.in": EXAMPLE["MANIFEST.in"],
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- pkg_A = tmp_path / "pkgA"
- # use pip to install to the target directory
- opts = ["--no-build-isolation"] # force current version of setuptools
- venv.run(["python", "-m", "pip", "-v", "install", "-e", str(pkg_A), *opts])
- out = venv.run(["python", "-c", "from mypkg.n import pkgA; print(pkgA.a)"])
- assert out.strip() == "1"
- cmd = """\
- try:
- import mypkg.other
- except ImportError:
- print("mypkg.other not defined")
- """
- out = venv.run(["python", "-c", dedent(cmd)])
- assert "mypkg.other not defined" in out
- def test_editable_with_prefix(tmp_path, sample_project, editable_opts):
- """
- Editable install to a prefix should be discoverable.
- """
- prefix = tmp_path / 'prefix'
- # figure out where pip will likely install the package
- site_packages_all = [
- prefix / Path(path).relative_to(sys.prefix)
- for path in sys.path
- if 'site-packages' in path and path.startswith(sys.prefix)
- ]
- for sp in site_packages_all:
- sp.mkdir(parents=True)
- # install workaround
- _addsitedirs(site_packages_all)
- env = dict(os.environ, PYTHONPATH=os.pathsep.join(map(str, site_packages_all)))
- cmd = [
- sys.executable,
- '-m',
- 'pip',
- 'install',
- '--editable',
- str(sample_project),
- '--prefix',
- str(prefix),
- '--no-build-isolation',
- *editable_opts,
- ]
- subprocess.check_call(cmd, env=env)
- # now run 'sample' with the prefix on the PYTHONPATH
- bin = 'Scripts' if platform.system() == 'Windows' else 'bin'
- exe = prefix / bin / 'sample'
- subprocess.check_call([exe], env=env)
- class TestFinderTemplate:
- """This test focus in getting a particular implementation detail right.
- If at some point in time the implementation is changed for something different,
- this test can be modified or even excluded.
- """
- def install_finder(self, finder):
- loc = {}
- exec(finder, loc, loc)
- loc["install"]()
- def test_packages(self, tmp_path):
- files = {
- "src1": {
- "pkg1": {
- "__init__.py": "",
- "subpkg": {"mod1.py": "a = 42"},
- },
- },
- "src2": {"mod2.py": "a = 43"},
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {
- "pkg1": str(tmp_path / "src1/pkg1"),
- "mod2": str(tmp_path / "src2/mod2"),
- }
- template = _finder_template(str(uuid4()), mapping, {})
- with contexts.save_paths(), contexts.save_sys_modules():
- for mod in ("pkg1", "pkg1.subpkg", "pkg1.subpkg.mod1", "mod2"):
- sys.modules.pop(mod, None)
- self.install_finder(template)
- mod1 = import_module("pkg1.subpkg.mod1")
- mod2 = import_module("mod2")
- subpkg = import_module("pkg1.subpkg")
- assert mod1.a == 42
- assert mod2.a == 43
- expected = str((tmp_path / "src1/pkg1/subpkg").resolve())
- assert_path(subpkg, expected)
- def test_namespace(self, tmp_path):
- files = {"pkg": {"__init__.py": "a = 13", "text.txt": "abc"}}
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {"ns.othername": str(tmp_path / "pkg")}
- namespaces = {"ns": []}
- template = _finder_template(str(uuid4()), mapping, namespaces)
- with contexts.save_paths(), contexts.save_sys_modules():
- for mod in ("ns", "ns.othername"):
- sys.modules.pop(mod, None)
- self.install_finder(template)
- pkg = import_module("ns.othername")
- text = importlib_resources.files(pkg) / "text.txt"
- expected = str((tmp_path / "pkg").resolve())
- assert_path(pkg, expected)
- assert pkg.a == 13
- # Make sure resources can also be found
- assert text.read_text(encoding="utf-8") == "abc"
- def test_combine_namespaces(self, tmp_path):
- files = {
- "src1": {"ns": {"pkg1": {"__init__.py": "a = 13"}}},
- "src2": {"ns": {"mod2.py": "b = 37"}},
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {
- "ns.pkgA": str(tmp_path / "src1/ns/pkg1"),
- "ns": str(tmp_path / "src2/ns"),
- }
- namespaces_ = {"ns": [str(tmp_path / "src1"), str(tmp_path / "src2")]}
- template = _finder_template(str(uuid4()), mapping, namespaces_)
- with contexts.save_paths(), contexts.save_sys_modules():
- for mod in ("ns", "ns.pkgA", "ns.mod2"):
- sys.modules.pop(mod, None)
- self.install_finder(template)
- pkgA = import_module("ns.pkgA")
- mod2 = import_module("ns.mod2")
- expected = str((tmp_path / "src1/ns/pkg1").resolve())
- assert_path(pkgA, expected)
- assert pkgA.a == 13
- assert mod2.b == 37
- def test_combine_namespaces_nested(self, tmp_path):
- """
- Users may attempt to combine namespace packages in a nested way via
- ``package_dir`` as shown in pypa/setuptools#4248.
- """
- files = {
- "src": {"my_package": {"my_module.py": "a = 13"}},
- "src2": {"my_package2": {"my_module2.py": "b = 37"}},
- }
- stack = jaraco.path.DirectoryStack()
- with stack.context(tmp_path):
- jaraco.path.build(files)
- attrs = {
- "script_name": "%PEP 517%",
- "package_dir": {
- "different_name": "src/my_package",
- "different_name.subpkg": "src2/my_package2",
- },
- "packages": ["different_name", "different_name.subpkg"],
- }
- dist = Distribution(attrs)
- finder = _TopLevelFinder(dist, str(uuid4()))
- code = next(v for k, v in finder.get_implementation() if k.endswith(".py"))
- with contexts.save_paths(), contexts.save_sys_modules():
- for mod in attrs["packages"]:
- sys.modules.pop(mod, None)
- self.install_finder(code)
- mod1 = import_module("different_name.my_module")
- mod2 = import_module("different_name.subpkg.my_module2")
- expected = str((tmp_path / "src/my_package/my_module.py").resolve())
- assert str(Path(mod1.__file__).resolve()) == expected
- expected = str((tmp_path / "src2/my_package2/my_module2.py").resolve())
- assert str(Path(mod2.__file__).resolve()) == expected
- assert mod1.a == 13
- assert mod2.b == 37
- def test_dynamic_path_computation(self, tmp_path):
- # Follows the example in PEP 420
- files = {
- "project1": {"parent": {"child": {"one.py": "x = 1"}}},
- "project2": {"parent": {"child": {"two.py": "x = 2"}}},
- "project3": {"parent": {"child": {"three.py": "x = 3"}}},
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {}
- namespaces_ = {"parent": [str(tmp_path / "project1/parent")]}
- template = _finder_template(str(uuid4()), mapping, namespaces_)
- mods = (f"parent.child.{name}" for name in ("one", "two", "three"))
- with contexts.save_paths(), contexts.save_sys_modules():
- for mod in ("parent", "parent.child", "parent.child", *mods):
- sys.modules.pop(mod, None)
- self.install_finder(template)
- one = import_module("parent.child.one")
- assert one.x == 1
- with pytest.raises(ImportError):
- import_module("parent.child.two")
- sys.path.append(str(tmp_path / "project2"))
- two = import_module("parent.child.two")
- assert two.x == 2
- with pytest.raises(ImportError):
- import_module("parent.child.three")
- sys.path.append(str(tmp_path / "project3"))
- three = import_module("parent.child.three")
- assert three.x == 3
- def test_no_recursion(self, tmp_path):
- # See issue #3550
- files = {
- "pkg": {
- "__init__.py": "from . import pkg",
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {
- "pkg": str(tmp_path / "pkg"),
- }
- template = _finder_template(str(uuid4()), mapping, {})
- with contexts.save_paths(), contexts.save_sys_modules():
- sys.modules.pop("pkg", None)
- self.install_finder(template)
- with pytest.raises(ImportError, match="pkg"):
- import_module("pkg")
- def test_similar_name(self, tmp_path):
- files = {
- "foo": {
- "__init__.py": "",
- "bar": {
- "__init__.py": "",
- },
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {
- "foo": str(tmp_path / "foo"),
- }
- template = _finder_template(str(uuid4()), mapping, {})
- with contexts.save_paths(), contexts.save_sys_modules():
- sys.modules.pop("foo", None)
- sys.modules.pop("foo.bar", None)
- self.install_finder(template)
- with pytest.raises(ImportError, match="foobar"):
- import_module("foobar")
- def test_case_sensitivity(self, tmp_path):
- files = {
- "foo": {
- "__init__.py": "",
- "lowercase.py": "x = 1",
- "bar": {
- "__init__.py": "",
- "lowercase.py": "x = 2",
- },
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {
- "foo": str(tmp_path / "foo"),
- }
- template = _finder_template(str(uuid4()), mapping, {})
- with contexts.save_paths(), contexts.save_sys_modules():
- sys.modules.pop("foo", None)
- self.install_finder(template)
- with pytest.raises(ImportError, match="'FOO'"):
- import_module("FOO")
- with pytest.raises(ImportError, match="'foo\\.LOWERCASE'"):
- import_module("foo.LOWERCASE")
- with pytest.raises(ImportError, match="'foo\\.bar\\.Lowercase'"):
- import_module("foo.bar.Lowercase")
- with pytest.raises(ImportError, match="'foo\\.BAR'"):
- import_module("foo.BAR.lowercase")
- with pytest.raises(ImportError, match="'FOO'"):
- import_module("FOO.bar.lowercase")
- mod = import_module("foo.lowercase")
- assert mod.x == 1
- mod = import_module("foo.bar.lowercase")
- assert mod.x == 2
- def test_namespace_case_sensitivity(self, tmp_path):
- files = {
- "pkg": {
- "__init__.py": "a = 13",
- "foo": {
- "__init__.py": "b = 37",
- "bar.py": "c = 42",
- },
- },
- }
- jaraco.path.build(files, prefix=tmp_path)
- mapping = {"ns.othername": str(tmp_path / "pkg")}
- namespaces = {"ns": []}
- template = _finder_template(str(uuid4()), mapping, namespaces)
- with contexts.save_paths(), contexts.save_sys_modules():
- for mod in ("ns", "ns.othername"):
- sys.modules.pop(mod, None)
- self.install_finder(template)
- pkg = import_module("ns.othername")
- expected = str((tmp_path / "pkg").resolve())
- assert_path(pkg, expected)
- assert pkg.a == 13
- foo = import_module("ns.othername.foo")
- assert foo.b == 37
- bar = import_module("ns.othername.foo.bar")
- assert bar.c == 42
- with pytest.raises(ImportError, match="'NS'"):
- import_module("NS.othername.foo")
- with pytest.raises(ImportError, match="'ns\\.othername\\.FOO\\'"):
- import_module("ns.othername.FOO")
- 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.
- If at some point in time the implementation is changed for something different,
- this test can be modified or even excluded.
- """
- files = {
- "a": {"b": {"__init__.py": "ab = 1"}, "__init__.py": "a = 1"},
- "d": {"__init__.py": "d = 1", "e": {"__init__.py": "de = 1"}},
- "f": {"g": {"h": {"__init__.py": "fgh = 1"}}},
- "other": {"__init__.py": "abc = 1"},
- "another": {"__init__.py": "abcxyz = 1"},
- "yet_another": {"__init__.py": "mnopq = 1"},
- }
- jaraco.path.build(files, prefix=tmp_path)
- package_dir = {
- "a.b.c": "other",
- "a.b.c.x.y.z": "another",
- "m.n.o.p.q": "yet_another",
- }
- packages = [
- "a",
- "a.b",
- "a.b.c",
- "a.b.c.x.y",
- "a.b.c.x.y.z",
- "d",
- "d.e",
- "f",
- "f.g",
- "f.g.h",
- "m.n.o.p.q",
- ]
- roots = _find_package_roots(packages, package_dir, tmp_path)
- assert roots == {
- "a": str(tmp_path / "a"),
- "a.b.c": str(tmp_path / "other"),
- "a.b.c.x.y.z": str(tmp_path / "another"),
- "d": str(tmp_path / "d"),
- "f": str(tmp_path / "f"),
- "m.n.o.p.q": str(tmp_path / "yet_another"),
- }
- ns = set(dict(_find_namespaces(packages, roots)))
- assert ns == {"f", "f.g"}
- ns = set(_find_virtual_namespaces(roots))
- assert ns == {"a.b", "a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"}
- class TestOverallBehaviour:
- PYPROJECT = """\
- [build-system]
- requires = ["setuptools"]
- build-backend = "setuptools.build_meta"
- [project]
- name = "mypkg"
- version = "3.14159"
- """
- # Any: Would need a TypedDict. Keep it simple for tests
- FLAT_LAYOUT: dict[str, Any] = {
- "pyproject.toml": dedent(PYPROJECT),
- "MANIFEST.in": EXAMPLE["MANIFEST.in"],
- "otherfile.py": "",
- "mypkg": {
- "__init__.py": "",
- "mod1.py": "var = 42",
- "subpackage": {
- "__init__.py": "",
- "mod2.py": "var = 13",
- "resource_file.txt": "resource 39",
- },
- },
- }
- EXAMPLES = {
- "flat-layout": FLAT_LAYOUT,
- "src-layout": {
- "pyproject.toml": dedent(PYPROJECT),
- "MANIFEST.in": EXAMPLE["MANIFEST.in"],
- "otherfile.py": "",
- "src": {"mypkg": FLAT_LAYOUT["mypkg"]},
- },
- "custom-layout": {
- "pyproject.toml": dedent(PYPROJECT)
- + dedent(
- """\
- [tool.setuptools]
- packages = ["mypkg", "mypkg.subpackage"]
- [tool.setuptools.package-dir]
- "mypkg.subpackage" = "other"
- """
- ),
- "MANIFEST.in": EXAMPLE["MANIFEST.in"],
- "otherfile.py": "",
- "mypkg": {
- "__init__.py": "",
- "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"],
- },
- "other": FLAT_LAYOUT["mypkg"]["subpackage"],
- },
- "namespace": {
- "pyproject.toml": dedent(PYPROJECT),
- "MANIFEST.in": EXAMPLE["MANIFEST.in"],
- "otherfile.py": "",
- "src": {
- "mypkg": {
- "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"],
- "subpackage": FLAT_LAYOUT["mypkg"]["subpackage"],
- },
- },
- },
- }
- @pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")
- @pytest.mark.parametrize("layout", EXAMPLES.keys())
- def test_editable_install(self, tmp_path, venv, layout, editable_opts):
- project, _ = install_project(
- "mypkg", venv, tmp_path, self.EXAMPLES[layout], *editable_opts
- )
- # Ensure stray files are not importable
- cmd_import_error = """\
- try:
- import otherfile
- except ImportError as ex:
- print(ex)
- """
- out = venv.run(["python", "-c", dedent(cmd_import_error)])
- assert "No module named 'otherfile'" in out
- # Ensure the modules are importable
- cmd_get_vars = """\
- import mypkg, mypkg.mod1, mypkg.subpackage.mod2
- print(mypkg.mod1.var, mypkg.subpackage.mod2.var)
- """
- out = venv.run(["python", "-c", dedent(cmd_get_vars)])
- assert "42 13" in out
- # Ensure resources are reachable
- cmd_get_resource = """\
- import mypkg.subpackage
- from setuptools._importlib import resources as importlib_resources
- text = importlib_resources.files(mypkg.subpackage) / "resource_file.txt"
- print(text.read_text(encoding="utf-8"))
- """
- out = venv.run(["python", "-c", dedent(cmd_get_resource)])
- assert "resource 39" in out
- # Ensure files are editable
- mod1 = next(project.glob("**/mod1.py"))
- mod2 = next(project.glob("**/mod2.py"))
- resource_file = next(project.glob("**/resource_file.txt"))
- mod1.write_text("var = 17", encoding="utf-8")
- mod2.write_text("var = 781", encoding="utf-8")
- resource_file.write_text("resource 374", encoding="utf-8")
- out = venv.run(["python", "-c", dedent(cmd_get_vars)])
- assert "42 13" not in out
- assert "17 781" in out
- out = venv.run(["python", "-c", dedent(cmd_get_resource)])
- assert "resource 39" not in out
- assert "resource 374" in out
- class TestLinkTree:
- FILES = deepcopy(TestOverallBehaviour.EXAMPLES["src-layout"])
- FILES["pyproject.toml"] += dedent(
- """\
- [tool.setuptools]
- # Temporary workaround: both `include-package-data` and `package-data` configs
- # can be removed after #3260 is fixed.
- include-package-data = false
- package-data = {"*" = ["*.txt"]}
- [tool.setuptools.packages.find]
- where = ["src"]
- exclude = ["*.subpackage*"]
- """
- )
- FILES["src"]["mypkg"]["resource.not_in_manifest"] = "abc"
- def test_generated_tree(self, tmp_path):
- jaraco.path.build(self.FILES, prefix=tmp_path)
- with _Path(tmp_path):
- name = "mypkg-3.14159"
- dist = Distribution({"script_name": "%PEP 517%"})
- dist.parse_config_files()
- wheel = Mock()
- aux = tmp_path / ".aux"
- build = tmp_path / ".build"
- aux.mkdir()
- build.mkdir()
- build_py = dist.get_command_obj("build_py")
- build_py.editable_mode = True
- build_py.build_lib = str(build)
- build_py.ensure_finalized()
- outputs = build_py.get_outputs()
- output_mapping = build_py.get_output_mapping()
- make_tree = _LinkTree(dist, name, aux, build)
- make_tree(wheel, outputs, output_mapping)
- mod1 = next(aux.glob("**/mod1.py"))
- expected = tmp_path / "src/mypkg/mod1.py"
- assert_link_to(mod1, expected)
- assert next(aux.glob("**/subpackage"), None) is None
- assert next(aux.glob("**/mod2.py"), None) is None
- assert next(aux.glob("**/resource_file.txt"), None) is None
- assert next(aux.glob("**/resource.not_in_manifest"), None) is None
- def test_strict_install(self, tmp_path, venv):
- opts = ["--config-settings", "editable-mode=strict"]
- install_project("mypkg", venv, tmp_path, self.FILES, *opts)
- out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
- assert "42" in out
- # Ensure packages excluded from distribution are not importable
- cmd_import_error = """\
- try:
- from mypkg import subpackage
- except ImportError as ex:
- print(ex)
- """
- out = venv.run(["python", "-c", dedent(cmd_import_error)])
- assert "cannot import name 'subpackage'" in out
- # Ensure resource files excluded from distribution are not reachable
- cmd_get_resource = """\
- import mypkg
- from setuptools._importlib import resources as importlib_resources
- try:
- text = importlib_resources.files(mypkg) / "resource.not_in_manifest"
- print(text.read_text(encoding="utf-8"))
- except FileNotFoundError as ex:
- print(ex)
- """
- out = venv.run(["python", "-c", dedent(cmd_get_resource)])
- assert "No such file or directory" in out
- assert "resource.not_in_manifest" in out
- @pytest.mark.filterwarnings("ignore:.*compat.*:setuptools.SetuptoolsDeprecationWarning")
- def test_compat_install(tmp_path, venv):
- # TODO: Remove `compat` after Dec/2022.
- opts = ["--config-settings", "editable-mode=compat"]
- files = TestOverallBehaviour.EXAMPLES["custom-layout"]
- install_project("mypkg", venv, tmp_path, files, *opts)
- out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
- assert "42" in out
- expected_path = comparable_path(str(tmp_path))
- # Compatible behaviour will make spurious modules and excluded
- # files importable directly from the original path
- for cmd in (
- "import otherfile; print(otherfile)",
- "import other; print(other)",
- "import mypkg; print(mypkg)",
- ):
- out = comparable_path(venv.run(["python", "-c", cmd]))
- assert expected_path in out
- # Compatible behaviour will not consider custom mappings
- cmd = """\
- try:
- from mypkg import subpackage;
- except ImportError as ex:
- print(ex)
- """
- out = venv.run(["python", "-c", dedent(cmd)])
- assert "cannot import name 'subpackage'" in out
- @pytest.mark.uses_network
- def test_pbr_integration(pbr_package, venv, editable_opts):
- """Ensure editable installs work with pbr, issue #3500"""
- cmd = [
- 'python',
- '-m',
- 'pip',
- '-v',
- 'install',
- '--editable',
- pbr_package,
- *editable_opts,
- ]
- venv.run(cmd, stderr=subprocess.STDOUT)
- out = venv.run(["python", "-c", "import mypkg.hello"])
- assert "Hello world!" in out
- class TestCustomBuildPy:
- """
- Issue #3501 indicates that some plugins/customizations might rely on:
- 1. ``build_py`` not running
- 2. ``build_py`` always copying files to ``build_lib``
- During the transition period setuptools should prevent potential errors from
- happening due to those assumptions.
- """
- # TODO: Remove tests after _run_build_steps is removed.
- FILES = {
- **TestOverallBehaviour.EXAMPLES["flat-layout"],
- "setup.py": dedent(
- """\
- import pathlib
- from setuptools import setup
- from setuptools.command.build_py import build_py as orig
- class my_build_py(orig):
- def run(self):
- super().run()
- raise ValueError("TEST_RAISE")
- setup(cmdclass={"build_py": my_build_py})
- """
- ),
- }
- def test_safeguarded_from_errors(self, tmp_path, venv):
- """Ensure that errors in custom build_py are reported as warnings"""
- # Warnings should show up
- _, out = install_project("mypkg", venv, tmp_path, self.FILES)
- assert "SetuptoolsDeprecationWarning" in out
- assert "ValueError: TEST_RAISE" in out
- # but installation should be successful
- out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
- assert "42" in out
- class TestCustomBuildWheel:
- def install_custom_build_wheel(self, dist):
- bdist_wheel_cls = dist.get_command_class("bdist_wheel")
- class MyBdistWheel(bdist_wheel_cls):
- def get_tag(self):
- # In issue #3513, we can see that some extensions may try to access
- # the `plat_name` property in bdist_wheel
- if self.plat_name.startswith("macosx-"):
- _ = "macOS platform"
- return super().get_tag()
- dist.cmdclass["bdist_wheel"] = MyBdistWheel
- def test_access_plat_name(self, tmpdir_cwd):
- # Even when a custom bdist_wheel tries to access plat_name the build should
- # be successful
- jaraco.path.build({"module.py": "x = 42"})
- dist = Distribution()
- dist.script_name = "setup.py"
- dist.set_defaults()
- self.install_custom_build_wheel(dist)
- cmd = editable_wheel(dist)
- cmd.ensure_finalized()
- cmd.run()
- wheel_file = str(next(Path().glob('dist/*.whl')))
- assert "editable" in wheel_file
- class TestCustomBuildExt:
- def install_custom_build_ext_distutils(self, dist):
- from distutils.command.build_ext import build_ext as build_ext_cls
- class MyBuildExt(build_ext_cls):
- pass
- dist.cmdclass["build_ext"] = MyBuildExt
- @pytest.mark.skipif(
- sys.platform != "linux", reason="compilers may fail without correct setup"
- )
- def test_distutils_leave_inplace_files(self, tmpdir_cwd):
- jaraco.path.build({"module.c": ""})
- attrs = {
- "ext_modules": [Extension("module", ["module.c"])],
- }
- dist = Distribution(attrs)
- dist.script_name = "setup.py"
- dist.set_defaults()
- self.install_custom_build_ext_distutils(dist)
- cmd = editable_wheel(dist)
- cmd.ensure_finalized()
- cmd.run()
- wheel_file = str(next(Path().glob('dist/*.whl')))
- assert "editable" in wheel_file
- files = [p for p in Path().glob("module.*") if p.suffix != ".c"]
- assert len(files) == 1
- name = files[0].name
- assert any(name.endswith(ext) for ext in EXTENSION_SUFFIXES)
- def test_debugging_tips(tmpdir_cwd, monkeypatch):
- """Make sure to display useful debugging tips to the user."""
- jaraco.path.build({"module.py": "x = 42"})
- dist = Distribution()
- dist.script_name = "setup.py"
- dist.set_defaults()
- cmd = editable_wheel(dist)
- cmd.ensure_finalized()
- SimulatedErr = type("SimulatedErr", (Exception,), {})
- simulated_failure = Mock(side_effect=SimulatedErr())
- monkeypatch.setattr(cmd, "get_finalized_command", simulated_failure)
- with pytest.raises(SimulatedErr) as ctx:
- cmd.run()
- assert any('debugging-tips' in note for note in ctx.value.__notes__)
- @pytest.mark.filterwarnings("error")
- def test_encode_pth():
- """Ensure _encode_pth function does not produce encoding warnings"""
- content = _encode_pth("tkmilan_ç_utf8") # no warnings (would be turned into errors)
- assert isinstance(content, bytes)
- def install_project(name, venv, tmp_path, files, *opts):
- project = tmp_path / name
- project.mkdir()
- jaraco.path.build(files, prefix=project)
- opts = [*opts, "--no-build-isolation"] # force current version of setuptools
- out = venv.run(
- ["python", "-m", "pip", "-v", "install", "-e", str(project), *opts],
- stderr=subprocess.STDOUT,
- )
- return project, out
- def _addsitedirs(new_dirs):
- """To use this function, it is necessary to insert new_dir in front of sys.path.
- The Python process will try to import a ``sitecustomize`` module on startup.
- If we manipulate sys.path/PYTHONPATH, we can force it to run our code,
- which invokes ``addsitedir`` and ensure ``.pth`` files are loaded.
- """
- content = '\n'.join(
- ("import site",)
- + tuple(f"site.addsitedir({os.fspath(new_dir)!r})" for new_dir in new_dirs)
- )
- (new_dirs[0] / "sitecustomize.py").write_text(content, encoding="utf-8")
- # ---- Assertion Helpers ----
- def assert_path(pkg, expected):
- # __path__ is not guaranteed to exist, so we have to account for that
- if pkg.__path__:
- path = next(iter(pkg.__path__), None)
- if path:
- assert str(Path(path).resolve()) == expected
- def assert_link_to(file: Path, other: Path) -> None:
- if file.is_symlink():
- assert str(file.resolve()) == str(other.resolve())
- else:
- file_stat = file.stat()
- other_stat = other.stat()
- assert file_stat[stat.ST_INO] == other_stat[stat.ST_INO]
- assert file_stat[stat.ST_DEV] == other_stat[stat.ST_DEV]
- def comparable_path(str_with_path: str) -> str:
- return str_with_path.lower().replace(os.sep, "/").replace("//", "/")
|