codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import os4import platform5import stat6import subprocess7import sys8from copy import deepcopy9from importlib import import_module10from importlib.machinery import EXTENSION_SUFFIXES11from pathlib import Path12from textwrap import dedent13from typing import Any14from unittest.mock import Mock15from uuid import uuid416 17import jaraco.envs18import jaraco.path19import pytest20from path import Path as _Path21 22from setuptools._importlib import resources as importlib_resources23from setuptools.command.editable_wheel import (24 _encode_pth,25 _find_namespaces,26 _find_package_roots,27 _find_virtual_namespaces,28 _finder_template,29 _LinkTree,30 _TopLevelFinder,31 editable_wheel,32)33from setuptools.dist import Distribution34from setuptools.extension import Extension35from setuptools.warnings import SetuptoolsDeprecationWarning36 37from . import contexts, namespaces38 39from distutils.core import run_setup40 41 42@pytest.fixture(params=["strict", "lenient"])43def editable_opts(request):44 if request.param == "strict":45 return ["--config-settings", "editable-mode=strict"]46 return []47 48 49EXAMPLE = {50 'pyproject.toml': dedent(51 """\52 [build-system]53 requires = ["setuptools"]54 build-backend = "setuptools.build_meta"55 56 [project]57 name = "mypkg"58 version = "3.14159"59 license = {text = "MIT"}60 description = "This is a Python package"61 dynamic = ["readme"]62 classifiers = [63 "Development Status :: 5 - Production/Stable",64 "Intended Audience :: Developers"65 ]66 urls = {Homepage = "https://github.com"}67 68 [tool.setuptools]69 package-dir = {"" = "src"}70 packages = {find = {where = ["src"]}}71 license-files = ["LICENSE*"]72 73 [tool.setuptools.dynamic]74 readme = {file = "README.rst"}75 76 [tool.distutils.egg_info]77 tag-build = ".post0"78 """79 ),80 "MANIFEST.in": dedent(81 """\82 global-include *.py *.txt83 global-exclude *.py[cod]84 prune dist85 prune build86 """87 ).strip(),88 "README.rst": "This is a ``README``",89 "LICENSE.txt": "---- placeholder MIT license ----",90 "src": {91 "mypkg": {92 "__init__.py": dedent(93 """\94 import sys95 from importlib.metadata import PackageNotFoundError, version96 97 try:98 __version__ = version(__name__)99 except PackageNotFoundError:100 __version__ = "unknown"101 """102 ),103 "__main__.py": dedent(104 """\105 from importlib.resources import read_text106 from . import __version__, __name__ as parent107 from .mod import x108 109 data = read_text(parent, "data.txt")110 print(__version__, data, x)111 """112 ),113 "mod.py": "x = ''",114 "data.txt": "Hello World",115 }116 },117}118 119 120SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"121 122 123@pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")124@pytest.mark.parametrize(125 "files",126 [127 {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB},128 EXAMPLE, # No setup.py script129 ],130)131def test_editable_with_pyproject(tmp_path, venv, files, editable_opts):132 project = tmp_path / "mypkg"133 project.mkdir()134 jaraco.path.build(files, prefix=project)135 136 cmd = [137 "python",138 "-m",139 "pip",140 "install",141 "--no-build-isolation", # required to force current version of setuptools142 "-e",143 str(project),144 *editable_opts,145 ]146 print(venv.run(cmd))147 148 cmd = ["python", "-m", "mypkg"]149 assert venv.run(cmd).strip() == "3.14159.post0 Hello World"150 151 (project / "src/mypkg/data.txt").write_text("foobar", encoding="utf-8")152 (project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8")153 assert venv.run(cmd).strip() == "3.14159.post0 foobar 42"154 155 156def test_editable_with_flat_layout(tmp_path, venv, editable_opts):157 files = {158 "mypkg": {159 "pyproject.toml": dedent(160 """\161 [build-system]162 requires = ["setuptools", "wheel"]163 build-backend = "setuptools.build_meta"164 165 [project]166 name = "mypkg"167 version = "3.14159"168 169 [tool.setuptools]170 packages = ["pkg"]171 py-modules = ["mod"]172 """173 ),174 "pkg": {"__init__.py": "a = 4"},175 "mod.py": "b = 2",176 },177 }178 jaraco.path.build(files, prefix=tmp_path)179 project = tmp_path / "mypkg"180 181 cmd = [182 "python",183 "-m",184 "pip",185 "install",186 "--no-build-isolation", # required to force current version of setuptools187 "-e",188 str(project),189 *editable_opts,190 ]191 print(venv.run(cmd))192 cmd = ["python", "-c", "import pkg, mod; print(pkg.a, mod.b)"]193 assert venv.run(cmd).strip() == "4 2"194 195 196def test_editable_with_single_module(tmp_path, venv, editable_opts):197 files = {198 "mypkg": {199 "pyproject.toml": dedent(200 """\201 [build-system]202 requires = ["setuptools", "wheel"]203 build-backend = "setuptools.build_meta"204 205 [project]206 name = "mod"207 version = "3.14159"208 209 [tool.setuptools]210 py-modules = ["mod"]211 """212 ),213 "mod.py": "b = 2",214 },215 }216 jaraco.path.build(files, prefix=tmp_path)217 project = tmp_path / "mypkg"218 219 cmd = [220 "python",221 "-m",222 "pip",223 "install",224 "--no-build-isolation", # required to force current version of setuptools225 "-e",226 str(project),227 *editable_opts,228 ]229 print(venv.run(cmd))230 cmd = ["python", "-c", "import mod; print(mod.b)"]231 assert venv.run(cmd).strip() == "2"232 233 234class TestLegacyNamespaces:235 # legacy => pkg_resources.declare_namespace(...) + setup(namespace_packages=...)236 237 def test_nspkg_file_is_unique(self, tmp_path, monkeypatch):238 deprecation = pytest.warns(239 SetuptoolsDeprecationWarning, match=".*namespace_packages parameter.*"240 )241 installation_dir = tmp_path / ".installation_dir"242 installation_dir.mkdir()243 examples = (244 "myns.pkgA",245 "myns.pkgB",246 "myns.n.pkgA",247 "myns.n.pkgB",248 )249 250 for name in examples:251 pkg = namespaces.build_namespace_package(tmp_path, name, version="42")252 with deprecation, monkeypatch.context() as ctx:253 ctx.chdir(pkg)254 dist = run_setup("setup.py", stop_after="config")255 cmd = editable_wheel(dist)256 cmd.finalize_options()257 editable_name = cmd.get_finalized_command("dist_info").name258 cmd._install_namespaces(installation_dir, editable_name)259 260 files = list(installation_dir.glob("*-nspkg.pth"))261 assert len(files) == len(examples)262 263 @pytest.mark.parametrize(264 "impl",265 (266 "pkg_resources",267 # "pkgutil", => does not work268 ),269 )270 @pytest.mark.parametrize("ns", ("myns.n",))271 def test_namespace_package_importable(272 self, venv, tmp_path, ns, impl, editable_opts273 ):274 """275 Installing two packages sharing the same namespace, one installed276 naturally using pip or `--single-version-externally-managed`277 and the other installed in editable mode should leave the namespace278 intact and both packages reachable by import.279 (Ported from test_develop).280 """281 build_system = """\282 [build-system]283 requires = ["setuptools"]284 build-backend = "setuptools.build_meta"285 """286 pkg_A = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgA", impl=impl)287 pkg_B = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgB", impl=impl)288 (pkg_A / "pyproject.toml").write_text(build_system, encoding="utf-8")289 (pkg_B / "pyproject.toml").write_text(build_system, encoding="utf-8")290 # use pip to install to the target directory291 opts = editable_opts[:]292 opts.append("--no-build-isolation") # force current version of setuptools293 venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])294 venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])295 venv.run(["python", "-c", f"import {ns}.pkgA; import {ns}.pkgB"])296 297 298class TestPep420Namespaces:299 def test_namespace_package_importable(self, venv, tmp_path, editable_opts):300 """301 Installing two packages sharing the same namespace, one installed302 normally using pip and the other installed in editable mode303 should allow importing both packages.304 """305 pkg_A = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgA')306 pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB')307 # use pip to install to the target directory308 opts = editable_opts[:]309 opts.append("--no-build-isolation") # force current version of setuptools310 venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])311 venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])312 venv.run(["python", "-c", "import myns.n.pkgA; import myns.n.pkgB"])313 314 def test_namespace_created_via_package_dir(self, venv, tmp_path, editable_opts):315 """Currently users can create a namespace by tweaking `package_dir`"""316 files = {317 "pkgA": {318 "pyproject.toml": dedent(319 """\320 [build-system]321 requires = ["setuptools", "wheel"]322 build-backend = "setuptools.build_meta"323 324 [project]325 name = "pkgA"326 version = "3.14159"327 328 [tool.setuptools]329 package-dir = {"myns.n.pkgA" = "src"}330 """331 ),332 "src": {"__init__.py": "a = 1"},333 },334 }335 jaraco.path.build(files, prefix=tmp_path)336 pkg_A = tmp_path / "pkgA"337 pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB')338 pkg_C = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgC')339 340 # use pip to install to the target directory341 opts = editable_opts[:]342 opts.append("--no-build-isolation") # force current version of setuptools343 venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])344 venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])345 venv.run(["python", "-m", "pip", "install", "-e", str(pkg_C), *opts])346 venv.run(["python", "-c", "from myns.n import pkgA, pkgB, pkgC"])347 348 def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path):349 """Sometimes users might specify an ``include`` pattern that ignores parent350 packages. In a normal installation this would ignore all modules inside the351 parent packages, and make them namespaces (reported in issue #3504),352 so the editable mode should preserve this behaviour.353 """354 files = {355 "pkgA": {356 "pyproject.toml": dedent(357 """\358 [build-system]359 requires = ["setuptools", "wheel"]360 build-backend = "setuptools.build_meta"361 362 [project]363 name = "pkgA"364 version = "3.14159"365 366 [tool.setuptools]367 packages.find.include = ["mypkg.*"]368 """369 ),370 "mypkg": {371 "__init__.py": "",372 "other.py": "b = 1",373 "n": {374 "__init__.py": "",375 "pkgA.py": "a = 1",376 },377 },378 "MANIFEST.in": EXAMPLE["MANIFEST.in"],379 },380 }381 jaraco.path.build(files, prefix=tmp_path)382 pkg_A = tmp_path / "pkgA"383 384 # use pip to install to the target directory385 opts = ["--no-build-isolation"] # force current version of setuptools386 venv.run(["python", "-m", "pip", "-v", "install", "-e", str(pkg_A), *opts])387 out = venv.run(["python", "-c", "from mypkg.n import pkgA; print(pkgA.a)"])388 assert out.strip() == "1"389 cmd = """\390 try:391 import mypkg.other392 except ImportError:393 print("mypkg.other not defined")394 """395 out = venv.run(["python", "-c", dedent(cmd)])396 assert "mypkg.other not defined" in out397 398 399def test_editable_with_prefix(tmp_path, sample_project, editable_opts):400 """401 Editable install to a prefix should be discoverable.402 """403 prefix = tmp_path / 'prefix'404 405 # figure out where pip will likely install the package406 site_packages_all = [407 prefix / Path(path).relative_to(sys.prefix)408 for path in sys.path409 if 'site-packages' in path and path.startswith(sys.prefix)410 ]411 412 for sp in site_packages_all:413 sp.mkdir(parents=True)414 415 # install workaround416 _addsitedirs(site_packages_all)417 418 env = dict(os.environ, PYTHONPATH=os.pathsep.join(map(str, site_packages_all)))419 cmd = [420 sys.executable,421 '-m',422 'pip',423 'install',424 '--editable',425 str(sample_project),426 '--prefix',427 str(prefix),428 '--no-build-isolation',429 *editable_opts,430 ]431 subprocess.check_call(cmd, env=env)432 433 # now run 'sample' with the prefix on the PYTHONPATH434 bin = 'Scripts' if platform.system() == 'Windows' else 'bin'435 exe = prefix / bin / 'sample'436 subprocess.check_call([exe], env=env)437 438 439class TestFinderTemplate:440 """This test focus in getting a particular implementation detail right.441 If at some point in time the implementation is changed for something different,442 this test can be modified or even excluded.443 """444 445 def install_finder(self, finder):446 loc = {}447 exec(finder, loc, loc)448 loc["install"]()449 450 def test_packages(self, tmp_path):451 files = {452 "src1": {453 "pkg1": {454 "__init__.py": "",455 "subpkg": {"mod1.py": "a = 42"},456 },457 },458 "src2": {"mod2.py": "a = 43"},459 }460 jaraco.path.build(files, prefix=tmp_path)461 462 mapping = {463 "pkg1": str(tmp_path / "src1/pkg1"),464 "mod2": str(tmp_path / "src2/mod2"),465 }466 template = _finder_template(str(uuid4()), mapping, {})467 468 with contexts.save_paths(), contexts.save_sys_modules():469 for mod in ("pkg1", "pkg1.subpkg", "pkg1.subpkg.mod1", "mod2"):470 sys.modules.pop(mod, None)471 472 self.install_finder(template)473 mod1 = import_module("pkg1.subpkg.mod1")474 mod2 = import_module("mod2")475 subpkg = import_module("pkg1.subpkg")476 477 assert mod1.a == 42478 assert mod2.a == 43479 expected = str((tmp_path / "src1/pkg1/subpkg").resolve())480 assert_path(subpkg, expected)481 482 def test_namespace(self, tmp_path):483 files = {"pkg": {"__init__.py": "a = 13", "text.txt": "abc"}}484 jaraco.path.build(files, prefix=tmp_path)485 486 mapping = {"ns.othername": str(tmp_path / "pkg")}487 namespaces = {"ns": []}488 489 template = _finder_template(str(uuid4()), mapping, namespaces)490 with contexts.save_paths(), contexts.save_sys_modules():491 for mod in ("ns", "ns.othername"):492 sys.modules.pop(mod, None)493 494 self.install_finder(template)495 pkg = import_module("ns.othername")496 text = importlib_resources.files(pkg) / "text.txt"497 498 expected = str((tmp_path / "pkg").resolve())499 assert_path(pkg, expected)500 assert pkg.a == 13501 502 # Make sure resources can also be found503 assert text.read_text(encoding="utf-8") == "abc"504 505 def test_combine_namespaces(self, tmp_path):506 files = {507 "src1": {"ns": {"pkg1": {"__init__.py": "a = 13"}}},508 "src2": {"ns": {"mod2.py": "b = 37"}},509 }510 jaraco.path.build(files, prefix=tmp_path)511 512 mapping = {513 "ns.pkgA": str(tmp_path / "src1/ns/pkg1"),514 "ns": str(tmp_path / "src2/ns"),515 }516 namespaces_ = {"ns": [str(tmp_path / "src1"), str(tmp_path / "src2")]}517 template = _finder_template(str(uuid4()), mapping, namespaces_)518 519 with contexts.save_paths(), contexts.save_sys_modules():520 for mod in ("ns", "ns.pkgA", "ns.mod2"):521 sys.modules.pop(mod, None)522 523 self.install_finder(template)524 pkgA = import_module("ns.pkgA")525 mod2 = import_module("ns.mod2")526 527 expected = str((tmp_path / "src1/ns/pkg1").resolve())528 assert_path(pkgA, expected)529 assert pkgA.a == 13530 assert mod2.b == 37531 532 def test_combine_namespaces_nested(self, tmp_path):533 """534 Users may attempt to combine namespace packages in a nested way via535 ``package_dir`` as shown in pypa/setuptools#4248.536 """537 538 files = {539 "src": {"my_package": {"my_module.py": "a = 13"}},540 "src2": {"my_package2": {"my_module2.py": "b = 37"}},541 }542 543 stack = jaraco.path.DirectoryStack()544 with stack.context(tmp_path):545 jaraco.path.build(files)546 attrs = {547 "script_name": "%PEP 517%",548 "package_dir": {549 "different_name": "src/my_package",550 "different_name.subpkg": "src2/my_package2",551 },552 "packages": ["different_name", "different_name.subpkg"],553 }554 dist = Distribution(attrs)555 finder = _TopLevelFinder(dist, str(uuid4()))556 code = next(v for k, v in finder.get_implementation() if k.endswith(".py"))557 558 with contexts.save_paths(), contexts.save_sys_modules():559 for mod in attrs["packages"]:560 sys.modules.pop(mod, None)561 562 self.install_finder(code)563 mod1 = import_module("different_name.my_module")564 mod2 = import_module("different_name.subpkg.my_module2")565 566 expected = str((tmp_path / "src/my_package/my_module.py").resolve())567 assert str(Path(mod1.__file__).resolve()) == expected568 569 expected = str((tmp_path / "src2/my_package2/my_module2.py").resolve())570 assert str(Path(mod2.__file__).resolve()) == expected571 572 assert mod1.a == 13573 assert mod2.b == 37574 575 def test_dynamic_path_computation(self, tmp_path):576 # Follows the example in PEP 420577 files = {578 "project1": {"parent": {"child": {"one.py": "x = 1"}}},579 "project2": {"parent": {"child": {"two.py": "x = 2"}}},580 "project3": {"parent": {"child": {"three.py": "x = 3"}}},581 }582 jaraco.path.build(files, prefix=tmp_path)583 mapping = {}584 namespaces_ = {"parent": [str(tmp_path / "project1/parent")]}585 template = _finder_template(str(uuid4()), mapping, namespaces_)586 587 mods = (f"parent.child.{name}" for name in ("one", "two", "three"))588 with contexts.save_paths(), contexts.save_sys_modules():589 for mod in ("parent", "parent.child", "parent.child", *mods):590 sys.modules.pop(mod, None)591 592 self.install_finder(template)593 594 one = import_module("parent.child.one")595 assert one.x == 1596 597 with pytest.raises(ImportError):598 import_module("parent.child.two")599 600 sys.path.append(str(tmp_path / "project2"))601 two = import_module("parent.child.two")602 assert two.x == 2603 604 with pytest.raises(ImportError):605 import_module("parent.child.three")606 607 sys.path.append(str(tmp_path / "project3"))608 three = import_module("parent.child.three")609 assert three.x == 3610 611 def test_no_recursion(self, tmp_path):612 # See issue #3550613 files = {614 "pkg": {615 "__init__.py": "from . import pkg",616 },617 }618 jaraco.path.build(files, prefix=tmp_path)619 620 mapping = {621 "pkg": str(tmp_path / "pkg"),622 }623 template = _finder_template(str(uuid4()), mapping, {})624 625 with contexts.save_paths(), contexts.save_sys_modules():626 sys.modules.pop("pkg", None)627 628 self.install_finder(template)629 with pytest.raises(ImportError, match="pkg"):630 import_module("pkg")631 632 def test_similar_name(self, tmp_path):633 files = {634 "foo": {635 "__init__.py": "",636 "bar": {637 "__init__.py": "",638 },639 },640 }641 jaraco.path.build(files, prefix=tmp_path)642 643 mapping = {644 "foo": str(tmp_path / "foo"),645 }646 template = _finder_template(str(uuid4()), mapping, {})647 648 with contexts.save_paths(), contexts.save_sys_modules():649 sys.modules.pop("foo", None)650 sys.modules.pop("foo.bar", None)651 652 self.install_finder(template)653 with pytest.raises(ImportError, match="foobar"):654 import_module("foobar")655 656 def test_case_sensitivity(self, tmp_path):657 files = {658 "foo": {659 "__init__.py": "",660 "lowercase.py": "x = 1",661 "bar": {662 "__init__.py": "",663 "lowercase.py": "x = 2",664 },665 },666 }667 jaraco.path.build(files, prefix=tmp_path)668 mapping = {669 "foo": str(tmp_path / "foo"),670 }671 template = _finder_template(str(uuid4()), mapping, {})672 with contexts.save_paths(), contexts.save_sys_modules():673 sys.modules.pop("foo", None)674 675 self.install_finder(template)676 with pytest.raises(ImportError, match="'FOO'"):677 import_module("FOO")678 679 with pytest.raises(ImportError, match="'foo\\.LOWERCASE'"):680 import_module("foo.LOWERCASE")681 682 with pytest.raises(ImportError, match="'foo\\.bar\\.Lowercase'"):683 import_module("foo.bar.Lowercase")684 685 with pytest.raises(ImportError, match="'foo\\.BAR'"):686 import_module("foo.BAR.lowercase")687 688 with pytest.raises(ImportError, match="'FOO'"):689 import_module("FOO.bar.lowercase")690 691 mod = import_module("foo.lowercase")692 assert mod.x == 1693 694 mod = import_module("foo.bar.lowercase")695 assert mod.x == 2696 697 def test_namespace_case_sensitivity(self, tmp_path):698 files = {699 "pkg": {700 "__init__.py": "a = 13",701 "foo": {702 "__init__.py": "b = 37",703 "bar.py": "c = 42",704 },705 },706 }707 jaraco.path.build(files, prefix=tmp_path)708 709 mapping = {"ns.othername": str(tmp_path / "pkg")}710 namespaces = {"ns": []}711 712 template = _finder_template(str(uuid4()), mapping, namespaces)713 with contexts.save_paths(), contexts.save_sys_modules():714 for mod in ("ns", "ns.othername"):715 sys.modules.pop(mod, None)716 717 self.install_finder(template)718 pkg = import_module("ns.othername")719 expected = str((tmp_path / "pkg").resolve())720 assert_path(pkg, expected)721 assert pkg.a == 13722 723 foo = import_module("ns.othername.foo")724 assert foo.b == 37725 726 bar = import_module("ns.othername.foo.bar")727 assert bar.c == 42728 729 with pytest.raises(ImportError, match="'NS'"):730 import_module("NS.othername.foo")731 732 with pytest.raises(ImportError, match="'ns\\.othername\\.FOO\\'"):733 import_module("ns.othername.FOO")734 735 with pytest.raises(ImportError, match="'ns\\.othername\\.foo\\.BAR\\'"):736 import_module("ns.othername.foo.BAR")737 738 def test_intermediate_packages(self, tmp_path):739 """740 The finder should not import ``fullname`` if the intermediate segments741 don't exist (see pypa/setuptools#4019).742 """743 files = {744 "src": {745 "mypkg": {746 "__init__.py": "",747 "config.py": "a = 13",748 "helloworld.py": "b = 13",749 "components": {750 "config.py": "a = 37",751 },752 },753 }754 }755 jaraco.path.build(files, prefix=tmp_path)756 757 mapping = {"mypkg": str(tmp_path / "src/mypkg")}758 template = _finder_template(str(uuid4()), mapping, {})759 760 with contexts.save_paths(), contexts.save_sys_modules():761 for mod in (762 "mypkg",763 "mypkg.config",764 "mypkg.helloworld",765 "mypkg.components",766 "mypkg.components.config",767 "mypkg.components.helloworld",768 ):769 sys.modules.pop(mod, None)770 771 self.install_finder(template)772 773 config = import_module("mypkg.components.config")774 assert config.a == 37775 776 helloworld = import_module("mypkg.helloworld")777 assert helloworld.b == 13778 779 with pytest.raises(ImportError):780 import_module("mypkg.components.helloworld")781 782 783def test_pkg_roots(tmp_path):784 """This test focus in getting a particular implementation detail right.785 If at some point in time the implementation is changed for something different,786 this test can be modified or even excluded.787 """788 files = {789 "a": {"b": {"__init__.py": "ab = 1"}, "__init__.py": "a = 1"},790 "d": {"__init__.py": "d = 1", "e": {"__init__.py": "de = 1"}},791 "f": {"g": {"h": {"__init__.py": "fgh = 1"}}},792 "other": {"__init__.py": "abc = 1"},793 "another": {"__init__.py": "abcxyz = 1"},794 "yet_another": {"__init__.py": "mnopq = 1"},795 }796 jaraco.path.build(files, prefix=tmp_path)797 package_dir = {798 "a.b.c": "other",799 "a.b.c.x.y.z": "another",800 "m.n.o.p.q": "yet_another",801 }802 packages = [803 "a",804 "a.b",805 "a.b.c",806 "a.b.c.x.y",807 "a.b.c.x.y.z",808 "d",809 "d.e",810 "f",811 "f.g",812 "f.g.h",813 "m.n.o.p.q",814 ]815 roots = _find_package_roots(packages, package_dir, tmp_path)816 assert roots == {817 "a": str(tmp_path / "a"),818 "a.b.c": str(tmp_path / "other"),819 "a.b.c.x.y.z": str(tmp_path / "another"),820 "d": str(tmp_path / "d"),821 "f": str(tmp_path / "f"),822 "m.n.o.p.q": str(tmp_path / "yet_another"),823 }824 825 ns = set(dict(_find_namespaces(packages, roots)))826 assert ns == {"f", "f.g"}827 828 ns = set(_find_virtual_namespaces(roots))829 assert ns == {"a.b", "a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"}830 831 832class TestOverallBehaviour:833 PYPROJECT = """\834 [build-system]835 requires = ["setuptools"]836 build-backend = "setuptools.build_meta"837 838 [project]839 name = "mypkg"840 version = "3.14159"841 """842 843 # Any: Would need a TypedDict. Keep it simple for tests844 FLAT_LAYOUT: dict[str, Any] = {845 "pyproject.toml": dedent(PYPROJECT),846 "MANIFEST.in": EXAMPLE["MANIFEST.in"],847 "otherfile.py": "",848 "mypkg": {849 "__init__.py": "",850 "mod1.py": "var = 42",851 "subpackage": {852 "__init__.py": "",853 "mod2.py": "var = 13",854 "resource_file.txt": "resource 39",855 },856 },857 }858 859 EXAMPLES = {860 "flat-layout": FLAT_LAYOUT,861 "src-layout": {862 "pyproject.toml": dedent(PYPROJECT),863 "MANIFEST.in": EXAMPLE["MANIFEST.in"],864 "otherfile.py": "",865 "src": {"mypkg": FLAT_LAYOUT["mypkg"]},866 },867 "custom-layout": {868 "pyproject.toml": dedent(PYPROJECT)869 + dedent(870 """\871 [tool.setuptools]872 packages = ["mypkg", "mypkg.subpackage"]873 874 [tool.setuptools.package-dir]875 "mypkg.subpackage" = "other"876 """877 ),878 "MANIFEST.in": EXAMPLE["MANIFEST.in"],879 "otherfile.py": "",880 "mypkg": {881 "__init__.py": "",882 "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"],883 },884 "other": FLAT_LAYOUT["mypkg"]["subpackage"],885 },886 "namespace": {887 "pyproject.toml": dedent(PYPROJECT),888 "MANIFEST.in": EXAMPLE["MANIFEST.in"],889 "otherfile.py": "",890 "src": {891 "mypkg": {892 "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"],893 "subpackage": FLAT_LAYOUT["mypkg"]["subpackage"],894 },895 },896 },897 }898 899 @pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")900 @pytest.mark.parametrize("layout", EXAMPLES.keys())901 def test_editable_install(self, tmp_path, venv, layout, editable_opts):902 project, _ = install_project(903 "mypkg", venv, tmp_path, self.EXAMPLES[layout], *editable_opts904 )905 906 # Ensure stray files are not importable907 cmd_import_error = """\908 try:909 import otherfile910 except ImportError as ex:911 print(ex)912 """913 out = venv.run(["python", "-c", dedent(cmd_import_error)])914 assert "No module named 'otherfile'" in out915 916 # Ensure the modules are importable917 cmd_get_vars = """\918 import mypkg, mypkg.mod1, mypkg.subpackage.mod2919 print(mypkg.mod1.var, mypkg.subpackage.mod2.var)920 """921 out = venv.run(["python", "-c", dedent(cmd_get_vars)])922 assert "42 13" in out923 924 # Ensure resources are reachable925 cmd_get_resource = """\926 import mypkg.subpackage927 from setuptools._importlib import resources as importlib_resources928 text = importlib_resources.files(mypkg.subpackage) / "resource_file.txt"929 print(text.read_text(encoding="utf-8"))930 """931 out = venv.run(["python", "-c", dedent(cmd_get_resource)])932 assert "resource 39" in out933 934 # Ensure files are editable935 mod1 = next(project.glob("**/mod1.py"))936 mod2 = next(project.glob("**/mod2.py"))937 resource_file = next(project.glob("**/resource_file.txt"))938 939 mod1.write_text("var = 17", encoding="utf-8")940 mod2.write_text("var = 781", encoding="utf-8")941 resource_file.write_text("resource 374", encoding="utf-8")942 943 out = venv.run(["python", "-c", dedent(cmd_get_vars)])944 assert "42 13" not in out945 assert "17 781" in out946 947 out = venv.run(["python", "-c", dedent(cmd_get_resource)])948 assert "resource 39" not in out949 assert "resource 374" in out950 951 952class TestLinkTree:953 FILES = deepcopy(TestOverallBehaviour.EXAMPLES["src-layout"])954 FILES["pyproject.toml"] += dedent(955 """\956 [tool.setuptools]957 # Temporary workaround: both `include-package-data` and `package-data` configs958 # can be removed after #3260 is fixed.959 include-package-data = false960 package-data = {"*" = ["*.txt"]}961 962 [tool.setuptools.packages.find]963 where = ["src"]964 exclude = ["*.subpackage*"]965 """966 )967 FILES["src"]["mypkg"]["resource.not_in_manifest"] = "abc"968 969 def test_generated_tree(self, tmp_path):970 jaraco.path.build(self.FILES, prefix=tmp_path)971 972 with _Path(tmp_path):973 name = "mypkg-3.14159"974 dist = Distribution({"script_name": "%PEP 517%"})975 dist.parse_config_files()976 977 wheel = Mock()978 aux = tmp_path / ".aux"979 build = tmp_path / ".build"980 aux.mkdir()981 build.mkdir()982 983 build_py = dist.get_command_obj("build_py")984 build_py.editable_mode = True985 build_py.build_lib = str(build)986 build_py.ensure_finalized()987 outputs = build_py.get_outputs()988 output_mapping = build_py.get_output_mapping()989 990 make_tree = _LinkTree(dist, name, aux, build)991 make_tree(wheel, outputs, output_mapping)992 993 mod1 = next(aux.glob("**/mod1.py"))994 expected = tmp_path / "src/mypkg/mod1.py"995 assert_link_to(mod1, expected)996 997 assert next(aux.glob("**/subpackage"), None) is None998 assert next(aux.glob("**/mod2.py"), None) is None999 assert next(aux.glob("**/resource_file.txt"), None) is None1000 1001 assert next(aux.glob("**/resource.not_in_manifest"), None) is None1002 1003 def test_strict_install(self, tmp_path, venv):1004 opts = ["--config-settings", "editable-mode=strict"]1005 install_project("mypkg", venv, tmp_path, self.FILES, *opts)1006 1007 out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])1008 assert "42" in out1009 1010 # Ensure packages excluded from distribution are not importable1011 cmd_import_error = """\1012 try:1013 from mypkg import subpackage1014 except ImportError as ex:1015 print(ex)1016 """1017 out = venv.run(["python", "-c", dedent(cmd_import_error)])1018 assert "cannot import name 'subpackage'" in out1019 1020 # Ensure resource files excluded from distribution are not reachable1021 cmd_get_resource = """\1022 import mypkg1023 from setuptools._importlib import resources as importlib_resources1024 try:1025 text = importlib_resources.files(mypkg) / "resource.not_in_manifest"1026 print(text.read_text(encoding="utf-8"))1027 except FileNotFoundError as ex:1028 print(ex)1029 """1030 out = venv.run(["python", "-c", dedent(cmd_get_resource)])1031 assert "No such file or directory" in out1032 assert "resource.not_in_manifest" in out1033 1034 1035@pytest.mark.filterwarnings("ignore:.*compat.*:setuptools.SetuptoolsDeprecationWarning")1036def test_compat_install(tmp_path, venv):1037 # TODO: Remove `compat` after Dec/2022.1038 opts = ["--config-settings", "editable-mode=compat"]1039 files = TestOverallBehaviour.EXAMPLES["custom-layout"]1040 install_project("mypkg", venv, tmp_path, files, *opts)1041 1042 out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])1043 assert "42" in out1044 1045 expected_path = comparable_path(str(tmp_path))1046 1047 # Compatible behaviour will make spurious modules and excluded1048 # files importable directly from the original path1049 for cmd in (1050 "import otherfile; print(otherfile)",1051 "import other; print(other)",1052 "import mypkg; print(mypkg)",1053 ):1054 out = comparable_path(venv.run(["python", "-c", cmd]))1055 assert expected_path in out1056 1057 # Compatible behaviour will not consider custom mappings1058 cmd = """\1059 try:1060 from mypkg import subpackage;1061 except ImportError as ex:1062 print(ex)1063 """1064 out = venv.run(["python", "-c", dedent(cmd)])1065 assert "cannot import name 'subpackage'" in out1066 1067 1068@pytest.mark.uses_network1069def test_pbr_integration(pbr_package, venv, editable_opts):1070 """Ensure editable installs work with pbr, issue #3500"""1071 cmd = [1072 'python',1073 '-m',1074 'pip',1075 '-v',1076 'install',1077 '--editable',1078 pbr_package,1079 *editable_opts,1080 ]1081 venv.run(cmd, stderr=subprocess.STDOUT)1082 out = venv.run(["python", "-c", "import mypkg.hello"])1083 assert "Hello world!" in out1084 1085 1086class TestCustomBuildPy:1087 """1088 Issue #3501 indicates that some plugins/customizations might rely on:1089 1090 1. ``build_py`` not running1091 2. ``build_py`` always copying files to ``build_lib``1092 1093 During the transition period setuptools should prevent potential errors from1094 happening due to those assumptions.1095 """1096 1097 # TODO: Remove tests after _run_build_steps is removed.1098 1099 FILES = {1100 **TestOverallBehaviour.EXAMPLES["flat-layout"],1101 "setup.py": dedent(1102 """\1103 import pathlib1104 from setuptools import setup1105 from setuptools.command.build_py import build_py as orig1106 1107 class my_build_py(orig):1108 def run(self):1109 super().run()1110 raise ValueError("TEST_RAISE")1111 1112 setup(cmdclass={"build_py": my_build_py})1113 """1114 ),1115 }1116 1117 def test_safeguarded_from_errors(self, tmp_path, venv):1118 """Ensure that errors in custom build_py are reported as warnings"""1119 # Warnings should show up1120 _, out = install_project("mypkg", venv, tmp_path, self.FILES)1121 assert "SetuptoolsDeprecationWarning" in out1122 assert "ValueError: TEST_RAISE" in out1123 # but installation should be successful1124 out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])1125 assert "42" in out1126 1127 1128class TestCustomBuildWheel:1129 def install_custom_build_wheel(self, dist):1130 bdist_wheel_cls = dist.get_command_class("bdist_wheel")1131 1132 class MyBdistWheel(bdist_wheel_cls):1133 def get_tag(self):1134 # In issue #3513, we can see that some extensions may try to access1135 # the `plat_name` property in bdist_wheel1136 if self.plat_name.startswith("macosx-"):1137 _ = "macOS platform"1138 return super().get_tag()1139 1140 dist.cmdclass["bdist_wheel"] = MyBdistWheel1141 1142 def test_access_plat_name(self, tmpdir_cwd):1143 # Even when a custom bdist_wheel tries to access plat_name the build should1144 # be successful1145 jaraco.path.build({"module.py": "x = 42"})1146 dist = Distribution()1147 dist.script_name = "setup.py"1148 dist.set_defaults()1149 self.install_custom_build_wheel(dist)1150 cmd = editable_wheel(dist)1151 cmd.ensure_finalized()1152 cmd.run()1153 wheel_file = str(next(Path().glob('dist/*.whl')))1154 assert "editable" in wheel_file1155 1156 1157class TestCustomBuildExt:1158 def install_custom_build_ext_distutils(self, dist):1159 from distutils.command.build_ext import build_ext as build_ext_cls1160 1161 class MyBuildExt(build_ext_cls):1162 pass1163 1164 dist.cmdclass["build_ext"] = MyBuildExt1165 1166 @pytest.mark.skipif(1167 sys.platform != "linux", reason="compilers may fail without correct setup"1168 )1169 def test_distutils_leave_inplace_files(self, tmpdir_cwd):1170 jaraco.path.build({"module.c": ""})1171 attrs = {1172 "ext_modules": [Extension("module", ["module.c"])],1173 }1174 dist = Distribution(attrs)1175 dist.script_name = "setup.py"1176 dist.set_defaults()1177 self.install_custom_build_ext_distutils(dist)1178 cmd = editable_wheel(dist)1179 cmd.ensure_finalized()1180 cmd.run()1181 wheel_file = str(next(Path().glob('dist/*.whl')))1182 assert "editable" in wheel_file1183 files = [p for p in Path().glob("module.*") if p.suffix != ".c"]1184 assert len(files) == 11185 name = files[0].name1186 assert any(name.endswith(ext) for ext in EXTENSION_SUFFIXES)1187 1188 1189def test_debugging_tips(tmpdir_cwd, monkeypatch):1190 """Make sure to display useful debugging tips to the user."""1191 jaraco.path.build({"module.py": "x = 42"})1192 dist = Distribution()1193 dist.script_name = "setup.py"1194 dist.set_defaults()1195 cmd = editable_wheel(dist)1196 cmd.ensure_finalized()1197 1198 SimulatedErr = type("SimulatedErr", (Exception,), {})1199 simulated_failure = Mock(side_effect=SimulatedErr())1200 monkeypatch.setattr(cmd, "get_finalized_command", simulated_failure)