From 6050634818943befefe3a85a12503b6d8a1e8106 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 20 Jan 2023 14:57:03 +0000 Subject: [PATCH 01/17] Replace pkg_resources in version.py --- setuptools/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/version.py b/setuptools/version.py index 95e1869658..75b2a14959 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' From ec238c4af3bdd91f161cf594fa1aa53f967a9d0a Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:38:39 +0000 Subject: [PATCH 02/17] Extract normalization functions from editable_wheel and dist_info into own module --- setuptools/_normalization.py | 99 ++++++++++++++++++++++++++++ setuptools/command/dist_info.py | 40 ++--------- setuptools/command/editable_wheel.py | 36 +++------- 3 files changed, 113 insertions(+), 62 deletions(-) create mode 100644 setuptools/_normalization.py diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py new file mode 100644 index 0000000000..d6343ca5c2 --- /dev/null +++ b/setuptools/_normalization.py @@ -0,0 +1,99 @@ +import os +import re +import sys +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 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 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' + """ + 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).strip("_") diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index 0685c94596..d5344471e0 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -4,18 +4,16 @@ """ 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): @@ -72,8 +70,8 @@ def finalize_options(self): egg_info.finalize_options() self.egg_info = egg_info - name = _safe(dist.get_name()) - version = _version(dist.get_version()) + name = _normalization.safe_name(dist.get_name()).replace(".", "_") + version = _normalization.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 +103,6 @@ def run(self): 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 d60cfbebb7..1875641f70 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -12,7 +12,6 @@ import logging import os -import re import shutil import sys import traceback @@ -36,10 +35,10 @@ 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, errors, namespaces, _normalization +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 @@ -490,7 +489,7 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str] )) 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 +568,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) + _normalization.path(Path(parent, *key.split('.'))) == _normalization.path(value) for key, value in layout.items() ) @@ -698,21 +697,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 = _normalization.path(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 == _normalization.path(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 +712,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 From 3c48e7c10655b79071f02b3559e6585e6c6b0d66 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:43:58 +0000 Subject: [PATCH 03/17] Prefer setuptools._normalization instead of pkg_resources in develop.py --- setuptools/command/develop.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 24fb0a7c81..5f9690f6fe 100644 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -5,9 +5,9 @@ import glob import io -import pkg_resources from setuptools.command.easy_install import easy_install from setuptools import namespaces +from setuptools import _normalization import setuptools @@ -42,6 +42,8 @@ def initialize_options(self): 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,8 +63,8 @@ def finalize_options(self): 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( + target = _normalization.path(self.egg_base) + egg_path = _normalization.path( os.path.join(self.install_dir, self.egg_path) ) if egg_path != target: @@ -94,15 +96,15 @@ def _resolve_setup_path(egg_base, install_dir, egg_path): 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 = _normalization.path( os.path.join(install_dir, egg_path, path_to_setup) ) - if resolved != pkg_resources.normalize_path(os.curdir): + if resolved != _normalization.path(os.curdir): raise DistutilsOptionError( "Can't get a consistent path to setup script from" " installation directory", resolved, - pkg_resources.normalize_path(os.curdir), + _normalization.path(os.curdir), ) return path_to_setup From daadc5af94d97baa4544e51a183ee0a5d83d00fe Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:48:13 +0000 Subject: [PATCH 04/17] Prefer setuptools._normalization and importlib_metadata instead of pkg_resources in dist.py --- setuptools/dist.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index cd34d74a9c..735d1b9fc4 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -17,6 +17,7 @@ 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 ._importlib import metadata -from . import SetuptoolsDeprecationWarning +from . import SetuptoolsDeprecationWarning, _normalization import setuptools import setuptools.command @@ -453,11 +454,12 @@ def patch_missing_pkg_info(self, attrs): # 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") From 61ad58b1620e4ca373863114c2e3bebfefcb1b0b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:51:30 +0000 Subject: [PATCH 05/17] Prefer setuptools._normalization instead of pkg_resources in egg_info.py --- setuptools/command/egg_info.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 86e99dd207..32b5e81f8e 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -18,7 +18,7 @@ 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 @@ -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 @@ def _already_tagged(self, version: str) -> bool: 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 = '' @@ -233,7 +234,7 @@ def finalize_options(self): 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 = self.egg_name.replace("-", "_") + '.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: From f8f56bceb9491f7fe56e2b8dcbb39a0088229dc8 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:52:41 +0000 Subject: [PATCH 06/17] Prefer packaging instead of pkg_resources in egg_info.py --- setuptools/command/egg_info.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 32b5e81f8e..e028df0d9e 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -25,9 +25,6 @@ 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 @@ -217,12 +214,12 @@ def finalize_options(self): # 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" % @@ -252,7 +249,7 @@ def finalize_options(self): 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 write_or_delete_file(self, what, filename, data, force=False): From 6f93ec71d5909195eb73b88fca52a546eafec78f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:54:25 +0000 Subject: [PATCH 07/17] Prefer packaging instead of pkg_resources in dist.py for markers --- setuptools/dist.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 735d1b9fc4..f650485512 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -300,11 +300,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: From ea6df151a0e32013dc4a82bc45ab9355842146fa Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 22:58:07 +0000 Subject: [PATCH 08/17] Implement alternative for pkg_resources.Distribution.egg_name in egg_info.py --- setuptools/command/egg_info.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index e028df0d9e..2314b448bb 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -33,6 +33,9 @@ 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. @@ -231,7 +234,7 @@ def finalize_options(self): self.egg_base = (dirs or {}).get('', os.curdir) self.ensure_dirname('egg_base') - self.egg_info = self.egg_name.replace("-", "_") + '.egg-info' + self.egg_info = _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: @@ -252,6 +255,10 @@ def finalize_options(self): 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 @@ -769,5 +776,19 @@ def get_pkg_info_revision(): return 0 +def _egg_basename(egg_name, egg_version, py_version=PY_MAJOR, platform=None): + """Compute filename of the output egg. Private API.""" + name = _filename_component(egg_name) + version = _filename_component(egg_version) + egg = f"{name}-{version}-py{py_version}" + if platform: + egg += f"-{platform}" + return egg + + +def _filename_component(value): + return value.replace("-", "_") + + class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning): """Deprecated behavior warning for EggInfo, bypassing suppression.""" From cbd0cb7d36689598c8cd69f06411b2785065f914 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 23:01:08 +0000 Subject: [PATCH 09/17] Prefer alternative from egg_info.py to pkg_resources.Distribution.egg_name --- setuptools/command/bdist_egg.py | 15 +++++++-------- setuptools/command/install_egg_info.py | 5 +---- setuptools/wheel.py | 9 +++++---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 11a1c6be28..33f483cf50 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -11,7 +11,6 @@ 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 @@ def finalize_options(self): 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/install_egg_info.py b/setuptools/command/install_egg_info.py index 65ede406bf..1c549c98ea 100644 --- a/setuptools/command/install_egg_info.py +++ b/setuptools/command/install_egg_info.py @@ -5,7 +5,6 @@ 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 @@ def finalize_options(self): 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/wheel.py b/setuptools/wheel.py index 527ed3b233..53ce27e1d9 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -15,7 +15,7 @@ from pkg_resources import 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 +89,11 @@ def is_compatible(self): 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 From 1cfd18b46c007900a1cff6a41d4546617aeead28 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 23:19:20 +0000 Subject: [PATCH 10/17] Prefer packaging instead for pkg_resources for version in wheel.py --- setuptools/wheel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 53ce27e1d9..e388083ba8 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -10,9 +10,8 @@ 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, _egg_basename @@ -122,6 +121,8 @@ def _install_as_egg(self, destination_eggdir, zf): @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') From dd7b8fbccaefc5d8cb0ec8bfc3fe57f5695617f3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 23 Jan 2023 23:23:27 +0000 Subject: [PATCH 11/17] Modify _reqs.py to prefer prefer packaging instead of pkg_resources --- setuptools/_reqs.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/setuptools/_reqs.py b/setuptools/_reqs.py index ca7241746b..5d5b927fd8 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)) From e5681c0e0d1292921a84dac61ba56e3cda8d857f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 24 Jan 2023 00:03:03 +0000 Subject: [PATCH 12/17] Centralize usage of pkg_resources from dist.py to installer.py --- pytest.ini | 2 +- setuptools/dist.py | 12 ++------ setuptools/installer.py | 62 +++++++++++++++++++++++++++++++++-------- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/pytest.ini b/pytest.ini index 12007aded6..9424333003 100644 --- a/pytest.ini +++ b/pytest.ini @@ -40,7 +40,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. diff --git a/setuptools/dist.py b/setuptools/dist.py index f650485512..49d1ac6b92 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -42,7 +42,6 @@ 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 @@ -888,14 +887,9 @@ def parse_config_files(self, filenames=None, ignore_option_errors=False): 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 b7096df14b..e9a7567abc 100644 --- a/setuptools/installer.py +++ b/setuptools/installer.py @@ -6,9 +6,10 @@ 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 + ) From 99b7b6450f4a71eed229607ccc1b8d567b59bf02 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 24 Jan 2023 00:20:59 +0000 Subject: [PATCH 13/17] Delay imports of pkg_resources in install_scripts.py --- setuptools/command/install_scripts.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py index aeb0e4240c..8b3133f1fd 100644 --- a/setuptools/command/install_scripts.py +++ b/setuptools/command/install_scripts.py @@ -4,7 +4,6 @@ import os import sys -from pkg_resources import Distribution, PathMetadata from .._path import ensure_directory @@ -16,8 +15,6 @@ def initialize_options(self): 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 @@ def run(self): 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( From 5e7b76f786e8a8fbfc3bfa4755033a8d9c2e06d8 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 24 Jan 2023 00:49:17 +0000 Subject: [PATCH 14/17] Replace/move _normalization.path with/to _path.samepath and _path.normpath --- setuptools/_normalization.py | 9 --------- setuptools/_path.py | 10 +++++++++- setuptools/command/develop.py | 22 ++++++++++------------ setuptools/command/editable_wheel.py | 15 +++++++++++---- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index d6343ca5c2..7012ffeaed 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -1,6 +1,4 @@ -import os import re -import sys import warnings from inspect import cleandoc from pathlib import Path @@ -17,13 +15,6 @@ _UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9.]+", re.I) -def 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 safe_identifier(name: str) -> str: """Make a string safe to be used as Python identifier. >>> safe_identifier("12abc") diff --git a/setuptools/_path.py b/setuptools/_path.py index 3767523b78..b99d9dadcf 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/command/develop.py b/setuptools/command/develop.py index 5f9690f6fe..08ae7f0df3 100644 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -6,8 +6,8 @@ import io from setuptools.command.easy_install import easy_install +from setuptools import _path from setuptools import namespaces -from setuptools import _normalization import setuptools @@ -63,20 +63,17 @@ def finalize_options(self): if self.egg_path is None: self.egg_path = os.path.abspath(ei.egg_base) - target = _normalization.path(self.egg_base) - egg_path = _normalization.path( - os.path.join(self.install_dir, self.egg_path) - ) - if egg_path != target: + egg_path = os.path.join(self.install_dir, self.egg_path) + if not _path.same_path(egg_path, self.egg_base): raise DistutilsOptionError( "--egg-path must be a relative path from the install" - " directory to " + target + f" directory to {self.egg_base}" ) # Make a distribution for the package's source self.dist = pkg_resources.Distribution( - target, - pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)), + self.egg_base, + pkg_resources.PathMetadata(self.egg_base, os.path.abspath(ei.egg_info)), project_name=ei.egg_name, ) @@ -96,15 +93,16 @@ def _resolve_setup_path(egg_base, install_dir, egg_path): path_to_setup = egg_base.replace(os.sep, '/').rstrip('/') if path_to_setup != os.curdir: path_to_setup = '../' * (path_to_setup.count('/') + 1) - resolved = _normalization.path( + resolved = _path.normpath( os.path.join(install_dir, egg_path, path_to_setup) ) - if resolved != _normalization.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, - _normalization.path(os.curdir), + curdir, ) return path_to_setup diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index 1875641f70..a3c7bd79e7 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -35,7 +35,14 @@ Union, ) -from .. import Command, SetuptoolsDeprecationWarning, errors, namespaces, _normalization +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 @@ -568,7 +575,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( - _normalization.path(Path(parent, *key.split('.'))) == _normalization.path(value) + _path.same_path(Path(parent, *key.split('.')), value) for key, value in layout.items() ) @@ -697,11 +704,11 @@ 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 = _normalization.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 == _normalization.path(Path(parent_path, *rest)) + and norm_pkg_path == _path.normpath(Path(parent_path, *rest)) ) From 33f3243c8256742112bec48d1f34c61ddbfb95be Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 24 Jan 2023 10:35:45 +0000 Subject: [PATCH 15/17] Fix dist-info naming discrepancy in relation to bdist_wheel --- setuptools/_normalization.py | 29 ++++++++++++++++++++++++++++- setuptools/command/dist_info.py | 4 ++-- setuptools/command/egg_info.py | 10 +++------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index 7012ffeaed..8ba7c802fa 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -1,3 +1,7 @@ +""" +Helpers for normalization as expected in wheel/sdist/module file names +and core metadata +""" import re import warnings from inspect import cleandoc @@ -71,6 +75,7 @@ def best_effort_version(version: str) -> str: >>> best_effort_version("ubuntu lts") 'ubuntu.lts' """ + # See pkg_resources.safe_version try: return safe_version(version) except packaging.version.InvalidVersion: @@ -87,4 +92,26 @@ def best_effort_version(version: str) -> str: """ warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning) v = version.replace(' ', '.') - return safe_name(v).strip("_") + 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/command/dist_info.py b/setuptools/command/dist_info.py index d5344471e0..71acdc1833 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -70,8 +70,8 @@ def finalize_options(self): egg_info.finalize_options() self.egg_info = egg_info - name = _normalization.safe_name(dist.get_name()).replace(".", "_") - version = _normalization.best_effort_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") diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 2314b448bb..30ecf1dd17 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -234,7 +234,7 @@ def finalize_options(self): self.egg_base = (dirs or {}).get('', os.curdir) self.ensure_dirname('egg_base') - self.egg_info = _filename_component(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: @@ -778,17 +778,13 @@ def get_pkg_info_revision(): def _egg_basename(egg_name, egg_version, py_version=PY_MAJOR, platform=None): """Compute filename of the output egg. Private API.""" - name = _filename_component(egg_name) - version = _filename_component(egg_version) + name = _normalization.filename_component(egg_name) + version = _normalization.filename_component(egg_version) egg = f"{name}-{version}-py{py_version}" if platform: egg += f"-{platform}" return egg -def _filename_component(value): - return value.replace("-", "_") - - class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning): """Deprecated behavior warning for EggInfo, bypassing suppression.""" From 3736cbc2bdaef3fa4dfbdf59dac69be42bf8aa15 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 24 Jan 2023 10:52:53 +0000 Subject: [PATCH 16/17] Revert to using normalized paths in develop to fix test errors --- setuptools/command/develop.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 08ae7f0df3..5630ca4cdc 100644 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -63,17 +63,18 @@ def finalize_options(self): if self.egg_path is None: self.egg_path = os.path.abspath(ei.egg_base) - egg_path = os.path.join(self.install_dir, self.egg_path) - if not _path.same_path(egg_path, self.egg_base): + 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" - f" directory to {self.egg_base}" + " directory to " + target ) # Make a distribution for the package's source self.dist = pkg_resources.Distribution( - self.egg_base, - pkg_resources.PathMetadata(self.egg_base, os.path.abspath(ei.egg_info)), + target, + pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)), project_name=ei.egg_name, ) From 51bf7563d1513ec3a2edbbcf00a533f85cf26d66 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 1 Feb 2023 14:02:34 +0000 Subject: [PATCH 17/17] Allow None to be passed to egg_basename --- setuptools/command/egg_info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 30ecf1dd17..afcde5a2a1 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -776,11 +776,11 @@ def get_pkg_info_revision(): return 0 -def _egg_basename(egg_name, egg_version, py_version=PY_MAJOR, platform=None): +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}" + egg = f"{name}-{version}-py{py_version or PY_MAJOR}" if platform: egg += f"-{platform}" return egg