Imported Upstream version 67.3.0 upstream/67.3.0
authorJinWang An <jinwang.an@samsung.com>
Mon, 27 Mar 2023 08:03:00 +0000 (17:03 +0900)
committerJinWang An <jinwang.an@samsung.com>
Mon, 27 Mar 2023 08:03:00 +0000 (17:03 +0900)
24 files changed:
.bumpversion.cfg
CHANGES.rst
docs/references/keywords.rst
docs/userguide/development_mode.rst
pkg_resources/__init__.py
pkg_resources/tests/test_resources.py
pytest.ini
setup.cfg
setuptools/_normalization.py [new file with mode: 0644]
setuptools/_path.py
setuptools/_reqs.py
setuptools/command/bdist_egg.py
setuptools/command/develop.py
setuptools/command/dist_info.py
setuptools/command/editable_wheel.py
setuptools/command/egg_info.py
setuptools/command/install_egg_info.py
setuptools/command/install_scripts.py
setuptools/dist.py
setuptools/installer.py
setuptools/tests/test_editable_install.py
setuptools/version.py
setuptools/wheel.py
tox.ini

index 126aa1596d9a27b9be33535e442c3cbbf3647bb6..2caed8679aca72cb86e8780e2931d3153b177daf 100644 (file)
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 67.2.0
+current_version = 67.3.0
 commit = True
 tag = True
 
index 428b833946b508e698e23aac6ceca7c79e5450d3..8c289f07a2451b22abae3c85cad23fb4459ccd1d 100644 (file)
@@ -1,3 +1,30 @@
+v67.3.0
+-------
+
+
+Deprecations
+^^^^^^^^^^^^
+* #3434: Added deprecation warning for ``pkg_resources.declare_namespace``.
+  Users that wish to implement namespace packages, are recommended to follow the
+  practice described in PEP 420 and omit the ``__init__.py`` file entirely.
+
+Changes
+^^^^^^^
+* #3792: Reduced usage of ``pkg_resources`` in ``setuptools`` via internal
+  restructuring and refactoring.
+
+Misc
+^^^^
+* #3822: Added debugging tips for "editable mode" and update related docs.
+  Instead of using a custom exception to display the help message to the user,
+  ``setuptools`` will now use a warning and re-raise the original exception.
+* #3822: Added clarification about ``editable_wheel`` and ``dist_info`` CLI commands:
+  they should not be called directly with ``python setup.py ...``.
+  Instead they are reserved for internal use of ``setuptools`` (effectively as "private" commands).
+  Users are recommended to rely on build backend APIs (:pep:`517` and :pep:`660`)
+  exposed by ``setuptools.build_meta``.
+
+
 v67.2.0
 -------
 
index ade147ad75efd8b2db99ceefb9997fba2c27254d..6173e3c2289b59dfb6f3472319b53c5a124a9c4b 100644 (file)
@@ -390,7 +390,10 @@ extensions).
 
 ``namespace_packages``
     .. warning::
-        ``namespace_packages`` is deprecated in favor of native/implicit
+        The ``namespace_packages`` implementation relies on ``pkg_resources``.
+        However, ``pkg_resources`` has some undesirable behaviours, and
+        Setuptools intends to obviate its usage in the future. Therefore,
+        ``namespace_packages`` was deprecated in favor of native/implicit
         namespaces (:pep:`420`). Check :doc:`the Python Packaging User Guide
         <PyPUG:guides/packaging-namespace-packages>` for more information.
 
index 6f9f5417f7fab5aaa81ff84f1e35a0d22cd659f1..12d50fbc93bcfdb73b2492ff1d21b1dbddf4c0cc 100644 (file)
@@ -192,16 +192,12 @@ works (still within the context of :pep:`660`).
    Users are encouraged to try out the new editable installation techniques
    and make the necessary adaptations.
 
-If the ``compat`` mode does not work for you, you can also disable the
-:pep:`editable install <660>` hooks in ``setuptools`` by setting an environment
-variable:
-
-.. code-block::
-
-   SETUPTOOLS_ENABLE_FEATURES="legacy-editable"
-
-This *may* cause the installer (e.g. ``pip``) to effectively run the "legacy"
-installation command: ``python setup.py develop`` [#installer]_.
+.. note::
+   Newer versions of ``pip`` no longer run the fallback command
+   ``python setup.py develop`` when the ``pyproject.toml`` file is present.
+   This means that setting the environment variable
+   ``SETUPTOOLS_ENABLE_FEATURES="legacy-editable"``
+   will have no effect when installing a package with ``pip``.
 
 
 How editable installations work
@@ -251,11 +247,6 @@ More information is available on the text of :pep:`PEP 660 <660#what-to-put-in-t
    <https://blog.ganssle.io/articles/2019/08/test-as-installed.html>`_ for more
    insights).
 
