[bumpversion]
-current_version = 64.0.1
+current_version = 64.0.2
commit = True
tag = True
+v64.0.2
+-------
+
+
+Misc
+^^^^
+* #3506: Suppress errors in custom ``build_py`` implementations when running editable
+ installs in favor of a warning indicating what is the most appropriate
+ migration path.
+ This is a *transitional* measure. Errors might be raised in future versions of
+ ``setuptools``.
+* #3512: Added capability of handling namespace packages created
+ accidentally/purposefully via discovery configuration during editable installs.
+ This should emulate the behaviour of a non-editable installation.
+
+
v64.0.1
-------
end users. The *strict* editable installation is not able to detect if files
are added or removed from the project (a new installation is required).
+ This implementation might also affect plugins and customizations that assume
+ certain ``build`` subcommands don't run during editable installs or that they
+ always copy files to the temporary build directory.
+
.. important::
The *editable* aspect of the *editable install* supported this implementation
is restricted to the Python modules contained in the distributed package.
changes iteratively, before cutting a release and preparing a distribution archive.
In normal circumstances this can be quite cumbersome and require the developers
-to manipulate the ``PATHONPATH`` environment variable or to continuous re-build
+to manipulate the ``PYTHONPATH`` environment variable or to continuous re-build
and re-install the project.
To facilitate iterative exploration and experimentation, setuptools allows
[metadata]
name = setuptools
-version = 64.0.1
+version = 64.0.2
author = Python Packaging Authority
author_email = distutils-sig@python.org
description = Easily download, build, install, upgrade, and uninstall Python packages
# copy to avoid sharing the object with parent class
sub_commands = _build.sub_commands[:]
- def run(self):
+ def get_sub_commands(self):
subcommands = {cmd[0] for cmd in _build.sub_commands}
if subcommands - _ORIGINAL_SUBCOMMANDS:
msg = """
"""
warnings.warn(msg, SetuptoolsDeprecationWarning)
self.sub_commands = _build.sub_commands
- super().run()
+ return super().get_sub_commands()
class SubCommand(Protocol):
)
from setuptools import Command, SetuptoolsDeprecationWarning, errors, namespaces
+from setuptools.command.build_py import build_py as build_py_cls
from setuptools.discovery import find_package_path
from setuptools.dist import Distribution
self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
) -> Tuple[List[str], Dict[str, str]]:
self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
- self.run_command("build")
+ self._run_build_subcommands()
files, mapping = self._collect_build_outputs()
self._run_install("headers")
self._run_install("scripts")
self._run_install("data")
return files, mapping
+ def _run_build_subcommands(self):
+ """
+ Issue #3501 indicates that some plugins/customizations might rely on:
+
+ 1. ``build_py`` not running
+ 2. ``build_py`` always copying files to ``build_lib``
+
+ However both these assumptions may be false in editable_wheel.
+ This method implements a temporary workaround to support the ecosystem
+ while the implementations catch up.
+ """
+ # TODO: Once plugins/customisations had the chance to catch up, replace
+ # `self._run_build_subcommands()` with `self.run_command("build")`.
+ # Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.
+ build: Command = self.get_finalized_command("build")
+ for name in build.get_sub_commands():
+ cmd = self.distribution.get_command_obj(name)
+ if name == "build_py" and type(cmd) != build_py_cls:
+ self._safely_run(name)
+ else:
+ self.run_command(name)
+
+ def _safely_run(self, cmd_name: str):
+ try:
+ return self.run_command(cmd_name)
+ except Exception:
+ msg = f"""{traceback.format_exc()}\n
+ If you are seeing this warning it is very likely that a setuptools
+ plugin or customization overrides the `{cmd_name}` command, without
+ tacking into consideration how editable installs run build steps
+ starting from v64.0.0.
+
+ Plugin authors and developers relying on custom build steps are encouraged
+ to update their `{cmd_name}` implementation considering the information in
+ https://setuptools.pypa.io/en/latest/userguide/extension.html
+ about editable installs.
+
+ For the time being `setuptools` will silence this error and ignore
+ the faulty command, but this behaviour will change in future versions.\n
+ """
+ warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
+
def _create_wheel_file(self, bdist_wheel):
from wheel.wheelfile import WheelFile
def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
"""By carefully designing ``package_dir``, it is possible to implement the logical
structure of PEP 420 in a package without the corresponding directories.
- This function will try to find this kind of namespaces.
+
+ Moreover a parent package can be purposefully/accidentally skipped in the discovery
+ phase (e.g. ``find_packages(include=["mypkg.*"])``, when ``mypkg.foo`` is included
+ by ``mypkg`` itself is not).
+ We consider this case to also be a virtual namespace (ignoring the original
+ directory) to emulate a non-editable installation.
+
+ This function will try to find these kinds of namespaces.
"""
for pkg in pkg_roots:
if "." not in pkg:
for i in range(len(parts) - 1, 0, -1):
partial_name = ".".join(parts[:i])
path = Path(find_package_path(partial_name, pkg_roots, ""))
- if not path.exists():
+ if not path.exists() or partial_name not in pkg_roots:
+ # partial_name not in pkg_roots ==> purposefully/accidentally skipped
yield partial_name
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 str(out, "utf-8").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 str(out, "utf-8")
+
# Moved here from test_develop:
@pytest.mark.xfail(
assert ns == {"f", "f.g"}
ns = set(_find_virtual_namespaces(roots))
- assert ns == {"a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"}
+ 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:
@pytest.mark.parametrize("layout", EXAMPLES.keys())
def test_editable_install(self, tmp_path, venv, layout, editable_opts):
- opts = editable_opts
- project = install_project("mypkg", venv, tmp_path, self.EXAMPLES[layout], *opts)
+ project, _ = install_project(
+ "mypkg", venv, tmp_path, self.EXAMPLES[layout], *editable_opts
+ )
# Ensure stray files are not importable
cmd_import_error = """\
assert b"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 b"SetuptoolsDeprecationWarning" in out
+ assert b"ValueError: TEST_RAISE" in out
+ # but installation should be successful
+ out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
+ assert b"42" in out
+
+
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
- venv.run(["python", "-m", "pip", "install", "-e", str(project), *opts])
- return project
+ out = venv.run(
+ ["python", "-m", "pip", "-v", "install", "-e", str(project), *opts],
+ stderr=subprocess.STDOUT,
+ )
+ return project, out
# ---- Assertion Helpers ----