From: JinWang An Date: Mon, 27 Mar 2023 08:03:00 +0000 (+0900) Subject: Imported Upstream version 67.3.0 X-Git-Tag: upstream/67.3.0^0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=2b2b70a6cde00e522b4350837e7e76b21916793e;p=platform%2Fupstream%2Fpython-setuptools.git Imported Upstream version 67.3.0 --- diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 126aa15..2caed86 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.2.0 +current_version = 67.3.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 428b833..8c289f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ------- diff --git a/docs/references/keywords.rst b/docs/references/keywords.rst index ade147a..6173e3c 100644 --- a/docs/references/keywords.rst +++ b/docs/references/keywords.rst @@ -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 ` for more information. diff --git a/docs/userguide/development_mode.rst b/docs/userguide/development_mode.rst index 6f9f541..12d50fb 100644 --- a/docs/userguide/development_mode.rst +++ b/docs/userguide/development_mode.rst @@ -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 `_ 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 diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 0ae951b..aed691a 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -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: diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 107dda7..2138f95 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -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 diff --git a/pytest.ini b/pytest.ini index 12007ad..1a651f5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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 diff --git a/setup.cfg b/setup.cfg index 41ca2f7..129a935 100644 --- 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 index 0000000..8ba7c80 --- /dev/null +++ b/setuptools/_normalization.py @@ -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)) diff --git a/setuptools/_path.py b/setuptools/_path.py index 3767523..b99d9da 100644 --- a/setuptools/_path.py +++ b/setuptools/_path.py @@ -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))) diff --git a/setuptools/_reqs.py b/setuptools/_reqs.py index ca72417..5d5b927 100644 --- a/setuptools/_reqs.py +++ b/setuptools/_reqs.py @@ -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)) diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 11a1c6b..33f483c 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -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') diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 24fb0a7..5630ca4 100644 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -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 diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index 0685c94..40fdfd0 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -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) diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index d60cfbe..6fddf03 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -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) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 86e99dd..afcde5a 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -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.""" diff --git a/setuptools/command/install_egg_info.py b/setuptools/command/install_egg_info.py index 65ede40..1c549c9 100644 --- a/setuptools/command/install_egg_info.py +++ b/setuptools/command/install_egg_info.py @@ -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 = [] diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py index aeb0e42..8b3133f 100644 --- a/setuptools/command/install_scripts.py +++ b/setuptools/command/install_scripts.py @@ -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( diff --git a/setuptools/dist.py b/setuptools/dist.py index cd34d74..eb59f3a 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -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): """ diff --git a/setuptools/installer.py b/setuptools/installer.py index b7096df..e9a7567 100644 --- a/setuptools/installer.py +++ b/setuptools/installer.py @@ -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 + ) diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 4406eda..ac574c0 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -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() diff --git a/setuptools/version.py b/setuptools/version.py index 95e1869..75b2a14 100644 --- a/setuptools/version.py +++ b/setuptools/version.py @@ -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' diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 527ed3b..e388083 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -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 fd7ef68..2bae672 100644 --- 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