-.. [#installer]
-   For this workaround to work, the installer tool needs to support legacy
-   editable installations. (Future versions of ``pip``, for example, may drop
-   support for this feature).
-
 .. [#criteria]
    ``setuptools`` strives to find a balance between allowing the user to see
    the effects of project files being edited while still trying to keep the
index 0ae951b6282fff0f3026e2f3b25b9654a7938380..aed691ac4c7bd806f3c50cb30c51cab0275c4c5b 100644 (file)
@@ -2288,6 +2288,14 @@ def _rebuild_mod_path(orig_path, package_name, module):
 def declare_namespace(packageName):
     """Declare that package 'packageName' is a namespace package"""
 
+    msg = (
+        "Implementing implicit namespace packages (as specified in PEP 420) "
+        "is preferred to `pkg_resources.declare_namespace`. "
+        "See https://setuptools.pypa.io/en/latest/references/"
+        "keywords.html#keyword-namespace-packages"
+    )
+    warnings.warn(msg, DeprecationWarning, stacklevel=2)
+
     _imp.acquire_lock()
     try:
         if packageName in _namespace_packages:
index 107dda7babc2348d1f6bd606c378dbe85074a7b7..2138f95e7b800b48e689067bef3264337092358b 100644 (file)
@@ -830,10 +830,12 @@ class TestNamespaces:
             pkg2.ensure_dir()
             (pkg1 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
             (pkg2 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
-        import pkg1
+        with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
+            import pkg1
         assert "pkg1" in pkg_resources._namespace_packages
         # attempt to import pkg2 from site-pkgs2
-        import pkg1.pkg2
+        with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
+            import pkg1.pkg2
         # check the _namespace_packages dict
         assert "pkg1.pkg2" in pkg_resources._namespace_packages
         assert pkg_resources._namespace_packages["pkg1"] == ["pkg1.pkg2"]
@@ -874,8 +876,9 @@ class TestNamespaces:
             (subpkg / '__init__.py').write_text(
                 vers_str % number, encoding='utf-8')
 
-        import nspkg.subpkg
-        import nspkg
+        with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
+            import nspkg.subpkg
+            import nspkg
         expected = [
             str(site.realpath() / 'nspkg')
             for site in site_dirs
index 12007aded67b055cc3872c59a13a1a4ff8b6c96b..1a651f5575acd158f41b4ae8e6e4969b356c8e8d 100644 (file)
@@ -10,6 +10,7 @@ filterwarnings=
        error
 
        ## upstream
+
        # Ensure ResourceWarnings are emitted
        default::ResourceWarning
 
@@ -26,8 +27,15 @@ filterwarnings=
        ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning
        ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning
 
-       # dbader/pytest-mypy#131
-       ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestDeprecationWarning
+       # shopkeep/pytest-black#67
+       ignore:'encoding' argument not specified::pytest_black
+
+       # realpython/pytest-mypy#152
+       ignore:'encoding' argument not specified::pytest_mypy
+
+       # python/cpython#100750
+       ignore:'encoding' argument not specified::platform
+
        ## end upstream
 
        # https://github.com/pypa/setuptools/issues/1823
@@ -40,7 +48,7 @@ filterwarnings=
        ignore:The Windows bytes API has been deprecated:DeprecationWarning
 
        # https://github.com/pypa/setuptools/issues/2823
-       ignore:setuptools.installer is deprecated.
+       ignore:setuptools.installer and fetch_build_eggs are deprecated.
 
        # https://github.com/pypa/setuptools/issues/917
        ignore:setup.py install is deprecated.
@@ -70,3 +78,11 @@ filterwarnings=
 
        # https://github.com/pypa/setuptools/issues/3655
        ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning
+
+       # Workarounds for pypa/setuptools#3810
+       # Can't use EncodingWarning as it doesn't exist on Python 3.9
+       default:'encoding' argument not specified
+       default:UTF-8 Mode affects locale.getpreferredencoding().
+
+       # Avoid errors when testing pkg_resources.declare_namespace
+       ignore:.*pkg_resources\.declare_namespace.*:DeprecationWarning
index 41ca2f73d978766779b8b669f1025c1384d6945a..129a9351e4e8f0b363c146dc54d123b372a24c45 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 67.2.0
+version = 67.3.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py
new file mode 100644 (file)
index 0000000..8ba7c80
--- /dev/null
@@ -0,0 +1,117 @@
+"""
+Helpers for normalization as expected in wheel/sdist/module file names
+and core metadata
+"""
+import re
+import warnings
+from inspect import cleandoc
+from pathlib import Path
+from typing import Union
+
+from setuptools.extern import packaging
+
+from ._deprecation_warning import SetuptoolsDeprecationWarning
+
+_Path = Union[str, Path]
+
+# https://packaging.python.org/en/latest/specifications/core-metadata/#name
+_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
+_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9.]+", re.I)
+
+
+def safe_identifier(name: str) -> str:
+    """Make a string safe to be used as Python identifier.
+    >>> safe_identifier("12abc")
+    '_12abc'
+    >>> safe_identifier("__editable__.myns.pkg-78.9.3_local")
+    '__editable___myns_pkg_78_9_3_local'
+    """
+    safe = re.sub(r'\W|^(?=\d)', '_', name)
+    assert safe.isidentifier()
+    return safe
+
+
+def safe_name(component: str) -> str:
+    """Escape a component used as a project name according to Core Metadata.
+    >>> safe_name("hello world")
+    'hello-world'
+    >>> safe_name("hello?world")
+    'hello-world'
+    """
+    # See pkg_resources.safe_name
+    return _UNSAFE_NAME_CHARS.sub("-", component)
+
+
+def safe_version(version: str) -> str:
+    """Convert an arbitrary string into a valid version string.
+    >>> safe_version("1988 12 25")
+    '1988.12.25'
+    >>> safe_version("v0.2.1")
+    '0.2.1'
+    >>> safe_version("v0.2?beta")
+    '0.2b0'
+    >>> safe_version("v0.2 beta")
+    '0.2b0'
+    >>> safe_version("ubuntu lts")
+    Traceback (most recent call last):
+    ...
+    setuptools.extern.packaging.version.InvalidVersion: Invalid version: 'ubuntu.lts'
+    """
+    v = version.replace(' ', '.')
+    try:
+        return str(packaging.version.Version(v))
+    except packaging.version.InvalidVersion:
+        attempt = _UNSAFE_NAME_CHARS.sub("-", v)
+        return str(packaging.version.Version(attempt))
+
+
+def best_effort_version(version: str) -> str:
+    """Convert an arbitrary string into a version-like string.
+    >>> best_effort_version("v0.2 beta")
+    '0.2b0'
+
+    >>> import warnings
+    >>> warnings.simplefilter("ignore", category=SetuptoolsDeprecationWarning)
+    >>> best_effort_version("ubuntu lts")
+    'ubuntu.lts'
+    """
+    # See pkg_resources.safe_version
+    try:
+        return safe_version(version)
+    except packaging.version.InvalidVersion:
+        msg = f"""Invalid version: {version!r}.
+        !!\n\n
+        ###################
+        # Invalid version #
+        ###################
+        {version!r} is not valid according to PEP 440.\n
+        Please make sure specify a valid version for your package.
+        Also note that future releases of setuptools may halt the build process
+        if an invalid version is given.
+        \n\n!!
+        """
+        warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning)
+        v = version.replace(' ', '.')
+        return safe_name(v)
+
+
+def filename_component(value: str) -> str:
+    """Normalize each component of a filename (e.g. distribution/version part of wheel)
+    Note: ``value`` needs to be already normalized.
+    >>> filename_component("my-pkg")
+    'my_pkg'
+    """
+    return value.replace("-", "_").strip("_")
+
+
+def safer_name(value: str) -> str:
+    """Like ``safe_name`` but can be used as filename component for wheel"""
+    # See bdist_wheel.safer_name
+    return filename_component(safe_name(value))
+
+
+def safer_best_effort_version(value: str) -> str:
+    """Like ``best_effort_version`` but can be used as filename component for wheel"""
+    # See bdist_wheel.safer_verion
+    # TODO: Replace with only safe_version in the future (no need for best effort)
+    return filename_component(best_effort_version(value))
index 3767523b784bb93b5b79890eff359628fcfcaa34..b99d9dadcfc3789629aa803d3256cd22d2873c29 100644 (file)
@@ -1,4 +1,5 @@
 import os
+import sys
 from typing import Union
 
 _Path = Union[str, os.PathLike]
@@ -26,4 +27,11 @@ def same_path(p1: _Path, p2: _Path) -> bool:
     >>> same_path("a", "a/b")
     False
     """
-    return os.path.normpath(p1) == os.path.normpath(p2)
+    return normpath(p1) == normpath(p2)
+
+
+def normpath(filename: _Path) -> str:
+    """Normalize a file/dir name for comparison purposes."""
+    # See pkg_resources.normalize_path for notes about cygwin
+    file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
+    return os.path.normcase(os.path.realpath(os.path.normpath(file)))
index ca7241746b18940a5f9a4bcd9dddd4b70a12e3f7..5d5b927fd8728592fa8eb11891b1a6b1379c4199 100644 (file)
@@ -1,9 +1,13 @@
+from typing import Callable, Iterable, Iterator, TypeVar, Union, overload
+
 import setuptools.extern.jaraco.text as text
+from setuptools.extern.packaging.requirements import Requirement
 
-from pkg_resources import Requirement
+_T = TypeVar("_T")
+_StrOrIter = Union[str, Iterable[str]]
 
 
-def parse_strings(strs):
+def parse_strings(strs: _StrOrIter) -> Iterator[str]:
     """
     Yield requirement strings for each specification in `strs`.
 
@@ -12,8 +16,18 @@ def parse_strings(strs):
     return text.join_continuation(map(text.drop_comment, text.yield_lines(strs)))
 
 
-def parse(strs):
+@overload
+def parse(strs: _StrOrIter) -> Iterator[Requirement]:
+    ...
+
+
+@overload
+def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> Iterator[_T]:
+    ...
+
+
+def parse(strs, parser=Requirement):
     """
-    Deprecated drop-in replacement for pkg_resources.parse_requirements.
+    Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``.
     """
-    return map(Requirement, parse_strings(strs))
+    return map(parser, parse_strings(strs))
index 11a1c6be28ad008b7c083c229bb0df644ec58a0e..33f483cf5014d5c93025f35caaa2ed048acc733f 100644 (file)
@@ -11,7 +11,6 @@ import re
 import textwrap
 import marshal
 
-from pkg_resources import get_build_platform, Distribution
 from setuptools.extension import Library
 from setuptools import Command
 from .._path import ensure_directory
@@ -64,7 +63,7 @@ class bdist_egg(Command):
         ('bdist-dir=', 'b',
          "temporary directory for creating the distribution"),
         ('plat-name=', 'p', "platform name to embed in generated filenames "
-                            "(default: %s)" % get_build_platform()),
+                            "(by default uses `pkg_resources.get_build_platform()`)"),
         ('exclude-source-files', None,
          "remove all .py files from the generated egg"),
         ('keep-temp', 'k',
@@ -98,18 +97,18 @@ class bdist_egg(Command):
             self.bdist_dir = os.path.join(bdist_base, 'egg')
 
         if self.plat_name is None:
+            from pkg_resources import get_build_platform
+
             self.plat_name = get_build_platform()
 
         self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
 
         if self.egg_output is None:
-
             # Compute filename of the output egg
-            basename = Distribution(
-                None, None, ei_cmd.egg_name, ei_cmd.egg_version,
-                get_python_version(),
-                self.distribution.has_ext_modules() and self.plat_name
-            ).egg_name()
+            basename = ei_cmd._get_egg_basename(
+                py_version=get_python_version(),
+                platform=self.distribution.has_ext_modules() and self.plat_name,
+            )
 
             self.egg_output = os.path.join(self.dist_dir, basename + '.egg')
 
index 24fb0a7c81bc665844d5d307eee2d720079c039f..5630ca4cdc2344ccb2e6eef29e28bfe064802866 100644 (file)
@@ -5,8 +5,8 @@ import os
 import glob
 import io
 
-import pkg_resources
 from setuptools.command.easy_install import easy_install
+from setuptools import _path
 from setuptools import namespaces
 import setuptools
 
@@ -42,6 +42,8 @@ class develop(namespaces.DevelopInstaller, easy_install):
         self.always_copy_from = '.'  # always copy eggs installed in curdir
 
     def finalize_options(self):
+        import pkg_resources
+
         ei = self.get_finalized_command("egg_info")
         if ei.broken_egg_info:
             template = "Please rename %r to %r before using 'develop'"
@@ -61,10 +63,8 @@ class develop(namespaces.DevelopInstaller, easy_install):
         if self.egg_path is None:
             self.egg_path = os.path.abspath(ei.egg_base)
 
-        target = pkg_resources.normalize_path(self.egg_base)
-        egg_path = pkg_resources.normalize_path(
-            os.path.join(self.install_dir, self.egg_path)
-        )
+        target = _path.normpath(self.egg_base)
+        egg_path = _path.normpath(os.path.join(self.install_dir, self.egg_path))
         if egg_path != target:
             raise DistutilsOptionError(
                 "--egg-path must be a relative path from the install"
@@ -94,15 +94,16 @@ class develop(namespaces.DevelopInstaller, easy_install):
         path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
         if path_to_setup != os.curdir:
             path_to_setup = '../' * (path_to_setup.count('/') + 1)
-        resolved = pkg_resources.normalize_path(
+        resolved = _path.normpath(
             os.path.join(install_dir, egg_path, path_to_setup)
         )
-        if resolved != pkg_resources.normalize_path(os.curdir):
+        curdir = _path.normpath(os.curdir)
+        if resolved != curdir:
             raise DistutilsOptionError(
                 "Can't get a consistent path to setup script from"
                 " installation directory",
                 resolved,
-                pkg_resources.normalize_path(os.curdir),
+                curdir,
             )
         return path_to_setup
 
index 0685c94596f2e74642ecf57b33b6c20f937d03c0..40fdfd0a2844a0da9ecb6eb2cafba9f4b799bf79 100644 (file)
@@ -4,23 +4,25 @@ As defined in the wheel specification
 """
 
 import os
-import re
 import shutil
 import sys
 import warnings
 from contextlib import contextmanager
-from inspect import cleandoc
+from distutils import log
+from distutils.core import Command
 from pathlib import Path
 
-from distutils.core import Command
-from distutils import log
-from setuptools.extern import packaging
-from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
+from .. import _normalization
+from .._deprecation_warning import SetuptoolsDeprecationWarning
 
 
 class dist_info(Command):
+    """
+    This command is private and reserved for internal use of setuptools,
+    users should rely on ``setuptools.build_meta`` APIs.
+    """
 
-    description = 'create a .dist-info directory'
+    description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create .dist-info directory"
 
     user_options = [
         ('egg-base=', 'e', "directory containing .egg-info directories"
@@ -72,8 +74,8 @@ class dist_info(Command):
         egg_info.finalize_options()
         self.egg_info = egg_info
 
-        name = _safe(dist.get_name())
-        version = _version(dist.get_version())
+        name = _normalization.safer_name(dist.get_name())
+        version = _normalization.safer_best_effort_version(dist.get_version())
         self.name = f"{name}-{version}"
         self.dist_info_dir = os.path.join(self.output_dir, f"{self.name}.dist-info")
 
@@ -105,32 +107,6 @@ class dist_info(Command):
             bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir)
 
 
-def _safe(component: str) -> str:
-    """Escape a component used to form a wheel name according to PEP 491"""
-    return re.sub(r"[^\w\d.]+", "_", component)
-
-
-def _version(version: str) -> str:
-    """Convert an arbitrary string to a version string."""
-    v = version.replace(' ', '.')
-    try:
-        return str(packaging.version.Version(v)).replace("-", "_")
-    except packaging.version.InvalidVersion:
-        msg = f"""Invalid version: {version!r}.
-        !!\n\n
-        ###################
-        # Invalid version #
-        ###################
-        {version!r} is not valid according to PEP 440.\n
-        Please make sure specify a valid version for your package.
-        Also note that future releases of setuptools may halt the build process
-        if an invalid version is given.
-        \n\n!!
-        """
-        warnings.warn(cleandoc(msg))
-        return _safe(v).strip("_")
-
-
 def _rm(dir_name, **opts):
     if os.path.isdir(dir_name):
         shutil.rmtree(dir_name, **opts)
index d60cfbebb7cc4a24f8d56facf841637b04661714..6fddf03d61623833f6f080ff53ba084d930e4b5e 100644 (file)
@@ -12,7 +12,6 @@ Create a wheel that, when installed, will make the source package 'editable'
 
 import logging
 import os
-import re
 import shutil
 import sys
 import traceback
@@ -36,10 +35,17 @@ from typing import (
     Union,
 )
 
-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
+from .. import (
+    Command,
+    SetuptoolsDeprecationWarning,
+    _normalization,
+    _path,
+    errors,
+    namespaces,
+)
+from ..discovery import find_package_path
+from ..dist import Distribution
+from .build_py import build_py as build_py_cls
 
 if TYPE_CHECKING:
     from wheel.wheelfile import WheelFile  # noqa
@@ -104,10 +110,11 @@ Options like `package-data`, `include/exclude-package-data` or
 
 class editable_wheel(Command):
     """Build 'editable' wheel for development.
-    (This command is reserved for internal use of setuptools).
+    This command is private and reserved for internal use of setuptools,
+    users should rely on ``setuptools.build_meta`` APIs.
     """
 
-    description = "create a PEP 660 'editable' wheel"
+    description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create PEP 660 editable wheel"
 
     user_options = [
         ("dist-dir=", "d", "directory to put final built distributions in"),
@@ -138,20 +145,11 @@ class editable_wheel(Command):
             bdist_wheel.write_wheelfile(self.dist_info_dir)
 
             self._create_wheel_file(bdist_wheel)
-        except Exception as ex:
+        except Exception:
             traceback.print_exc()
-            msg = """
-            Support for editable installs via PEP 660 was recently introduced
-            in `setuptools`. If you are seeing this error, please report to:
-
-            https://github.com/pypa/setuptools/issues
-
-            Meanwhile you can try the legacy behavior by setting an
-            environment variable and trying to install again:
-
-            SETUPTOOLS_ENABLE_FEATURES="legacy-editable"
-            """
-            raise errors.InternalError(cleandoc(msg)) from ex
+            project = self.distribution.name or self.distribution.get_name()
+            _DebuggingTips.warn(project)
+            raise
 
     def _ensure_dist_info(self):
         if self.dist_info_dir is None:
@@ -490,7 +488,7 @@ class _TopLevelFinder:
         ))
 
         name = f"__editable__.{self.name}.finder"
-        finder = _make_identifier(name)
+        finder = _normalization.safe_identifier(name)
         content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
         wheel.writestr(f"{finder}.py", content)
 
@@ -569,7 +567,7 @@ def _simple_layout(
         return set(package_dir) in ({}, {""})
     parent = os.path.commonpath([_parent_path(k, v) for k, v in layout.items()])
     return all(
-        _normalize_path(Path(parent, *key.split('.'))) == _normalize_path(value)
+        _path.same_path(Path(parent, *key.split('.')), value)
         for key, value in layout.items()
     )
 
@@ -698,21 +696,14 @@ def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:
     >>> _is_nested("b.a", "path/b/a", "a", "path/a")
     False
     """
-    norm_pkg_path = _normalize_path(pkg_path)
+    norm_pkg_path = _path.normpath(pkg_path)
     rest = pkg.replace(parent, "", 1).strip(".").split(".")
     return (
         pkg.startswith(parent)
-        and norm_pkg_path == _normalize_path(Path(parent_path, *rest))
+        and norm_pkg_path == _path.normpath(Path(parent_path, *rest))
     )
 
 
-def _normalize_path(filename: _Path) -> str:
-    """Normalize a file/dir name for comparison purposes"""
-    # See pkg_resources.normalize_path
-    file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
-    return os.path.normcase(os.path.realpath(os.path.normpath(file)))
-
-
 def _empty_dir(dir_: _P) -> _P:
     """Create a directory ensured to be empty. Existing files may be removed."""
     shutil.rmtree(dir_, ignore_errors=True)
@@ -720,18 +711,6 @@ def _empty_dir(dir_: _P) -> _P:
     return dir_
 
 
-def _make_identifier(name: str) -> str:
-    """Make a string safe to be used as Python identifier.
-    >>> _make_identifier("12abc")
-    '_12abc'
-    >>> _make_identifier("__editable__.myns.pkg-78.9.3_local")
-    '__editable___myns_pkg_78_9_3_local'
-    """
-    safe = re.sub(r'\W|^(?=\d)', '_', name)
-    assert safe.isidentifier()
-    return safe
-
-
 class _NamespaceInstaller(namespaces.Installer):
     def __init__(self, distribution, installation_dir, editable_name, src_root):
         self.distribution = distribution
@@ -842,3 +821,36 @@ class InformationOnly(UserWarning):
 
 class LinksNotSupported(errors.FileError):
     """File system does not seem to support either symlinks or hard links."""
+
+
+class _DebuggingTips(InformationOnly):
+    @classmethod
+    def warn(cls, project: str):
+        msg = f"""An error happened while installing {project!r} in editable mode.
+
+        ************************************************************************
+        The following steps are recommended to help debugging this problem:
+
+        - Try to install the project normally, without using the editable mode.
+          Does the error still persists?
+          (If it does, try fixing the problem before attempting the editable mode).
+        - If you are using binary extensions, make sure you have all OS-level
+          dependencies installed (e.g. compilers, toolchains, binary libraries, ...).
+        - Try the latest version of setuptools (maybe the error was already fixed).
+        - If you (or your project dependencies) are using any setuptools extension
+          or customization, make sure they support the editable mode.
+
+        After following the steps above, if the problem still persist and
+        you think this is related to how setuptools handles editable installations,
+        please submit a reproducible example
+        (see https://stackoverflow.com/help/minimal-reproducible-example) to:
+
+            https://github.com/pypa/setuptools/issues
+
+        More information about editable installs can be found in the docs:
+
+            https://setuptools.pypa.io/en/latest/userguide/development_mode.html
+        ************************************************************************
+        """
+        # We cannot use `add_notes` since pip hides PEP 678 notes
+        warnings.warn(msg, cls, stacklevel=2)
index 86e99dd207c67ce89f340eb08a3036ae170667f5..afcde5a2a13b2f41c7750cd69ac20dfbf5befa48 100644 (file)
@@ -18,16 +18,13 @@ import time
 import collections
 
 from .._importlib import metadata
-from .. import _entry_points
+from .. import _entry_points, _normalization
 
 from setuptools import Command
 from setuptools.command.sdist import sdist
 from setuptools.command.sdist import walk_revctrl
 from setuptools.command.setopt import edit_config
 from setuptools.command import bdist_egg
-from pkg_resources import (
-    Requirement, safe_name, parse_version,
-    safe_version, to_filename)
 import setuptools.unicode_utils as unicode_utils
 from setuptools.glob import glob
 
@@ -36,6 +33,9 @@ from setuptools.extern.jaraco.text import yield_lines
 from setuptools import SetuptoolsDeprecationWarning
 
 
+PY_MAJOR = '{}.{}'.format(*sys.version_info)
+
+
 def translate_pattern(glob):  # noqa: C901  # is too complex (14)  # FIXME
     """
     Translate a file path glob like '*.txt' in to a regular expression.
@@ -125,10 +125,11 @@ class InfoCommon:
 
     @property
     def name(self):
-        return safe_name(self.distribution.get_name())
+        return _normalization.safe_name(self.distribution.get_name())
 
     def tagged_version(self):
-        return safe_version(self._maybe_tag(self.distribution.get_version()))
+        tagged = self._maybe_tag(self.distribution.get_version())
+        return _normalization.best_effort_version(tagged)
 
     def _maybe_tag(self, version):
         """
@@ -148,7 +149,7 @@ class InfoCommon:
     def _safe_tags(self) -> str:
         # To implement this we can rely on `safe_version` pretending to be version 0
         # followed by tags. Then we simply discard the starting 0 (fake version number)
-        return safe_version(f"0{self.vtags}")[1:]
+        return _normalization.best_effort_version(f"0{self.vtags}")[1:]
 
     def tags(self) -> str:
         version = ''
@@ -216,12 +217,12 @@ class egg_info(InfoCommon, Command):
         # repercussions.
         self.egg_name = self.name
         self.egg_version = self.tagged_version()
-        parsed_version = parse_version(self.egg_version)
+        parsed_version = packaging.version.Version(self.egg_version)
 
         try:
             is_version = isinstance(parsed_version, packaging.version.Version)
             spec = "%s==%s" if is_version else "%s===%s"
-            Requirement(spec % (self.egg_name, self.egg_version))
+            packaging.requirements.Requirement(spec % (self.egg_name, self.egg_version))
         except ValueError as e:
             raise distutils.errors.DistutilsOptionError(
                 "Invalid distribution name or version syntax: %s-%s" %
@@ -233,7 +234,7 @@ class egg_info(InfoCommon, Command):
             self.egg_base = (dirs or {}).get('', os.curdir)
 
         self.ensure_dirname('egg_base')
-        self.egg_info = to_filename(self.egg_name) + '.egg-info'
+        self.egg_info = _normalization.filename_component(self.egg_name) + '.egg-info'
         if self.egg_base != os.curdir:
             self.egg_info = os.path.join(self.egg_base, self.egg_info)
         if '-' in self.egg_name:
@@ -251,9 +252,13 @@ class egg_info(InfoCommon, Command):
         pd = self.distribution._patched_dist
         if pd is not None and pd.key == self.egg_name.lower():
             pd._version = self.egg_version
-            pd._parsed_version = parse_version(self.egg_version)
+            pd._parsed_version = packaging.version.Version(self.egg_version)
             self.distribution._patched_dist = None
 
+    def _get_egg_basename(self, py_version=PY_MAJOR, platform=None):
+        """Compute filename of the output egg. Private API."""
+        return _egg_basename(self.egg_name, self.egg_version, py_version, platform)
+
     def write_or_delete_file(self, what, filename, data, force=False):
         """Write `data` to `filename` or delete if empty
 
@@ -771,5 +776,15 @@ def get_pkg_info_revision():
     return 0
 
 
+def _egg_basename(egg_name, egg_version, py_version=None, platform=None):
+    """Compute filename of the output egg. Private API."""
+    name = _normalization.filename_component(egg_name)
+    version = _normalization.filename_component(egg_version)
+    egg = f"{name}-{version}-py{py_version or PY_MAJOR}"
+    if platform:
+        egg += f"-{platform}"
+    return egg
+
+
 class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):
     """Deprecated behavior warning for EggInfo, bypassing suppression."""
index 65ede406bfa32204acecb48a3fc73537b2801ddc..1c549c98ea430c8d08dc5968f19ea23e6fe6c9b0 100644 (file)
@@ -5,7 +5,6 @@ from setuptools import Command
 from setuptools import namespaces
 from setuptools.archive_util import unpack_archive
 from .._path import ensure_directory
-import pkg_resources
 
 
 class install_egg_info(namespaces.Installer, Command):
@@ -24,9 +23,7 @@ class install_egg_info(namespaces.Installer, Command):
         self.set_undefined_options('install_lib',
                                    ('install_dir', 'install_dir'))
         ei_cmd = self.get_finalized_command("egg_info")
-        basename = pkg_resources.Distribution(
-            None, None, ei_cmd.egg_name, ei_cmd.egg_version
-        ).egg_name() + '.egg-info'
+        basename = f"{ei_cmd._get_egg_basename()}.egg-info"
         self.source = ei_cmd.egg_info
         self.target = os.path.join(self.install_dir, basename)
         self.outputs = []
index aeb0e4240cd92c77e0ba96e6651487625cd9f391..8b3133f1fdfa67e58e4f944ecec432f42a4c048a 100644 (file)
@@ -4,7 +4,6 @@ from distutils.errors import DistutilsModuleError
 import os
 import sys
 
-from pkg_resources import Distribution, PathMetadata
 from .._path import ensure_directory
 
 
@@ -16,8 +15,6 @@ class install_scripts(orig.install_scripts):
         self.no_ep = False
 
     def run(self):
-        import setuptools.command.easy_install as ei
-
         self.run_command("egg_info")
         if self.distribution.scripts:
             orig.install_scripts.run(self)  # run first to set up self.outfiles
@@ -26,6 +23,12 @@ class install_scripts(orig.install_scripts):
         if self.no_ep:
             # don't install entry point scripts into .egg file!
             return
+        self._install_ep_scripts()
+
+    def _install_ep_scripts(self):
+        # Delay import side-effects
+        from pkg_resources import Distribution, PathMetadata
+        from . import easy_install as ei
 
         ei_cmd = self.get_finalized_command("egg_info")
         dist = Distribution(
index cd34d74a9c88d11605ccff342e3e3cdde6b9abff..eb59f3a0a3017fe60ef09d411b877a6d18036326 100644 (file)
@@ -17,6 +17,7 @@ from distutils.fancy_getopt import translate_longopt
 from glob import iglob
 import itertools
 import textwrap
+from contextlib import suppress
 from typing import List, Optional, Set, TYPE_CHECKING
 from pathlib import Path
 
@@ -32,7 +33,7 @@ from setuptools.extern.more_itertools import unique_everseen, partition
 
 from ._importlib import metadata
 
-from . import SetuptoolsDeprecationWarning
+from . import SetuptoolsDeprecationWarning, _normalization
 
 import setuptools
 import setuptools.command
@@ -41,7 +42,6 @@ from setuptools.monkey import get_unpatched
 from setuptools.config import setupcfg, pyprojecttoml
 from setuptools.discovery import ConfigDiscovery
 
-import pkg_resources
 from setuptools.extern.packaging import version
 from . import _reqs
 from . import _entry_points
@@ -280,7 +280,9 @@ def check_nsp(dist, attr, value):
             )
         msg = (
             "The namespace_packages parameter is deprecated, "
-            "consider using implicit namespaces instead (PEP 420)."
+            "consider using implicit namespaces instead (PEP 420). "
+            "See https://setuptools.pypa.io/en/latest/references/"
+            "keywords.html#keyword-namespace-packages"
         )
         warnings.warn(msg, SetuptoolsDeprecationWarning)
 
@@ -299,11 +301,21 @@ def check_extras(dist, attr, value):
 
 def _check_extra(extra, reqs):
     name, sep, marker = extra.partition(':')
-    if marker and pkg_resources.invalid_marker(marker):
-        raise DistutilsSetupError("Invalid environment marker: " + marker)
+    try:
+        _check_marker(marker)
+    except packaging.markers.InvalidMarker:
+        msg = f"Invalid environment marker: {marker} ({extra!r})"
+        raise DistutilsSetupError(msg) from None
     list(_reqs.parse(reqs))
 
 
+def _check_marker(marker):
+    if not marker:
+        return
+    m = packaging.markers.Marker(marker)
+    m.evaluate()
+
+
 def assert_bool(dist, attr, value):
     """Verify that value is True, False, 0, or 1"""
     if bool(value) != value:
@@ -453,11 +465,12 @@ class Distribution(_Distribution):
         #
         if not attrs or 'name' not in attrs or 'version' not in attrs:
             return
-        key = pkg_resources.safe_name(str(attrs['name'])).lower()
-        dist = pkg_resources.working_set.by_key.get(key)
-        if dist is not None and not dist.has_metadata('PKG-INFO'):
-            dist._version = pkg_resources.safe_version(str(attrs['version']))
-            self._patched_dist = dist
+        name = _normalization.safe_name(str(attrs['name'])).lower()
+        with suppress(metadata.PackageNotFoundError):
+            dist = metadata.distribution(name)
+            if dist is not None and not dist.read_text('PKG-INFO'):
+                dist._version = _normalization.safe_version(str(attrs['version']))
+                self._patched_dist = dist
 
     def __init__(self, attrs=None):
         have_package_data = hasattr(self, "package_data")
@@ -876,14 +889,9 @@ class Distribution(_Distribution):
 
     def fetch_build_eggs(self, requires):
         """Resolve pre-setup requirements"""
-        resolved_dists = pkg_resources.working_set.resolve(
-            _reqs.parse(requires),
-            installer=self.fetch_build_egg,
-            replace_conflicting=True,
-        )
-        for dist in resolved_dists:
-            pkg_resources.working_set.add(dist, replace=True)
-        return resolved_dists
+        from setuptools.installer import _fetch_build_eggs
+
+        return _fetch_build_eggs(self, requires)
 
     def finalize_options(self):
         """
index b7096df14b4a15980ad138a3990d3e25aeb3bfe1..e9a7567abccc46e60cb66ebc1403a6b18a00d1a7 100644 (file)
@@ -6,9 +6,10 @@ import tempfile
 import warnings
 from distutils import log
 from distutils.errors import DistutilsError
+from functools import partial
 
-import pkg_resources
-from setuptools.wheel import Wheel
+from . import _reqs
+from .wheel import Wheel
 from ._deprecation_warning import SetuptoolsDeprecationWarning
 
 
@@ -20,20 +21,34 @@ def _fixup_find_links(find_links):
     return find_links
 
 
-def fetch_build_egg(dist, req):  # noqa: C901  # is too complex (16)  # FIXME
+def fetch_build_egg(dist, req):
     """Fetch an egg needed for building.
 
     Use pip/wheel to fetch/build a wheel."""
-    warnings.warn(
-        "setuptools.installer is deprecated. Requirements should "
-        "be satisfied by a PEP 517 installer.",
-        SetuptoolsDeprecationWarning,
+    _DeprecatedInstaller.warn(stacklevel=2)
+    _warn_wheel_not_available(dist)
+    return _fetch_build_egg_no_warn(dist, req)
+
+
+def _fetch_build_eggs(dist, requires):
+    import pkg_resources  # Delay import to avoid unnecessary side-effects
+
+    _DeprecatedInstaller.warn(stacklevel=3)
+    _warn_wheel_not_available(dist)
+
+    resolved_dists = pkg_resources.working_set.resolve(
+        _reqs.parse(requires, pkg_resources.Requirement),  # required for compatibility
+        installer=partial(_fetch_build_egg_no_warn, dist),  # avoid warning twice
+        replace_conflicting=True,
     )
-    # Warn if wheel is not available
-    try:
-        pkg_resources.get_distribution('wheel')
-    except pkg_resources.DistributionNotFound:
-        dist.announce('WARNING: The wheel package is not available.', log.WARN)
+    for dist in resolved_dists:
+        pkg_resources.working_set.add(dist, replace=True)
+    return resolved_dists
+
+
+def _fetch_build_egg_no_warn(dist, req):  # noqa: C901  # is too complex (16)  # FIXME
+    import pkg_resources  # Delay import to avoid unnecessary side-effects
+
     # Ignore environment markers; if supplied, it is required.
     req = strip_marker(req)
     # Take easy_install options into account, but do not override relevant
@@ -98,7 +113,30 @@ def strip_marker(req):
     calling pip with something like `babel; extra == "i18n"`, which
     would always be ignored.
     """
+    import pkg_resources  # Delay import to avoid unnecessary side-effects
+
     # create a copy to avoid mutating the input
     req = pkg_resources.Requirement.parse(str(req))
     req.marker = None
     return req
+
+
+def _warn_wheel_not_available(dist):
+    import pkg_resources  # Delay import to avoid unnecessary side-effects
+
+    try:
+        pkg_resources.get_distribution('wheel')
+    except pkg_resources.DistributionNotFound:
+        dist.announce('WARNING: The wheel package is not available.', log.WARN)
+
+
+class _DeprecatedInstaller(SetuptoolsDeprecationWarning):
+    @classmethod
+    def warn(cls, stacklevel=1):
+        warnings.warn(
+            "setuptools.installer and fetch_build_eggs are deprecated. "
+            "Requirements should be satisfied by a PEP 517 installer. "
+            "If you are using pip, you can try `pip install --use-pep517`.",
+            cls,
+            stacklevel=stacklevel+1
+        )
index 4406eda5fd7887fe0aeb3a229d0967a5d2ef9404..ac574c0eb851daf4a735125b4deca70f2f8778f5 100644 (file)
@@ -21,6 +21,7 @@ from . import contexts, namespaces
 
 from setuptools._importlib import resources as importlib_resources
 from setuptools.command.editable_wheel import (
+    _DebuggingTips,
     _LinkTree,
     _find_virtual_namespaces,
     _find_namespaces,
@@ -955,6 +956,24 @@ class TestCustomBuildExt:
         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 debugging"
+    with pytest.raises(SimulatedErr), pytest.warns(_DebuggingTips, match=expected_msg):
+        cmd.run()
+
+
 def install_project(name, venv, tmp_path, files, *opts):
     project = tmp_path / name
     project.mkdir()
index 95e1869658566aac3060562d8cd5a6b647887d1e..75b2a14959a5085297ab1aa47626bbb5ec08526d 100644 (file)
@@ -1,6 +1,6 @@
-import pkg_resources
+from ._importlib import metadata
 
 try:
-    __version__ = pkg_resources.get_distribution('setuptools').version
+    __version__ = metadata.version('setuptools')
 except Exception:
     __version__ = 'unknown'
index 527ed3b23306a3822388520115bafaf3eabb5024..e388083ba8efaee7e433daed9bf302aeaec888e5 100644 (file)
@@ -10,12 +10,11 @@ import contextlib
 
 from distutils.util import get_platform
 
-import pkg_resources
 import setuptools
-from pkg_resources import parse_version
+from setuptools.extern.packaging.version import Version as parse_version
 from setuptools.extern.packaging.tags import sys_tags
 from setuptools.extern.packaging.utils import canonicalize_name
-from setuptools.command.egg_info import write_requirements
+from setuptools.command.egg_info import write_requirements, _egg_basename
 from setuptools.archive_util import _unpack_zipfile_obj
 
 
@@ -89,10 +88,11 @@ class Wheel:
         return next((True for t in self.tags() if t in supported_tags), False)
 
     def egg_name(self):
-        return pkg_resources.Distribution(
-            project_name=self.project_name, version=self.version,
+        return _egg_basename(
+            self.project_name,
+            self.version,
             platform=(None if self.platform == 'any' else get_platform()),
-        ).egg_name() + '.egg'
+        ) + ".egg"
 
     def get_dist_info(self, zf):
         # find the correct name of the .dist-info dir in the wheel file
@@ -121,6 +121,8 @@ class Wheel:
 
     @staticmethod
     def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):
+        import pkg_resources
+
         def get_metadata(name):
             with zf.open(posixpath.join(dist_info, name)) as fp:
                 value = fp.read().decode('utf-8')
diff --git a/tox.ini b/tox.ini
index fd7ef681dbbced16086d9acd1f6ccf20b7a4a37d..2bae6728f6b2d63692e767a8ddbce85336481558 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -8,10 +8,13 @@ toxworkdir={env:TOX_WORK_DIR:.tox}
 [testenv]
 deps =
        # Ideally all the dependencies should be set as "extras"
+setenv =
+       PYTHONWARNDEFAULTENCODING = 1
 commands =
        pytest {posargs}
 usedevelop = True
-extras = testing
+extras =
+       testing
 passenv =
        SETUPTOOLS_USE_DISTUTILS
        PRE_BUILT_SETUPTOOLS_WHEEL