Imported Upstream version 64.0.2 upstream/64.0.2
authorDongHun Kwak <dh0128.kwak@samsung.com>
Wed, 14 Sep 2022 05:24:45 +0000 (14:24 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Wed, 14 Sep 2022 05:24:45 +0000 (14:24 +0900)
.bumpversion.cfg
CHANGES.rst
docs/userguide/development_mode.rst
setup.cfg
setuptools/command/build.py
setuptools/command/editable_wheel.py
setuptools/tests/test_editable_install.py

index e14a394..617ffcb 100644 (file)
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 64.0.1
+current_version = 64.0.2
 commit = True
 tag = True
 
index 20aba6c..3efa5f7 100644 (file)
@@ -1,3 +1,19 @@
+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
 -------
 
@@ -40,6 +56,10 @@ Breaking Changes
   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.
index e7c755a..ddf9a3f 100644 (file)
@@ -5,7 +5,7 @@ When creating a Python project, developers usually want to implement and test
 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
index 9093b09..840b82e 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [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
index 283999d..c0676d8 100644 (file)
@@ -20,7 +20,7 @@ class build(_build):
     # 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 = """
@@ -30,7 +30,7 @@ class build(_build):
             """
             warnings.warn(msg, SetuptoolsDeprecationWarning)
             self.sub_commands = _build.sub_commands
-        super().run()
+        return super().get_sub_commands()
 
 
 class SubCommand(Protocol):
index 1bb7ddf..2631a08 100644 (file)
@@ -37,6 +37,7 @@ from typing import (
 )
 
 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
 
@@ -254,13 +255,55 @@ class editable_wheel(Command):
         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
 
@@ -585,7 +628,14 @@ def _absolute_root(path: _Path) -> str:
 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:
@@ -594,7 +644,8 @@ def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
         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
 
 
index ea31cb4..67d377e 100644 (file)
@@ -269,6 +269,54 @@ class TestPep420Namespaces:
         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(
@@ -490,7 +538,7 @@ def test_pkg_roots(tmp_path):
     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:
@@ -558,8 +606,9 @@ 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 = """\
@@ -758,13 +807,55 @@ def test_pbr_integration(tmp_path, venv, editable_opts):
     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 ----