1286 lines
42 KiB
Python
1286 lines
42 KiB
Python
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 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 (
|
|
_DebuggingTips,
|
|
_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"
|
|
"""
|
|
|
|
FLAT_LAYOUT = {
|
|
"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"], # type: ignore
|
|
},
|
|
"other": FLAT_LAYOUT["mypkg"]["subpackage"], # type: ignore
|
|
},
|
|
"namespace": {
|
|
"pyproject.toml": dedent(PYPROJECT),
|
|
"MANIFEST.in": EXAMPLE["MANIFEST.in"],
|
|
"otherfile.py": "",
|
|
"src": {
|
|
"mypkg": {
|
|
"mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"], # type: ignore
|
|
"subpackage": FLAT_LAYOUT["mypkg"]["subpackage"], # type: ignore
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
@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
|
|
|
|
|
|
def test_pbr_integration(tmp_path, venv, editable_opts):
|
|
"""Ensure editable installs work with pbr, issue #3500"""
|
|
files = {
|
|
"pyproject.toml": dedent(
|
|
"""\
|
|
[build-system]
|
|
requires = ["setuptools"]
|
|
build-backend = "setuptools.build_meta"
|
|
"""
|
|
),
|
|
"setup.py": dedent(
|
|
"""\
|
|
__import__('setuptools').setup(
|
|
pbr=True,
|
|
setup_requires=["pbr"],
|
|
)
|
|
"""
|
|
),
|
|
"setup.cfg": dedent(
|
|
"""\
|
|
[metadata]
|
|
name = mypkg
|
|
|
|
[files]
|
|
packages =
|
|
mypkg
|
|
"""
|
|
),
|
|
"mypkg": {
|
|
"__init__.py": "",
|
|
"hello.py": "print('Hello world!')",
|
|
},
|
|
"other": {"test.txt": "Another file in here."},
|
|
}
|
|
venv.run(["python", "-m", "pip", "install", "pbr"])
|
|
|
|
with contexts.environment(PBR_VERSION="0.42"):
|
|
install_project("mypkg", venv, tmp_path, files, *editable_opts)
|
|
|
|
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)
|
|
|
|
expected_msg = "following steps are recommended to help debug"
|
|
with pytest.raises(SimulatedErr), pytest.warns(_DebuggingTips, match=expected_msg):
|
|
cmd.run()
|
|
|
|
|
|
@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):
|
|
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("//", "/")
|