[bumpversion]
-current_version = 67.2.0
+current_version = 67.3.0
commit = True
tag = True
+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
-------
``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.
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
<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
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:
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"]
(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
error
## upstream
+
# Ensure ResourceWarnings are emitted
default::ResourceWarning
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
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.
# 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
[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
--- /dev/null
+"""
+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))
import os
+import sys
from typing import Union
_Path = Union[str, os.PathLike]
>>> 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)))
+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`.
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))
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
('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',
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')
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
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'"
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"
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
"""
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"
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")
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)
import logging
import os
-import re
import shutil
import sys
import traceback
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
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"),
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:
))
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)
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()
)
>>> _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)
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
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)
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
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.
@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):
"""
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 = ''
# 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" %
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:
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
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."""
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):
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 = []
import os
import sys
-from pkg_resources import Distribution, PathMetadata
from .._path import ensure_directory
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
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 glob import iglob
import itertools
import textwrap
+from contextlib import suppress
from typing import List, Optional, Set, TYPE_CHECKING
from pathlib import Path
from ._importlib import metadata
-from . import SetuptoolsDeprecationWarning
+from . import SetuptoolsDeprecationWarning, _normalization
import setuptools
import setuptools.command
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
)
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)
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:
#
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")
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):
"""
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
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
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 setuptools._importlib import resources as importlib_resources
from setuptools.command.editable_wheel import (
+ _DebuggingTips,
_LinkTree,
_find_virtual_namespaces,
_find_namespaces,
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()
-import pkg_resources
+from ._importlib import metadata
try:
- __version__ = pkg_resources.get_distribution('setuptools').version
+ __version__ = metadata.version('setuptools')
except Exception:
__version__ = 'unknown'
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
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
@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')
[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