summaryrefslogtreecommitdiffstats
path: root/contrib/python/setuptools
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-01-14 19:52:35 +0300
committerrobot-piglet <[email protected]>2026-01-14 20:21:53 +0300
commitd980d7a650f4a3cfe8d1b7f030847b03d5c511e4 (patch)
tree2abf6c93dd93742d418aed2930f57ee56b35d1bc /contrib/python/setuptools
parente24dde6b64a154b0296225e86e4b68bdf668b64c (diff)
Intermediate changes
commit_hash:df4e6068190137786e6f2f5bf3604e06432cba52
Diffstat (limited to 'contrib/python/setuptools')
-rw-r--r--contrib/python/setuptools/py3/.dist-info/METADATA3
-rw-r--r--contrib/python/setuptools/py3/pkg_resources/__init__.py13
-rw-r--r--contrib/python/setuptools/py3/setuptools/__init__.py38
-rw-r--r--contrib/python/setuptools/py3/setuptools/_discovery.py33
-rw-r--r--contrib/python/setuptools/py3/setuptools/_distutils/command/build_scripts.py2
-rw-r--r--contrib/python/setuptools/py3/setuptools/_entry_points.py4
-rw-r--r--contrib/python/setuptools/py3/setuptools/_normalization.py2
-rw-r--r--contrib/python/setuptools/py3/setuptools/_path.py15
-rw-r--r--contrib/python/setuptools/py3/setuptools/_reqs.py2
-rw-r--r--contrib/python/setuptools/py3/setuptools/_scripts.py361
-rw-r--r--contrib/python/setuptools/py3/setuptools/_shutil.py6
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/bdist_egg.py20
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/bdist_wheel.py2
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/build_ext.py57
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/develop.py218
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/easy_install.py2369
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/editable_wheel.py45
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/egg_info.py2
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/install.py58
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/install_scripts.py16
-rw-r--r--contrib/python/setuptools/py3/setuptools/command/sdist.py2
-rw-r--r--contrib/python/setuptools/py3/setuptools/compat/py310.py11
-rw-r--r--contrib/python/setuptools/py3/setuptools/dist.py22
-rw-r--r--contrib/python/setuptools/py3/setuptools/installer.py63
-rw-r--r--contrib/python/setuptools/py3/setuptools/package_index.py1179
-rw-r--r--contrib/python/setuptools/py3/setuptools/sandbox.py536
-rw-r--r--contrib/python/setuptools/py3/setuptools/wheel.py73
-rw-r--r--contrib/python/setuptools/py3/ya.make6
28 files changed, 651 insertions, 4507 deletions
diff --git a/contrib/python/setuptools/py3/.dist-info/METADATA b/contrib/python/setuptools/py3/.dist-info/METADATA
index 8d42d121a54..f125370964e 100644
--- a/contrib/python/setuptools/py3/.dist-info/METADATA
+++ b/contrib/python/setuptools/py3/.dist-info/METADATA
@@ -1,8 +1,9 @@
Metadata-Version: 2.4
Name: setuptools
-Version: 79.0.1
+Version: 80.9.0
Summary: Easily download, build, install, upgrade, and uninstall Python packages
Author-email: Python Packaging Authority <[email protected]>
+License-Expression: MIT
Project-URL: Source, https://github.com/pypa/setuptools
Project-URL: Documentation, https://setuptools.pypa.io/
Project-URL: Changelog, https://setuptools.pypa.io/en/stable/history.html
diff --git a/contrib/python/setuptools/py3/pkg_resources/__init__.py b/contrib/python/setuptools/py3/pkg_resources/__init__.py
index b25c6c1f656..03cf8d3f8ae 100644
--- a/contrib/python/setuptools/py3/pkg_resources/__init__.py
+++ b/contrib/python/setuptools/py3/pkg_resources/__init__.py
@@ -97,8 +97,11 @@ if TYPE_CHECKING:
warnings.warn(
"pkg_resources is deprecated as an API. "
- "See https://setuptools.pypa.io/en/latest/pkg_resources.html",
- DeprecationWarning,
+ "See https://setuptools.pypa.io/en/latest/pkg_resources.html. "
+ "The pkg_resources package is slated for removal as early as "
+ "2025-11-30. Refrain from using this package or pin to "
+ "Setuptools<81.",
+ UserWarning,
stacklevel=2,
)
@@ -440,11 +443,7 @@ def _macos_arch(machine):
def get_build_platform():
- """Return this platform's string for platform-specific distributions
-
- XXX Currently this is the same as ``distutils.util.get_platform()``, but it
- needs some hacks for Linux and macOS.
- """
+ """Return this platform's string for platform-specific distributions"""
from sysconfig import get_platform
plat = get_platform()
diff --git a/contrib/python/setuptools/py3/setuptools/__init__.py b/contrib/python/setuptools/py3/setuptools/__init__.py
index 64464dfaa38..f1b9bfe9b8a 100644
--- a/contrib/python/setuptools/py3/setuptools/__init__.py
+++ b/contrib/python/setuptools/py3/setuptools/__init__.py
@@ -9,7 +9,6 @@ from __future__ import annotations
import functools
import os
-import re
import sys
from abc import abstractmethod
from collections.abc import Mapping
@@ -30,7 +29,6 @@ from .version import __version__ as __version__
from .warnings import SetuptoolsDeprecationWarning
import distutils.core
-from distutils.errors import DistutilsOptionError
__all__ = [
'setup',
@@ -175,42 +173,6 @@ class Command(_Command):
super().__init__(dist)
vars(self).update(kw)
- def _ensure_stringlike(self, option, what, default=None):
- val = getattr(self, option)
- if val is None:
- setattr(self, option, default)
- return default
- elif not isinstance(val, str):
- raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)")
- return val
-
- def ensure_string_list(self, option: str) -> None:
- r"""Ensure that 'option' is a list of strings. If 'option' is
- currently a string, we split it either on /,\s*/ or /\s+/, so
- "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
- ["foo", "bar", "baz"].
-
- ..
- TODO: This method seems to be similar to the one in ``distutils.cmd``
- Probably it is just here for backward compatibility with old Python versions?
-
- :meta private:
- """
- val = getattr(self, option)
- if val is None:
- return
- elif isinstance(val, str):
- setattr(self, option, re.split(r',\s*|\s+', val))
- else:
- if isinstance(val, list):
- ok = all(isinstance(v, str) for v in val)
- else:
- ok = False
- if not ok:
- raise DistutilsOptionError(
- f"'{option}' must be a list of strings (got {val!r})"
- )
-
@overload
def reinitialize_command(
self, command: str, reinit_subcommands: bool = False, **kw
diff --git a/contrib/python/setuptools/py3/setuptools/_discovery.py b/contrib/python/setuptools/py3/setuptools/_discovery.py
new file mode 100644
index 00000000000..d1b4a0ee035
--- /dev/null
+++ b/contrib/python/setuptools/py3/setuptools/_discovery.py
@@ -0,0 +1,33 @@
+import functools
+import operator
+
+import packaging.requirements
+
+
+# from coherent.build.discovery
+def extras_from_dep(dep):
+ try:
+ markers = packaging.requirements.Requirement(dep).marker._markers
+ except AttributeError:
+ markers = ()
+ return set(
+ marker[2].value
+ for marker in markers
+ if isinstance(marker, tuple) and marker[0].value == 'extra'
+ )
+
+
+def extras_from_deps(deps):
+ """
+ >>> extras_from_deps(['requests'])
+ set()
+ >>> extras_from_deps(['pytest; extra == "test"'])
+ {'test'}
+ >>> sorted(extras_from_deps([
+ ... 'requests',
+ ... 'pytest; extra == "test"',
+ ... 'pytest-cov; extra == "test"',
+ ... 'sphinx; extra=="doc"']))
+ ['doc', 'test']
+ """
+ return functools.reduce(operator.or_, map(extras_from_dep, deps), set())
diff --git a/contrib/python/setuptools/py3/setuptools/_distutils/command/build_scripts.py b/contrib/python/setuptools/py3/setuptools/_distutils/command/build_scripts.py
index 127c51d8dc2..b86ee6e6ba6 100644
--- a/contrib/python/setuptools/py3/setuptools/_distutils/command/build_scripts.py
+++ b/contrib/python/setuptools/py3/setuptools/_distutils/command/build_scripts.py
@@ -106,7 +106,7 @@ class build_scripts(Command):
log.info("copying and adjusting %s -> %s", script, self.build_dir)
if not self.dry_run:
post_interp = shebang_match.group(1) or ''
- shebang = f"#!python{post_interp}\n"
+ shebang = "#!" + self.executable + post_interp + "\n"
self._validate_shebang(shebang, f.encoding)
with open(outfile, "w", encoding=f.encoding) as outf:
outf.write(shebang)
diff --git a/contrib/python/setuptools/py3/setuptools/_entry_points.py b/contrib/python/setuptools/py3/setuptools/_entry_points.py
index e785fc7df8d..cd5dd2c8ac9 100644
--- a/contrib/python/setuptools/py3/setuptools/_entry_points.py
+++ b/contrib/python/setuptools/py3/setuptools/_entry_points.py
@@ -15,6 +15,10 @@ def ensure_valid(ep):
"""
Exercise one of the dynamic properties to trigger
the pattern match.
+
+ This function is deprecated in favor of importlib_metadata 8.7 and
+ Python 3.14 importlib.metadata, which validates entry points on
+ construction.
"""
try:
ep.extras
diff --git a/contrib/python/setuptools/py3/setuptools/_normalization.py b/contrib/python/setuptools/py3/setuptools/_normalization.py
index 0937a4faf8c..fb89323c9db 100644
--- a/contrib/python/setuptools/py3/setuptools/_normalization.py
+++ b/contrib/python/setuptools/py3/setuptools/_normalization.py
@@ -36,7 +36,6 @@ def safe_name(component: str) -> str:
>>> safe_name("hello_world")
'hello_world'
"""
- # See pkg_resources.safe_name
return _UNSAFE_NAME_CHARS.sub("-", component)
@@ -81,7 +80,6 @@ def best_effort_version(version: str) -> str:
>>> best_effort_version("42.+?1")
'42.dev0+sanitized.1'
"""
- # See pkg_resources._forgiving_version
try:
return safe_version(version)
except packaging.version.InvalidVersion:
diff --git a/contrib/python/setuptools/py3/setuptools/_path.py b/contrib/python/setuptools/py3/setuptools/_path.py
index 0d99b0f539f..2b78022934d 100644
--- a/contrib/python/setuptools/py3/setuptools/_path.py
+++ b/contrib/python/setuptools/py3/setuptools/_path.py
@@ -39,11 +39,20 @@ def same_path(p1: StrPath, p2: StrPath) -> bool:
return normpath(p1) == normpath(p2)
+def _cygwin_patch(filename: StrPath): # pragma: nocover
+ """
+ Contrary to POSIX 2008, on Cygwin, getcwd (3) contains
+ symlink components. Using
+ os.path.abspath() works around this limitation. A fix in os.getcwd()
+ would probably better, in Cygwin even more so, except
+ that this seems to be by design...
+ """
+ return os.path.abspath(filename) if sys.platform == 'cygwin' else filename
+
+
def normpath(filename: StrPath) -> 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)))
+ return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename))))
@contextlib.contextmanager
diff --git a/contrib/python/setuptools/py3/setuptools/_reqs.py b/contrib/python/setuptools/py3/setuptools/_reqs.py
index c793be4d6eb..7be56cbf35d 100644
--- a/contrib/python/setuptools/py3/setuptools/_reqs.py
+++ b/contrib/python/setuptools/py3/setuptools/_reqs.py
@@ -37,6 +37,6 @@ def parse(strs: _StrOrIter) -> Iterator[Requirement]: ...
def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> Iterator[_T]: ...
def parse(strs: _StrOrIter, parser: Callable[[str], _T] = parse_req) -> Iterator[_T]: # type: ignore[assignment]
"""
- Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``.
+ Parse requirements.
"""
return map(parser, parse_strings(strs))
diff --git a/contrib/python/setuptools/py3/setuptools/_scripts.py b/contrib/python/setuptools/py3/setuptools/_scripts.py
new file mode 100644
index 00000000000..88bf02f927b
--- /dev/null
+++ b/contrib/python/setuptools/py3/setuptools/_scripts.py
@@ -0,0 +1,361 @@
+from __future__ import annotations
+
+import os
+import re
+import shlex
+import shutil
+import struct
+import subprocess
+import sys
+import textwrap
+from collections.abc import Iterable
+from typing import TYPE_CHECKING, TypedDict
+
+from ._importlib import metadata, resources
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+from .warnings import SetuptoolsWarning
+
+from distutils.command.build_scripts import first_line_re
+from distutils.util import get_platform
+
+
+class _SplitArgs(TypedDict, total=False):
+ comments: bool
+ posix: bool
+
+
+class CommandSpec(list):
+ """
+ A command spec for a #! header, specified as a list of arguments akin to
+ those passed to Popen.
+ """
+
+ options: list[str] = []
+ split_args = _SplitArgs()
+
+ @classmethod
+ def best(cls):
+ """
+ Choose the best CommandSpec class based on environmental conditions.
+ """
+ return cls
+
+ @classmethod
+ def _sys_executable(cls):
+ _default = os.path.normpath(sys.executable)
+ return os.environ.get('__PYVENV_LAUNCHER__', _default)
+
+ @classmethod
+ def from_param(cls, param: Self | str | Iterable[str] | None) -> Self:
+ """
+ Construct a CommandSpec from a parameter to build_scripts, which may
+ be None.
+ """
+ if isinstance(param, cls):
+ return param
+ if isinstance(param, str):
+ return cls.from_string(param)
+ if isinstance(param, Iterable):
+ return cls(param)
+ if param is None:
+ return cls.from_environment()
+ raise TypeError(f"Argument has an unsupported type {type(param)}")
+
+ @classmethod
+ def from_environment(cls):
+ return cls([cls._sys_executable()])
+
+ @classmethod
+ def from_string(cls, string: str) -> Self:
+ """
+ Construct a command spec from a simple string representing a command
+ line parseable by shlex.split.
+ """
+ items = shlex.split(string, **cls.split_args)
+ return cls(items)
+
+ def install_options(self, script_text: str):
+ self.options = shlex.split(self._extract_options(script_text))
+ cmdline = subprocess.list2cmdline(self)
+ if not isascii(cmdline):
+ self.options[:0] = ['-x']
+
+ @staticmethod
+ def _extract_options(orig_script):
+ """
+ Extract any options from the first line of the script.
+ """
+ first = (orig_script + '\n').splitlines()[0]
+ match = _first_line_re().match(first)
+ options = match.group(1) or '' if match else ''
+ return options.strip()
+
+ def as_header(self):
+ return self._render(self + list(self.options))
+
+ @staticmethod
+ def _strip_quotes(item):
+ _QUOTES = '"\''
+ for q in _QUOTES:
+ if item.startswith(q) and item.endswith(q):
+ return item[1:-1]
+ return item
+
+ @staticmethod
+ def _render(items):
+ cmdline = subprocess.list2cmdline(
+ CommandSpec._strip_quotes(item.strip()) for item in items
+ )
+ return '#!' + cmdline + '\n'
+
+
+class WindowsCommandSpec(CommandSpec):
+ split_args = _SplitArgs(posix=False)
+
+
+class ScriptWriter:
+ """
+ Encapsulates behavior around writing entry point scripts for console and
+ gui apps.
+ """
+
+ template = textwrap.dedent(
+ r"""
+ # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
+ import re
+ import sys
+
+ # for compatibility with easy_install; see #2198
+ __requires__ = %(spec)r
+
+ try:
+ from importlib.metadata import distribution
+ except ImportError:
+ try:
+ from importlib_metadata import distribution
+ except ImportError:
+ from pkg_resources import load_entry_point
+
+
+ def importlib_load_entry_point(spec, group, name):
+ dist_name, _, _ = spec.partition('==')
+ matches = (
+ entry_point
+ for entry_point in distribution(dist_name).entry_points
+ if entry_point.group == group and entry_point.name == name
+ )
+ return next(matches).load()
+
+
+ globals().setdefault('load_entry_point', importlib_load_entry_point)
+
+
+ if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+ sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)())
+ """
+ ).lstrip()
+
+ command_spec_class = CommandSpec
+
+ @classmethod
+ def get_args(cls, dist, header=None):
+ """
+ Yield write_script() argument tuples for a distribution's
+ console_scripts and gui_scripts entry points.
+ """
+
+ # If distribution is not an importlib.metadata.Distribution, assume
+ # it's a pkg_resources.Distribution and transform it.
+ if not hasattr(dist, 'entry_points'):
+ SetuptoolsWarning.emit("Unsupported distribution encountered.")
+ dist = metadata.Distribution.at(dist.egg_info)
+
+ if header is None:
+ header = cls.get_header()
+ spec = f'{dist.name}=={dist.version}'
+ for type_ in 'console', 'gui':
+ group = f'{type_}_scripts'
+ for ep in dist.entry_points.select(group=group):
+ name = ep.name
+ cls._ensure_safe_name(ep.name)
+ script_text = cls.template % locals()
+ args = cls._get_script_args(type_, ep.name, header, script_text)
+ yield from args
+
+ @staticmethod
+ def _ensure_safe_name(name):
+ """
+ Prevent paths in *_scripts entry point names.
+ """
+ has_path_sep = re.search(r'[\\/]', name)
+ if has_path_sep:
+ raise ValueError("Path separators not allowed in script names")
+
+ @classmethod
+ def best(cls):
+ """
+ Select the best ScriptWriter for this environment.
+ """
+ if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):
+ return WindowsScriptWriter.best()
+ else:
+ return cls
+
+ @classmethod
+ def _get_script_args(cls, type_, name, header, script_text):
+ # Simply write the stub with no extension.
+ yield (name, header + script_text)
+
+ @classmethod
+ def get_header(
+ cls,
+ script_text: str = "",
+ executable: str | CommandSpec | Iterable[str] | None = None,
+ ) -> str:
+ """Create a #! line, getting options (if any) from script_text"""
+ cmd = cls.command_spec_class.best().from_param(executable)
+ cmd.install_options(script_text)
+ return cmd.as_header()
+
+
+class WindowsScriptWriter(ScriptWriter):
+ command_spec_class = WindowsCommandSpec
+
+ @classmethod
+ def best(cls):
+ """
+ Select the best ScriptWriter suitable for Windows
+ """
+ writer_lookup = dict(
+ executable=WindowsExecutableLauncherWriter,
+ natural=cls,
+ )
+ # for compatibility, use the executable launcher by default
+ launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable')
+ return writer_lookup[launcher]
+
+ @classmethod
+ def _get_script_args(cls, type_, name, header, script_text):
+ "For Windows, add a .py extension"
+ ext = dict(console='.pya', gui='.pyw')[type_]
+ if ext not in os.environ['PATHEXT'].lower().split(';'):
+ msg = (
+ "{ext} not listed in PATHEXT; scripts will not be "
+ "recognized as executables."
+ ).format(**locals())
+ SetuptoolsWarning.emit(msg)
+ old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
+ old.remove(ext)
+ header = cls._adjust_header(type_, header)
+ blockers = [name + x for x in old]
+ yield name + ext, header + script_text, 't', blockers
+
+ @classmethod
+ def _adjust_header(cls, type_, orig_header):
+ """
+ Make sure 'pythonw' is used for gui and 'python' is used for
+ console (regardless of what sys.executable is).
+ """
+ pattern = 'pythonw.exe'
+ repl = 'python.exe'
+ if type_ == 'gui':
+ pattern, repl = repl, pattern
+ pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)
+ new_header = pattern_ob.sub(string=orig_header, repl=repl)
+ return new_header if cls._use_header(new_header) else orig_header
+
+ @staticmethod
+ def _use_header(new_header):
+ """
+ Should _adjust_header use the replaced header?
+
+ On non-windows systems, always use. On
+ Windows systems, only use the replaced header if it resolves
+ to an executable on the system.
+ """
+ clean_header = new_header[2:-1].strip('"')
+ return sys.platform != 'win32' or shutil.which(clean_header)
+
+
+class WindowsExecutableLauncherWriter(WindowsScriptWriter):
+ @classmethod
+ def _get_script_args(cls, type_, name, header, script_text):
+ """
+ For Windows, add a .py extension and an .exe launcher
+ """
+ if type_ == 'gui':
+ launcher_type = 'gui'
+ ext = '-script.pyw'
+ old = ['.pyw']
+ else:
+ launcher_type = 'cli'
+ ext = '-script.py'
+ old = ['.py', '.pyc', '.pyo']
+ hdr = cls._adjust_header(type_, header)
+ blockers = [name + x for x in old]
+ yield (name + ext, hdr + script_text, 't', blockers)
+ yield (
+ name + '.exe',
+ get_win_launcher(launcher_type),
+ 'b', # write in binary mode
+ )
+ if not is_64bit():
+ # install a manifest for the launcher to prevent Windows
+ # from detecting it as an installer (which it will for
+ # launchers like easy_install.exe). Consider only
+ # adding a manifest for launchers detected as installers.
+ # See Distribute #143 for details.
+ m_name = name + '.exe.manifest'
+ yield (m_name, load_launcher_manifest(name), 't')
+
+
+def get_win_launcher(type):
+ """
+ Load the Windows launcher (executable) suitable for launching a script.
+
+ `type` should be either 'cli' or 'gui'
+
+ Returns the executable as a byte string.
+ """
+ launcher_fn = f'{type}.exe'
+ if is_64bit():
+ if get_platform() == "win-arm64":
+ launcher_fn = launcher_fn.replace(".", "-arm64.")
+ else:
+ launcher_fn = launcher_fn.replace(".", "-64.")
+ else:
+ launcher_fn = launcher_fn.replace(".", "-32.")
+ return resources.files('setuptools').joinpath(launcher_fn).read_bytes()
+
+
+def load_launcher_manifest(name):
+ res = resources.files(__name__).joinpath('launcher manifest.xml')
+ return res.read_text(encoding='utf-8') % vars()
+
+
+def _first_line_re():
+ """
+ Return a regular expression based on first_line_re suitable for matching
+ strings.
+ """
+ if isinstance(first_line_re.pattern, str):
+ return first_line_re
+
+ # first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern.
+ return re.compile(first_line_re.pattern.decode())
+
+
+def is_64bit():
+ return struct.calcsize("P") == 8
+
+
+def isascii(s):
+ try:
+ s.encode('ascii')
+ except UnicodeError:
+ return False
+ return True
diff --git a/contrib/python/setuptools/py3/setuptools/_shutil.py b/contrib/python/setuptools/py3/setuptools/_shutil.py
index 6acbb4281fc..660459a1102 100644
--- a/contrib/python/setuptools/py3/setuptools/_shutil.py
+++ b/contrib/python/setuptools/py3/setuptools/_shutil.py
@@ -51,3 +51,9 @@ def rmtree(path, ignore_errors=False, onexc=_auto_chmod):
def rmdir(path, **opts):
if os.path.isdir(path):
rmtree(path, **opts)
+
+
+def current_umask():
+ tmp = os.umask(0o022)
+ os.umask(tmp)
+ return tmp
diff --git a/contrib/python/setuptools/py3/setuptools/command/bdist_egg.py b/contrib/python/setuptools/py3/setuptools/command/bdist_egg.py
index 7f66c3ba6a7..b66020c8634 100644
--- a/contrib/python/setuptools/py3/setuptools/command/bdist_egg.py
+++ b/contrib/python/setuptools/py3/setuptools/command/bdist_egg.py
@@ -9,7 +9,7 @@ import os
import re
import sys
import textwrap
-from sysconfig import get_path, get_python_version
+from sysconfig import get_path, get_platform, get_python_version
from types import CodeType
from typing import TYPE_CHECKING, Literal
@@ -55,12 +55,12 @@ def write_stub(resource, pyfile) -> None:
"""
def __bootstrap__():
global __bootstrap__, __loader__, __file__
- import sys, pkg_resources, importlib.util
- __file__ = pkg_resources.resource_filename(__name__, %r)
- __loader__ = None; del __bootstrap__, __loader__
- spec = importlib.util.spec_from_file_location(__name__,__file__)
- mod = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(mod)
+ import sys, importlib.resources as irs, importlib.util
+ with irs.as_file(irs.files(__name__).joinpath(%r)) as __file__:
+ __loader__ = None; del __bootstrap__, __loader__
+ spec = importlib.util.spec_from_file_location(__name__,__file__)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
__bootstrap__()
"""
).lstrip()
@@ -77,7 +77,7 @@ class bdist_egg(Command):
'plat-name=',
'p',
"platform name to embed in generated filenames "
- "(by default uses `pkg_resources.get_build_platform()`)",
+ "(by default uses `sysconfig.get_platform()`)",
),
('exclude-source-files', None, "remove all .py files from the generated egg"),
(
@@ -110,9 +110,7 @@ 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.plat_name = get_platform()
self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
diff --git a/contrib/python/setuptools/py3/setuptools/command/bdist_wheel.py b/contrib/python/setuptools/py3/setuptools/command/bdist_wheel.py
index 1e3f637bcc3..91ed00170e9 100644
--- a/contrib/python/setuptools/py3/setuptools/command/bdist_wheel.py
+++ b/contrib/python/setuptools/py3/setuptools/command/bdist_wheel.py
@@ -285,7 +285,7 @@ class bdist_wheel(Command):
raise ValueError(
f"`py_limited_api={self.py_limited_api!r}` not supported. "
"`Py_LIMITED_API` is currently incompatible with "
- "`Py_GIL_DISABLED`."
+ "`Py_GIL_DISABLED`. "
"See https://github.com/python/cpython/issues/111506."
)
diff --git a/contrib/python/setuptools/py3/setuptools/command/build_ext.py b/contrib/python/setuptools/py3/setuptools/command/build_ext.py
index be833a379c7..af73fff7a58 100644
--- a/contrib/python/setuptools/py3/setuptools/command/build_ext.py
+++ b/contrib/python/setuptools/py3/setuptools/command/build_ext.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import itertools
import os
import sys
+import textwrap
from collections.abc import Iterator
from importlib.machinery import EXTENSION_SUFFIXES
from importlib.util import cache_from_source as _compiled_file_name
@@ -74,10 +75,6 @@ elif os.name != 'nt':
pass
-def if_dl(s):
- return s if have_rtld else ''
-
-
def get_abi3_suffix():
"""Return the file extension for an abi3-compliant Extension()"""
for suffix in EXTENSION_SUFFIXES:
@@ -355,30 +352,34 @@ class build_ext(_build_ext):
raise BaseError(stub_file + " already exists! Please delete.")
if not self.dry_run:
with open(stub_file, 'w', encoding="utf-8") as f:
- content = '\n'.join([
- "def __bootstrap__():",
- " global __bootstrap__, __file__, __loader__",
- " import sys, os, pkg_resources, importlib.util" + if_dl(", dl"),
- " __file__ = pkg_resources.resource_filename"
- f"(__name__,{os.path.basename(ext._file_name)!r})",
- " del __bootstrap__",
- " if '__loader__' in globals():",
- " del __loader__",
- if_dl(" old_flags = sys.getdlopenflags()"),
- " old_dir = os.getcwd()",
- " try:",
- " os.chdir(os.path.dirname(__file__))",
- if_dl(" sys.setdlopenflags(dl.RTLD_NOW)"),
- " spec = importlib.util.spec_from_file_location(",
- " __name__, __file__)",
- " mod = importlib.util.module_from_spec(spec)",
- " spec.loader.exec_module(mod)",
- " finally:",
- if_dl(" sys.setdlopenflags(old_flags)"),
- " os.chdir(old_dir)",
- "__bootstrap__()",
- "", # terminal \n
- ])
+ content = (
+ textwrap.dedent(f"""
+ def __bootstrap__():
+ global __bootstrap__, __file__, __loader__
+ import sys, os, importlib.resources as irs, importlib.util
+ #rtld import dl
+ with irs.files(__name__).joinpath(
+ {os.path.basename(ext._file_name)!r}) as __file__:
+ del __bootstrap__
+ if '__loader__' in globals():
+ del __loader__
+ #rtld old_flags = sys.getdlopenflags()
+ old_dir = os.getcwd()
+ try:
+ os.chdir(os.path.dirname(__file__))
+ #rtld sys.setdlopenflags(dl.RTLD_NOW)
+ spec = importlib.util.spec_from_file_location(
+ __name__, __file__)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ finally:
+ #rtld sys.setdlopenflags(old_flags)
+ os.chdir(old_dir)
+ __bootstrap__()
+ """)
+ .lstrip()
+ .replace('#rtld', '#rtld' * (not have_rtld))
+ )
f.write(content)
if compile:
self._compile_and_remove_stub(stub_file)
diff --git a/contrib/python/setuptools/py3/setuptools/command/develop.py b/contrib/python/setuptools/py3/setuptools/command/develop.py
index 7eee29d491f..1f704fcee86 100644
--- a/contrib/python/setuptools/py3/setuptools/command/develop.py
+++ b/contrib/python/setuptools/py3/setuptools/command/develop.py
@@ -1,195 +1,55 @@
-import glob
-import os
+import site
+import subprocess
+import sys
-import setuptools
-from setuptools import _normalization, _path, namespaces
-from setuptools.command.easy_install import easy_install
+from setuptools import Command
+from setuptools.warnings import SetuptoolsDeprecationWarning
-from ..unicode_utils import _read_utf8_with_fallback
-from distutils import log
-from distutils.errors import DistutilsOptionError
-from distutils.util import convert_path
-
-
-class develop(namespaces.DevelopInstaller, easy_install):
+class develop(Command):
"""Set up package for development"""
- description = "install package in 'development mode'"
-
- user_options = easy_install.user_options + [
- ("uninstall", "u", "Uninstall this source package"),
- ("egg-path=", None, "Set the path to be used in the .egg-link file"),
+ user_options = [
+ ("install-dir=", "d", "install package to DIR"),
+ ('no-deps', 'N', "don't install dependencies"),
+ ('user', None, f"install in user site-package '{site.USER_SITE}'"),
+ ('prefix=', None, "installation prefix"),
+ ("index-url=", "i", "base URL of Python Package Index"),
+ ]
+ boolean_options = [
+ 'no-deps',
+ 'user',
]
- boolean_options = easy_install.boolean_options + ['uninstall']
-
- command_consumes_arguments = False # override base
+ install_dir = None
+ no_deps = False
+ user = False
+ prefix = None
+ index_url = None
def run(self):
- if self.uninstall:
- self.multi_version = True
- self.uninstall_link()
- self.uninstall_namespaces()
- else:
- self.install_for_development()
- self.warn_deprecated_options()
+ cmd = (
+ [sys.executable, '-m', 'pip', 'install', '-e', '.', '--use-pep517']
+ + ['--target', self.install_dir] * bool(self.install_dir)
+ + ['--no-deps'] * self.no_deps
+ + ['--user'] * self.user
+ + ['--prefix', self.prefix] * bool(self.prefix)
+ + ['--index-url', self.index_url] * bool(self.index_url)
+ )
+ subprocess.check_call(cmd)
def initialize_options(self):
- self.uninstall = None
- self.egg_path = None
- easy_install.initialize_options(self)
- self.setup_path = None
- self.always_copy_from = '.' # always copy eggs installed in curdir
+ DevelopDeprecationWarning.emit()
def finalize_options(self) -> None:
- import pkg_resources
-
- ei = self.get_finalized_command("egg_info")
- self.args = [ei.egg_name]
-
- easy_install.finalize_options(self)
- self.expand_basedirs()
- self.expand_dirs()
- # pick up setup-dir .egg files only: no .egg-info
- self.package_index.scan(glob.glob('*.egg'))
-
- egg_link_fn = (
- _normalization.filename_component_broken(ei.egg_name) + '.egg-link'
- )
- self.egg_link = os.path.join(self.install_dir, egg_link_fn)
- self.egg_base = ei.egg_base
- if self.egg_path is None:
- self.egg_path = os.path.abspath(ei.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"
- " directory to " + target
- )
-
- # Make a distribution for the package's source
- self.dist = pkg_resources.Distribution(
- target,
- pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)),
- project_name=ei.egg_name,
- )
-
- self.setup_path = self._resolve_setup_path(
- self.egg_base,
- self.install_dir,
- self.egg_path,
- )
-
- @staticmethod
- def _resolve_setup_path(egg_base, install_dir, egg_path):
- """
- Generate a path from egg_base back to '.' where the
- setup script resides and ensure that path points to the
- setup path from $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 = _path.normpath(os.path.join(install_dir, egg_path, path_to_setup))
- curdir = _path.normpath(os.curdir)
- if resolved != curdir:
- raise DistutilsOptionError(
- "Can't get a consistent path to setup script from"
- " installation directory",
- resolved,
- curdir,
- )
- return path_to_setup
-
- def install_for_development(self) -> None:
- self.run_command('egg_info')
-
- # Build extensions in-place
- self.reinitialize_command('build_ext', inplace=True)
- self.run_command('build_ext')
+ pass
- if setuptools.bootstrap_install_from:
- self.easy_install(setuptools.bootstrap_install_from)
- setuptools.bootstrap_install_from = None
- self.install_namespaces()
-
- # create an .egg-link in the installation dir, pointing to our egg
- log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
- if not self.dry_run:
- with open(self.egg_link, "w", encoding="utf-8") as f:
- f.write(self.egg_path + "\n" + self.setup_path)
- # postprocess the installed distro, fixing up .pth, installing scripts,
- # and handling requirements
- self.process_distribution(None, self.dist, not self.no_deps)
-
- def uninstall_link(self) -> None:
- if os.path.exists(self.egg_link):
- log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
-
- contents = [
- line.rstrip()
- for line in _read_utf8_with_fallback(self.egg_link).splitlines()
- ]
-
- if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
- log.warn("Link points to %s: uninstall aborted", contents)
- return
- if not self.dry_run:
- os.unlink(self.egg_link)
- if not self.dry_run:
- self.update_pth(self.dist) # remove any .pth link to us
- if self.distribution.scripts:
- # XXX should also check for entry point scripts!
- log.warn("Note: you must uninstall or replace scripts manually!")
-
- def install_egg_scripts(self, dist):
- if dist is not self.dist:
- # Installing a dependency, so fall back to normal behavior
- return easy_install.install_egg_scripts(self, dist)
-
- # create wrapper scripts in the script dir, pointing to dist.scripts
-
- # new-style...
- self.install_wrapper_scripts(dist)
-
- # ...and old-style
- for script_name in self.distribution.scripts or []:
- script_path = os.path.abspath(convert_path(script_name))
- script_name = os.path.basename(script_path)
- script_text = _read_utf8_with_fallback(script_path)
- self.install_script(dist, script_name, script_text, script_path)
-
- return None
-
- def install_wrapper_scripts(self, dist):
- dist = VersionlessRequirement(dist)
- return easy_install.install_wrapper_scripts(self, dist)
-
-
-class VersionlessRequirement:
- """
- Adapt a pkg_resources.Distribution to simply return the project
- name as the 'requirement' so that scripts will work across
- multiple versions.
-
- >>> from pkg_resources import Distribution
- >>> dist = Distribution(project_name='foo', version='1.0')
- >>> str(dist.as_requirement())
- 'foo==1.0'
- >>> adapted_dist = VersionlessRequirement(dist)
- >>> str(adapted_dist.as_requirement())
- 'foo'
+class DevelopDeprecationWarning(SetuptoolsDeprecationWarning):
+ _SUMMARY = "develop command is deprecated."
+ _DETAILS = """
+ Please avoid running ``setup.py`` and ``develop``.
+ Instead, use standards-based tools like pip or uv.
"""
-
- def __init__(self, dist) -> None:
- self.__dist = dist
-
- def __getattr__(self, name: str):
- return getattr(self.__dist, name)
-
- def as_requirement(self):
- return self.project_name
+ _SEE_URL = "https://github.com/pypa/setuptools/issues/917"
+ _DUE_DATE = 2025, 10, 31
diff --git a/contrib/python/setuptools/py3/setuptools/command/easy_install.py b/contrib/python/setuptools/py3/setuptools/command/easy_install.py
index eb1b4c1fcc2..8765793d4cd 100644
--- a/contrib/python/setuptools/py3/setuptools/command/easy_install.py
+++ b/contrib/python/setuptools/py3/setuptools/command/easy_install.py
@@ -1,2365 +1,30 @@
-"""
-Easy Install
-------------
-
-A tool for doing automatic download/extract/build of distutils-based Python
-packages. For detailed documentation, see the accompanying EasyInstall.txt
-file, or visit the `EasyInstall home page`__.
-
-__ https://setuptools.pypa.io/en/latest/deprecated/easy_install.html
-
-"""
-
-from __future__ import annotations
-
-import configparser
-import contextlib
-import io
import os
-import random
-import re
-import shlex
-import shutil
-import site
-import stat
-import struct
-import subprocess
import sys
-import sysconfig
-import tempfile
-import textwrap
-import warnings
-import zipfile
-import zipimport
-from collections.abc import Iterable
-from glob import glob
-from sysconfig import get_path
-from typing import TYPE_CHECKING, NoReturn, TypedDict
-
-from jaraco.text import yield_lines
+import types
-import pkg_resources
-from pkg_resources import (
- DEVELOP_DIST,
- Distribution,
- DistributionNotFound,
- EggMetadata,
- Environment,
- PathMetadata,
- Requirement,
- VersionConflict,
- WorkingSet,
- find_distributions,
- get_distribution,
- normalize_path,
- resource_string,
-)
from setuptools import Command
-from setuptools.archive_util import unpack_archive
-from setuptools.command import bdist_egg, egg_info, setopt
-from setuptools.package_index import URL_SCHEME, PackageIndex, parse_requirement_arg
-from setuptools.sandbox import run_setup
-from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
-from setuptools.wheel import Wheel
-
-from .._path import ensure_directory
-from .._shutil import attempt_chmod_verbose as chmod, rmtree as _rmtree
-from ..compat import py39, py312
-
-from distutils import dir_util, log
-from distutils.command import install
-from distutils.command.build_scripts import first_line_re
-from distutils.errors import (
- DistutilsArgError,
- DistutilsError,
- DistutilsOptionError,
- DistutilsPlatformError,
-)
-from distutils.util import convert_path, get_platform, subst_vars
-
-if TYPE_CHECKING:
- from typing_extensions import Self
-
-# Turn on PEP440Warnings
-warnings.filterwarnings("default", category=pkg_resources.PEP440Warning)
-
-__all__ = [
- 'easy_install',
- 'PthDistributions',
- 'extract_wininst_cfg',
- 'get_exe_prefixes',
-]
-
-def is_64bit():
- return struct.calcsize("P") == 8
-
-
-def _to_bytes(s):
- return s.encode('utf8')
-
-
-def isascii(s):
- try:
- s.encode('ascii')
- except UnicodeError:
- return False
- return True
-
-
-def _one_liner(text):
- return textwrap.dedent(text).strip().replace('\n', '; ')
+from .. import _scripts, warnings
class easy_install(Command):
- """Manage a download/build/install process"""
+ """Stubbed command for temporary pbr compatibility."""
- description = "Find/get/install Python packages"
- command_consumes_arguments = True
- user_options = [
- ('prefix=', None, "installation prefix"),
- ("zip-ok", "z", "install package as a zipfile"),
- ("multi-version", "m", "make apps have to require() a version"),
- ("upgrade", "U", "force upgrade (searches PyPI for latest versions)"),
- ("install-dir=", "d", "install package to DIR"),
- ("script-dir=", "s", "install scripts to DIR"),
- ("exclude-scripts", "x", "Don't install scripts"),
- ("always-copy", "a", "Copy all needed packages to install dir"),
- ("index-url=", "i", "base URL of Python Package Index"),
- ("find-links=", "f", "additional URL(s) to search for packages"),
- ("build-directory=", "b", "download/extract/build in DIR; keep the results"),
- (
- 'optimize=',
- 'O',
- 'also compile with optimization: -O1 for "python -O", '
- '-O2 for "python -OO", and -O0 to disable [default: -O0]',
- ),
- ('record=', None, "filename in which to record list of installed files"),
- ('always-unzip', 'Z', "don't install as a zipfile, no matter what"),
- ('site-dirs=', 'S', "list of directories where .pth files work"),
- ('editable', 'e', "Install specified packages in editable form"),
- ('no-deps', 'N', "don't install dependencies"),
- ('allow-hosts=', 'H', "pattern(s) that hostnames must match"),
- ('local-snapshots-ok', 'l', "allow building eggs from local checkouts"),
- ('version', None, "print version information and exit"),
- (
- 'no-find-links',
- None,
- "Don't load find-links defined in packages being installed",
+def __getattr__(name):
+ attr = getattr(
+ types.SimpleNamespace(
+ ScriptWriter=_scripts.ScriptWriter,
+ sys_executable=os.environ.get(
+ "__PYVENV_LAUNCHER__", os.path.normpath(sys.executable)
+ ),
),
- ('user', None, f"install in user site-package '{site.USER_SITE}'"),
- ]
- boolean_options = [
- 'zip-ok',
- 'multi-version',
- 'exclude-scripts',
- 'upgrade',
- 'always-copy',
- 'editable',
- 'no-deps',
- 'local-snapshots-ok',
- 'version',
- 'user',
- ]
-
- negative_opt = {'always-unzip': 'zip-ok'}
- create_index = PackageIndex
-
- def initialize_options(self):
- EasyInstallDeprecationWarning.emit()
-
- # the --user option seems to be an opt-in one,
- # so the default should be False.
- self.user = False
- self.zip_ok = self.local_snapshots_ok = None
- self.install_dir = self.script_dir = self.exclude_scripts = None
- self.index_url = None
- self.find_links = None
- self.build_directory = None
- self.args = None
- self.optimize = self.record = None
- self.upgrade = self.always_copy = self.multi_version = None
- self.editable = self.no_deps = self.allow_hosts = None
- self.root = self.prefix = self.no_report = None
- self.version = None
- self.install_purelib = None # for pure module distributions
- self.install_platlib = None # non-pure (dists w/ extensions)
- self.install_headers = None # for C/C++ headers
- self.install_lib = None # set to either purelib or platlib
- self.install_scripts = None
- self.install_data = None
- self.install_base = None
- self.install_platbase = None
- self.install_userbase = site.USER_BASE
- self.install_usersite = site.USER_SITE
- self.no_find_links = None
-
- # Options not specifiable via command line
- self.package_index = None
- self.pth_file = self.always_copy_from = None
- self.site_dirs = None
- self.installed_projects = {}
- # Always read easy_install options, even if we are subclassed, or have
- # an independent instance created. This ensures that defaults will
- # always come from the standard configuration file(s)' "easy_install"
- # section, even if this is a "develop" or "install" command, or some
- # other embedding.
- self._dry_run = None
- self.verbose = self.distribution.verbose
- self.distribution._set_command_options(
- self, self.distribution.get_option_dict('easy_install')
- )
-
- def delete_blockers(self, blockers) -> None:
- extant_blockers = (
- filename
- for filename in blockers
- if os.path.exists(filename) or os.path.islink(filename)
- )
- list(map(self._delete_path, extant_blockers))
-
- def _delete_path(self, path):
- log.info("Deleting %s", path)
- if self.dry_run:
- return
-
- is_tree = os.path.isdir(path) and not os.path.islink(path)
- remover = _rmtree if is_tree else os.unlink
- remover(path)
-
- @staticmethod
- def _render_version():
- """
- Render the Setuptools version and installation details, then exit.
- """
- ver = f'{sys.version_info.major}.{sys.version_info.minor}'
- dist = get_distribution('setuptools')
- print(f'setuptools {dist.version} from {dist.location} (Python {ver})')
- raise SystemExit
-
- def finalize_options(self) -> None: # noqa: C901 # is too complex (25) # FIXME
- self.version and self._render_version()
-
- py_version = sys.version.split()[0]
-
- self.config_vars = dict(sysconfig.get_config_vars())
-
- self.config_vars.update({
- 'dist_name': self.distribution.get_name(),
- 'dist_version': self.distribution.get_version(),
- 'dist_fullname': self.distribution.get_fullname(),
- 'py_version': py_version,
- 'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}',
- 'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}',
- 'sys_prefix': self.config_vars['prefix'],
- 'sys_exec_prefix': self.config_vars['exec_prefix'],
- # Only POSIX systems have abiflags
- 'abiflags': getattr(sys, 'abiflags', ''),
- # Only python 3.9+ has platlibdir
- 'platlibdir': getattr(sys, 'platlibdir', 'lib'),
- })
- with contextlib.suppress(AttributeError):
- # only for distutils outside stdlib
- self.config_vars.update({
- 'implementation_lower': install._get_implementation().lower(),
- 'implementation': install._get_implementation(),
- })
-
- # pypa/distutils#113 Python 3.9 compat
- self.config_vars.setdefault(
- 'py_version_nodot_plat',
- getattr(sys, 'windir', '').replace('.', ''),
- )
-
- self.config_vars['userbase'] = self.install_userbase
- self.config_vars['usersite'] = self.install_usersite
- if self.user and not site.ENABLE_USER_SITE:
- log.warn("WARNING: The user site-packages directory is disabled.")
-
- self._fix_install_dir_for_user_site()
-
- self.expand_basedirs()
- self.expand_dirs()
-
- self._expand(
- 'install_dir',
- 'script_dir',
- 'build_directory',
- 'site_dirs',
- )
- # If a non-default installation directory was specified, default the
- # script directory to match it.
- if self.script_dir is None:
- self.script_dir = self.install_dir
-
- if self.no_find_links is None:
- self.no_find_links = False
-
- # Let install_dir get set by install_lib command, which in turn
- # gets its info from the install command, and takes into account
- # --prefix and --home and all that other crud.
- self.set_undefined_options('install_lib', ('install_dir', 'install_dir'))
- # Likewise, set default script_dir from 'install_scripts.install_dir'
- self.set_undefined_options('install_scripts', ('install_dir', 'script_dir'))
-
- if self.user and self.install_purelib:
- self.install_dir = self.install_purelib
- self.script_dir = self.install_scripts
- # default --record from the install command
- self.set_undefined_options('install', ('record', 'record'))
- self.all_site_dirs = get_site_dirs()
- self.all_site_dirs.extend(self._process_site_dirs(self.site_dirs))
-
- if not self.editable:
- self.check_site_dir()
- default_index = os.getenv("__EASYINSTALL_INDEX", "https://pypi.org/simple/")
- # ^ Private API for testing purposes only
- self.index_url = self.index_url or default_index
- self.shadow_path = self.all_site_dirs[:]
- for path_item in self.install_dir, normalize_path(self.script_dir):
- if path_item not in self.shadow_path:
- self.shadow_path.insert(0, path_item)
-
- if self.allow_hosts is not None:
- hosts = [s.strip() for s in self.allow_hosts.split(',')]
- else:
- hosts = ['*']
- if self.package_index is None:
- self.package_index = self.create_index(
- self.index_url,
- search_path=self.shadow_path,
- hosts=hosts,
- )
- self.local_index = Environment(self.shadow_path + sys.path)
-
- if self.find_links is not None:
- if isinstance(self.find_links, str):
- self.find_links = self.find_links.split()
- else:
- self.find_links = []
- if self.local_snapshots_ok:
- self.package_index.scan_egg_links(self.shadow_path + sys.path)
- if not self.no_find_links:
- self.package_index.add_find_links(self.find_links)
- self.set_undefined_options('install_lib', ('optimize', 'optimize'))
- self.optimize = self._validate_optimize(self.optimize)
-
- if self.editable and not self.build_directory:
- raise DistutilsArgError(
- "Must specify a build directory (-b) when using --editable"
- )
- if not self.args:
- raise DistutilsArgError(
- "No urls, filenames, or requirements specified (see --help)"
- )
-
- self.outputs: list[str] = []
-
- @staticmethod
- def _process_site_dirs(site_dirs):
- if site_dirs is None:
- return
-
- normpath = map(normalize_path, sys.path)
- site_dirs = [os.path.expanduser(s.strip()) for s in site_dirs.split(',')]
- for d in site_dirs:
- if not os.path.isdir(d):
- log.warn("%s (in --site-dirs) does not exist", d)
- elif normalize_path(d) not in normpath:
- raise DistutilsOptionError(d + " (in --site-dirs) is not on sys.path")
- else:
- yield normalize_path(d)
-
- @staticmethod
- def _validate_optimize(value):
- try:
- value = int(value)
- if value not in range(3):
- raise ValueError
- except ValueError as e:
- raise DistutilsOptionError("--optimize must be 0, 1, or 2") from e
-
- return value
-
- def _fix_install_dir_for_user_site(self):
- """
- Fix the install_dir if "--user" was used.
- """
- if not self.user:
- return
-
- self.create_home_path()
- if self.install_userbase is None:
- msg = "User base directory is not specified"
- raise DistutilsPlatformError(msg)
- self.install_base = self.install_platbase = self.install_userbase
- scheme_name = f'{os.name}_user'
- self.select_scheme(scheme_name)
-
- def _expand_attrs(self, attrs):
- for attr in attrs:
- val = getattr(self, attr)
- if val is not None:
- if os.name == 'posix' or os.name == 'nt':
- val = os.path.expanduser(val)
- val = subst_vars(val, self.config_vars)
- setattr(self, attr, val)
-
- def expand_basedirs(self) -> None:
- """Calls `os.path.expanduser` on install_base, install_platbase and
- root."""
- self._expand_attrs(['install_base', 'install_platbase', 'root'])
-
- def expand_dirs(self) -> None:
- """Calls `os.path.expanduser` on install dirs."""
- dirs = [
- 'install_purelib',
- 'install_platlib',
- 'install_lib',
- 'install_headers',
- 'install_scripts',
- 'install_data',
- ]
- self._expand_attrs(dirs)
-
- def run(self, show_deprecation: bool = True) -> None:
- if show_deprecation:
- self.announce(
- "WARNING: The easy_install command is deprecated "
- "and will be removed in a future version.",
- log.WARN,
- )
- if self.verbose != self.distribution.verbose:
- log.set_verbosity(self.verbose)
- try:
- for spec in self.args:
- self.easy_install(spec, not self.no_deps)
- if self.record:
- outputs = self.outputs
- if self.root: # strip any package prefix
- root_len = len(self.root)
- for counter in range(len(outputs)):
- outputs[counter] = outputs[counter][root_len:]
- from distutils import file_util
-
- self.execute(
- file_util.write_file,
- (self.record, outputs),
- f"writing list of installed files to '{self.record}'",
- )
- self.warn_deprecated_options()
- finally:
- log.set_verbosity(self.distribution.verbose)
-
- def pseudo_tempname(self):
- """Return a pseudo-tempname base in the install directory.
- This code is intentionally naive; if a malicious party can write to
- the target directory you're already in deep doodoo.
- """
- try:
- pid = os.getpid()
- except Exception:
- pid = random.randint(0, sys.maxsize)
- return os.path.join(self.install_dir, f"test-easy-install-{pid}")
-
- def warn_deprecated_options(self) -> None:
- pass
-
- def check_site_dir(self) -> None: # is too complex (12) # FIXME
- """Verify that self.install_dir is .pth-capable dir, if needed"""
-
- instdir = normalize_path(self.install_dir)
- pth_file = os.path.join(instdir, 'easy-install.pth')
-
- if not os.path.exists(instdir):
- try:
- os.makedirs(instdir)
- except OSError:
- self.cant_write_to_target()
-
- # Is it a configured, PYTHONPATH, implicit, or explicit site dir?
- is_site_dir = instdir in self.all_site_dirs
-
- if not is_site_dir and not self.multi_version:
- # No? Then directly test whether it does .pth file processing
- is_site_dir = self.check_pth_processing()
- else:
- # make sure we can write to target dir
- testfile = self.pseudo_tempname() + '.write-test'
- test_exists = os.path.exists(testfile)
- try:
- if test_exists:
- os.unlink(testfile)
- open(testfile, 'wb').close()
- os.unlink(testfile)
- except OSError:
- self.cant_write_to_target()
-
- if not is_site_dir and not self.multi_version:
- # Can't install non-multi to non-site dir with easy_install
- pythonpath = os.environ.get('PYTHONPATH', '')
- log.warn(self.__no_default_msg, self.install_dir, pythonpath)
-
- if is_site_dir:
- if self.pth_file is None:
- self.pth_file = PthDistributions(pth_file, self.all_site_dirs)
- else:
- self.pth_file = None
-
- if self.multi_version and not os.path.exists(pth_file):
- self.pth_file = None # don't create a .pth file
- self.install_dir = instdir
-
- __cant_write_msg = textwrap.dedent(
- """
- can't create or remove files in install directory
-
- The following error occurred while trying to add or remove files in the
- installation directory:
-
- %s
-
- The installation directory you specified (via --install-dir, --prefix, or
- the distutils default setting) was:
-
- %s
- """
- ).lstrip()
-
- __not_exists_id = textwrap.dedent(
- """
- This directory does not currently exist. Please create it and try again, or
- choose a different installation directory (using the -d or --install-dir
- option).
- """
- ).lstrip()
-
- __access_msg = textwrap.dedent(
- """
- Perhaps your account does not have write access to this directory? If the
- installation directory is a system-owned directory, you may need to sign in
- as the administrator or "root" account. If you do not have administrative
- access to this machine, you may wish to choose a different installation
- directory, preferably one that is listed in your PYTHONPATH environment
- variable.
-
- For information on other options, you may wish to consult the
- documentation at:
-
- https://setuptools.pypa.io/en/latest/deprecated/easy_install.html
-
- Please make the appropriate changes for your system and try again.
- """
- ).lstrip()
-
- def cant_write_to_target(self) -> NoReturn:
- msg = self.__cant_write_msg % (
- sys.exc_info()[1],
- self.install_dir,
- )
-
- if not os.path.exists(self.install_dir):
- msg += '\n' + self.__not_exists_id
- else:
- msg += '\n' + self.__access_msg
- raise DistutilsError(msg)
-
- def check_pth_processing(self): # noqa: C901
- """Empirically verify whether .pth files are supported in inst. dir"""
- instdir = self.install_dir
- log.info("Checking .pth file support in %s", instdir)
- pth_file = self.pseudo_tempname() + ".pth"
- ok_file = pth_file + '.ok'
- ok_exists = os.path.exists(ok_file)
- tmpl = (
- _one_liner(
- """
- import os
- f = open({ok_file!r}, 'w', encoding="utf-8")
- f.write('OK')
- f.close()
- """
- )
- + '\n'
- )
- try:
- if ok_exists:
- os.unlink(ok_file)
- dirname = os.path.dirname(ok_file)
- os.makedirs(dirname, exist_ok=True)
- f = open(pth_file, 'w', encoding=py312.PTH_ENCODING)
- # ^-- Python<3.13 require encoding="locale" instead of "utf-8",
- # see python/cpython#77102.
- except OSError:
- self.cant_write_to_target()
- else:
- try:
- f.write(tmpl.format(**locals()))
- f.close()
- f = None
- executable = sys.executable
- if os.name == 'nt':
- dirname, basename = os.path.split(executable)
- alt = os.path.join(dirname, 'pythonw.exe')
- use_alt = basename.lower() == 'python.exe' and os.path.exists(alt)
- if use_alt:
- # use pythonw.exe to avoid opening a console window
- executable = alt
-
- from distutils.spawn import spawn
-
- spawn([executable, '-E', '-c', 'pass'], 0)
-
- if os.path.exists(ok_file):
- log.info("TEST PASSED: %s appears to support .pth files", instdir)
- return True
- finally:
- if f:
- f.close()
- if os.path.exists(ok_file):
- os.unlink(ok_file)
- if os.path.exists(pth_file):
- os.unlink(pth_file)
- if not self.multi_version:
- log.warn("TEST FAILED: %s does NOT support .pth files", instdir)
- return False
-
- def install_egg_scripts(self, dist) -> None:
- """Write all the scripts for `dist`, unless scripts are excluded"""
- if not self.exclude_scripts and dist.metadata_isdir('scripts'):
- for script_name in dist.metadata_listdir('scripts'):
- if dist.metadata_isdir('scripts/' + script_name):
- # The "script" is a directory, likely a Python 3
- # __pycache__ directory, so skip it.
- continue
- self.install_script(
- dist, script_name, dist.get_metadata('scripts/' + script_name)
- )
- self.install_wrapper_scripts(dist)
-
- def add_output(self, path) -> None:
- if os.path.isdir(path):
- for base, dirs, files in os.walk(path):
- for filename in files:
- self.outputs.append(os.path.join(base, filename))
- else:
- self.outputs.append(path)
-
- def not_editable(self, spec) -> None:
- if self.editable:
- raise DistutilsArgError(
- f"Invalid argument {spec!r}: you can't use filenames or URLs "
- "with --editable (except via the --find-links option)."
- )
-
- def check_editable(self, spec) -> None:
- if not self.editable:
- return
-
- if os.path.exists(os.path.join(self.build_directory, spec.key)):
- raise DistutilsArgError(
- f"{spec.key!r} already exists in {self.build_directory}; can't do a checkout there"
- )
-
- @contextlib.contextmanager
- def _tmpdir(self):
- tmpdir = tempfile.mkdtemp(prefix="easy_install-")
- try:
- # cast to str as workaround for #709 and #710 and #712
- yield str(tmpdir)
- finally:
- os.path.exists(tmpdir) and _rmtree(tmpdir)
-
- def easy_install(self, spec, deps: bool = False) -> Distribution | None:
- with self._tmpdir() as tmpdir:
- if not isinstance(spec, Requirement):
- if URL_SCHEME(spec):
- # It's a url, download it to tmpdir and process
- self.not_editable(spec)
- dl = self.package_index.download(spec, tmpdir)
- return self.install_item(None, dl, tmpdir, deps, True)
-
- elif os.path.exists(spec):
- # Existing file or directory, just process it directly
- self.not_editable(spec)
- return self.install_item(None, spec, tmpdir, deps, True)
- else:
- spec = parse_requirement_arg(spec)
-
- self.check_editable(spec)
- dist = self.package_index.fetch_distribution(
- spec,
- tmpdir,
- self.upgrade,
- self.editable,
- not self.always_copy,
- self.local_index,
- )
- if dist is None:
- msg = f"Could not find suitable distribution for {spec!r}"
- if self.always_copy:
- msg += " (--always-copy skips system and development eggs)"
- raise DistutilsError(msg)
- elif dist.precedence == DEVELOP_DIST:
- # .egg-info dists don't need installing, just process deps
- self.process_distribution(spec, dist, deps, "Using")
- return dist
- else:
- return self.install_item(spec, dist.location, tmpdir, deps)
-
- def install_item(
- self, spec, download, tmpdir, deps, install_needed: bool = False
- ) -> Distribution | None:
- # Installation is also needed if file in tmpdir or is not an egg
- install_needed = install_needed or bool(self.always_copy)
- install_needed = install_needed or os.path.dirname(download) == tmpdir
- install_needed = install_needed or not download.endswith('.egg')
- install_needed = install_needed or (
- self.always_copy_from is not None
- and os.path.dirname(normalize_path(download))
- == normalize_path(self.always_copy_from)
- )
-
- if spec and not install_needed:
- # at this point, we know it's a local .egg, we just don't know if
- # it's already installed.
- for dist in self.local_index[spec.project_name]:
- if dist.location == download:
- break
- else:
- install_needed = True # it's not in the local index
-
- log.info("Processing %s", os.path.basename(download))
-
- if install_needed:
- dists = self.install_eggs(spec, download, tmpdir)
- for dist in dists:
- self.process_distribution(spec, dist, deps)
- else:
- dists = [self.egg_distribution(download)]
- self.process_distribution(spec, dists[0], deps, "Using")
-
- if spec is not None:
- for dist in dists:
- if dist in spec:
- return dist
- return None
-
- def select_scheme(self, name):
- try:
- install._select_scheme(self, name)
- except AttributeError:
- # stdlib distutils
- install.install.select_scheme(self, name.replace('posix', 'unix'))
-
- # FIXME: 'easy_install.process_distribution' is too complex (12)
- def process_distribution( # noqa: C901
- self,
- requirement,
- dist,
- deps: bool = True,
- *info,
- ) -> None:
- self.update_pth(dist)
- self.package_index.add(dist)
- if dist in self.local_index[dist.key]:
- self.local_index.remove(dist)
- self.local_index.add(dist)
- self.install_egg_scripts(dist)
- self.installed_projects[dist.key] = dist
- log.info(self.installation_report(requirement, dist, *info))
- if dist.has_metadata('dependency_links.txt') and not self.no_find_links:
- self.package_index.add_find_links(
- dist.get_metadata_lines('dependency_links.txt')
- )
- if not deps and not self.always_copy:
- return
- elif requirement is not None and dist.key != requirement.key:
- log.warn("Skipping dependencies for %s", dist)
- return # XXX this is not the distribution we were looking for
- elif requirement is None or dist not in requirement:
- # if we wound up with a different version, resolve what we've got
- distreq = dist.as_requirement()
- requirement = Requirement(str(distreq))
- log.info("Processing dependencies for %s", requirement)
- try:
- distros = WorkingSet([]).resolve(
- [requirement], self.local_index, self.easy_install
- )
- except DistributionNotFound as e:
- raise DistutilsError(str(e)) from e
- except VersionConflict as e:
- raise DistutilsError(e.report()) from e
- if self.always_copy or self.always_copy_from:
- # Force all the relevant distros to be copied or activated
- for dist in distros:
- if dist.key not in self.installed_projects:
- self.easy_install(dist.as_requirement())
- log.info("Finished processing dependencies for %s", requirement)
-
- def should_unzip(self, dist) -> bool:
- if self.zip_ok is not None:
- return not self.zip_ok
- if dist.has_metadata('not-zip-safe'):
- return True
- if not dist.has_metadata('zip-safe'):
- return True
- return False
-
- def maybe_move(self, spec, dist_filename, setup_base):
- dst = os.path.join(self.build_directory, spec.key)
- if os.path.exists(dst):
- msg = "%r already exists in %s; build directory %s will not be kept"
- log.warn(msg, spec.key, self.build_directory, setup_base)
- return setup_base
- if os.path.isdir(dist_filename):
- setup_base = dist_filename
- else:
- if os.path.dirname(dist_filename) == setup_base:
- os.unlink(dist_filename) # get it out of the tmp dir
- contents = os.listdir(setup_base)
- if len(contents) == 1:
- dist_filename = os.path.join(setup_base, contents[0])
- if os.path.isdir(dist_filename):
- # if the only thing there is a directory, move it instead
- setup_base = dist_filename
- ensure_directory(dst)
- shutil.move(setup_base, dst)
- return dst
-
- def install_wrapper_scripts(self, dist) -> None:
- if self.exclude_scripts:
- return
- for args in ScriptWriter.best().get_args(dist):
- self.write_script(*args)
-
- def install_script(self, dist, script_name, script_text, dev_path=None) -> None:
- """Generate a legacy script wrapper and install it"""
- spec = str(dist.as_requirement())
- is_script = is_python_script(script_text, script_name)
-
- if is_script:
- body = self._load_template(dev_path) % locals()
- script_text = ScriptWriter.get_header(script_text) + body
- self.write_script(script_name, _to_bytes(script_text), 'b')
-
- @staticmethod
- def _load_template(dev_path):
- """
- There are a couple of template scripts in the package. This
- function loads one of them and prepares it for use.
- """
- # See https://github.com/pypa/setuptools/issues/134 for info
- # on script file naming and downstream issues with SVR4
- name = 'script.tmpl'
- if dev_path:
- name = name.replace('.tmpl', ' (dev).tmpl')
-
- raw_bytes = resource_string('setuptools', name)
- return raw_bytes.decode('utf-8')
-
- def write_script(self, script_name, contents, mode: str = "t", blockers=()) -> None:
- """Write an executable file to the scripts directory"""
- self.delete_blockers( # clean up old .py/.pyw w/o a script
- [os.path.join(self.script_dir, x) for x in blockers]
- )
- log.info("Installing %s script to %s", script_name, self.script_dir)
- target = os.path.join(self.script_dir, script_name)
- self.add_output(target)
-
- if self.dry_run:
- return
-
- mask = current_umask()
- ensure_directory(target)
- if os.path.exists(target):
- os.unlink(target)
-
- encoding = None if "b" in mode else "utf-8"
- with open(target, "w" + mode, encoding=encoding) as f:
- f.write(contents)
- chmod(target, 0o777 - mask)
-
- def install_eggs(self, spec, dist_filename, tmpdir) -> list[Distribution]:
- # .egg dirs or files are already built, so just return them
- installer_map = {
- '.egg': self.install_egg,
- '.exe': self.install_exe,
- '.whl': self.install_wheel,
- }
- try:
- install_dist = installer_map[dist_filename.lower()[-4:]]
- except KeyError:
- pass
- else:
- return [install_dist(dist_filename, tmpdir)]
-
- # Anything else, try to extract and build
- setup_base = tmpdir
- if os.path.isfile(dist_filename) and not dist_filename.endswith('.py'):
- unpack_archive(dist_filename, tmpdir, self.unpack_progress)
- elif os.path.isdir(dist_filename):
- setup_base = os.path.abspath(dist_filename)
-
- if (
- setup_base.startswith(tmpdir) # something we downloaded
- and self.build_directory
- and spec is not None
- ):
- setup_base = self.maybe_move(spec, dist_filename, setup_base)
-
- # Find the setup.py file
- setup_script = os.path.join(setup_base, 'setup.py')
-
- if not os.path.exists(setup_script):
- setups = glob(os.path.join(setup_base, '*', 'setup.py'))
- if not setups:
- raise DistutilsError(
- f"Couldn't find a setup script in {os.path.abspath(dist_filename)}"
- )
- if len(setups) > 1:
- raise DistutilsError(
- f"Multiple setup scripts in {os.path.abspath(dist_filename)}"
- )
- setup_script = setups[0]
-
- # Now run it, and return the result
- if self.editable:
- log.info(self.report_editable(spec, setup_script))
- return []
- else:
- return self.build_and_install(setup_script, setup_base)
-
- def egg_distribution(self, egg_path):
- if os.path.isdir(egg_path):
- metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO'))
- else:
- metadata = EggMetadata(zipimport.zipimporter(egg_path))
- return Distribution.from_filename(egg_path, metadata=metadata)
-
- # FIXME: 'easy_install.install_egg' is too complex (11)
- def install_egg(self, egg_path, tmpdir):
- destination = os.path.join(
- self.install_dir,
- os.path.basename(egg_path),
- )
- destination = os.path.abspath(destination)
- if not self.dry_run:
- ensure_directory(destination)
-
- dist = self.egg_distribution(egg_path)
- if not (
- os.path.exists(destination) and os.path.samefile(egg_path, destination)
- ):
- if os.path.isdir(destination) and not os.path.islink(destination):
- dir_util.remove_tree(destination, dry_run=self.dry_run)
- elif os.path.exists(destination):
- self.execute(
- os.unlink,
- (destination,),
- "Removing " + destination,
- )
- try:
- new_dist_is_zipped = False
- if os.path.isdir(egg_path):
- if egg_path.startswith(tmpdir):
- f, m = shutil.move, "Moving"
- else:
- f, m = shutil.copytree, "Copying"
- elif self.should_unzip(dist):
- self.mkpath(destination)
- f, m = self.unpack_and_compile, "Extracting"
- else:
- new_dist_is_zipped = True
- if egg_path.startswith(tmpdir):
- f, m = shutil.move, "Moving"
- else:
- f, m = shutil.copy2, "Copying"
- self.execute(
- f,
- (egg_path, destination),
- (m + " %s to %s")
- % (os.path.basename(egg_path), os.path.dirname(destination)),
- )
- update_dist_caches(
- destination,
- fix_zipimporter_caches=new_dist_is_zipped,
- )
- except Exception:
- update_dist_caches(destination, fix_zipimporter_caches=False)
- raise
-
- self.add_output(destination)
- return self.egg_distribution(destination)
-
- def install_exe(self, dist_filename, tmpdir):
- # See if it's valid, get data
- cfg = extract_wininst_cfg(dist_filename)
- if cfg is None:
- raise DistutilsError(
- f"{dist_filename} is not a valid distutils Windows .exe"
- )
- # Create a dummy distribution object until we build the real distro
- dist = Distribution(
- None,
- project_name=cfg.get('metadata', 'name'),
- version=cfg.get('metadata', 'version'),
- platform=get_platform(),
- )
-
- # Convert the .exe to an unpacked egg
- egg_path = os.path.join(tmpdir, dist.egg_name() + '.egg')
- dist.location = egg_path
- egg_tmp = egg_path + '.tmp'
- _egg_info = os.path.join(egg_tmp, 'EGG-INFO')
- pkg_inf = os.path.join(_egg_info, 'PKG-INFO')
- ensure_directory(pkg_inf) # make sure EGG-INFO dir exists
- dist._provider = PathMetadata(egg_tmp, _egg_info) # XXX
- self.exe_to_egg(dist_filename, egg_tmp)
-
- # Write EGG-INFO/PKG-INFO
- if not os.path.exists(pkg_inf):
- with open(pkg_inf, 'w', encoding="utf-8") as f:
- f.write('Metadata-Version: 1.0\n')
- for k, v in cfg.items('metadata'):
- if k != 'target_version':
- k = k.replace('_', '-').title()
- f.write(f'{k}: {v}\n')
- script_dir = os.path.join(_egg_info, 'scripts')
- # delete entry-point scripts to avoid duping
- self.delete_blockers([
- os.path.join(script_dir, args[0]) for args in ScriptWriter.get_args(dist)
- ])
- # Build .egg file from tmpdir
- bdist_egg.make_zipfile(
- egg_path,
- egg_tmp,
- verbose=self.verbose,
- dry_run=self.dry_run,
- )
- # install the .egg
- return self.install_egg(egg_path, tmpdir)
-
- # FIXME: 'easy_install.exe_to_egg' is too complex (12)
- def exe_to_egg(self, dist_filename, egg_tmp) -> None: # noqa: C901
- """Extract a bdist_wininst to the directories an egg would use"""
- # Check for .pth file and set up prefix translations
- prefixes = get_exe_prefixes(dist_filename)
- to_compile = []
- native_libs = []
- top_level = set()
-
- def process(src, dst):
- s = src.lower()
- for old, new in prefixes:
- if s.startswith(old):
- src = new + src[len(old) :]
- parts = src.split('/')
- dst = os.path.join(egg_tmp, *parts)
- dl = dst.lower()
- if dl.endswith('.pyd') or dl.endswith('.dll'):
- parts[-1] = bdist_egg.strip_module(parts[-1])
- top_level.add([os.path.splitext(parts[0])[0]])
- native_libs.append(src)
- elif dl.endswith('.py') and old != 'SCRIPTS/':
- top_level.add([os.path.splitext(parts[0])[0]])
- to_compile.append(dst)
- return dst
- if not src.endswith('.pth'):
- log.warn("WARNING: can't process %s", src)
- return None
-
- # extract, tracking .pyd/.dll->native_libs and .py -> to_compile
- unpack_archive(dist_filename, egg_tmp, process)
- stubs = []
- for res in native_libs:
- if res.lower().endswith('.pyd'): # create stubs for .pyd's
- parts = res.split('/')
- resource = parts[-1]
- parts[-1] = bdist_egg.strip_module(parts[-1]) + '.py'
- pyfile = os.path.join(egg_tmp, *parts)
- to_compile.append(pyfile)
- stubs.append(pyfile)
- bdist_egg.write_stub(resource, pyfile)
- self.byte_compile(to_compile) # compile .py's
- bdist_egg.write_safety_flag(
- os.path.join(egg_tmp, 'EGG-INFO'), bdist_egg.analyze_egg(egg_tmp, stubs)
- ) # write zip-safety flag
-
- for name in 'top_level', 'native_libs':
- if locals()[name]:
- txt = os.path.join(egg_tmp, 'EGG-INFO', name + '.txt')
- if not os.path.exists(txt):
- with open(txt, 'w', encoding="utf-8") as f:
- f.write('\n'.join(locals()[name]) + '\n')
-
- def install_wheel(self, wheel_path, tmpdir):
- wheel = Wheel(wheel_path)
- assert wheel.is_compatible()
- destination = os.path.join(self.install_dir, wheel.egg_name())
- destination = os.path.abspath(destination)
- if not self.dry_run:
- ensure_directory(destination)
- if os.path.isdir(destination) and not os.path.islink(destination):
- dir_util.remove_tree(destination, dry_run=self.dry_run)
- elif os.path.exists(destination):
- self.execute(
- os.unlink,
- (destination,),
- "Removing " + destination,
- )
- try:
- self.execute(
- wheel.install_as_egg,
- (destination,),
- (
- f"Installing {os.path.basename(wheel_path)} to {os.path.dirname(destination)}"
- ),
- )
- finally:
- update_dist_caches(destination, fix_zipimporter_caches=False)
- self.add_output(destination)
- return self.egg_distribution(destination)
-
- __mv_warning = textwrap.dedent(
- """
- Because this distribution was installed --multi-version, before you can
- import modules from this package in an application, you will need to
- 'import pkg_resources' and then use a 'require()' call similar to one of
- these examples, in order to select the desired version:
-
- pkg_resources.require("%(name)s") # latest installed version
- pkg_resources.require("%(name)s==%(version)s") # this exact version
- pkg_resources.require("%(name)s>=%(version)s") # this version or higher
- """
- ).lstrip()
-
- __id_warning = textwrap.dedent(
- """
- Note also that the installation directory must be on sys.path at runtime for
- this to work. (e.g. by being the application's script directory, by being on
- PYTHONPATH, or by being added to sys.path by your code.)
- """
+ name,
)
-
- def installation_report(self, req, dist, what: str = "Installed") -> str:
- """Helpful installation message for display to package users"""
- msg = "\n%(what)s %(eggloc)s%(extras)s"
- if self.multi_version and not self.no_report:
- msg += '\n' + self.__mv_warning
- if self.install_dir not in map(normalize_path, sys.path):
- msg += '\n' + self.__id_warning
-
- eggloc = dist.location
- name = dist.project_name
- version = dist.version
- extras = '' # TODO: self.report_extras(req, dist)
- return msg % locals()
-
- __editable_msg = textwrap.dedent(
- """
- Extracted editable version of %(spec)s to %(dirname)s
-
- If it uses setuptools in its setup script, you can activate it in
- "development" mode by going to that directory and running::
-
- %(python)s setup.py develop
-
- See the setuptools documentation for the "develop" command for more info.
- """
- ).lstrip()
-
- def report_editable(self, spec, setup_script):
- dirname = os.path.dirname(setup_script)
- python = sys.executable
- return '\n' + self.__editable_msg % locals()
-
- def run_setup(self, setup_script, setup_base, args) -> None:
- sys.modules.setdefault('distutils.command.bdist_egg', bdist_egg)
- sys.modules.setdefault('distutils.command.egg_info', egg_info)
-
- args = list(args)
- if self.verbose > 2:
- v = 'v' * (self.verbose - 1)
- args.insert(0, '-' + v)
- elif self.verbose < 2:
- args.insert(0, '-q')
- if self.dry_run:
- args.insert(0, '-n')
- log.info("Running %s %s", setup_script[len(setup_base) + 1 :], ' '.join(args))
- try:
- run_setup(setup_script, args)
- except SystemExit as v:
- raise DistutilsError(f"Setup script exited with {v.args[0]}") from v
-
- def build_and_install(self, setup_script, setup_base):
- args = ['bdist_egg', '--dist-dir']
-
- dist_dir = tempfile.mkdtemp(
- prefix='egg-dist-tmp-', dir=os.path.dirname(setup_script)
- )
- try:
- self._set_fetcher_options(os.path.dirname(setup_script))
- args.append(dist_dir)
-
- self.run_setup(setup_script, setup_base, args)
- all_eggs = Environment([dist_dir])
- eggs = [
- self.install_egg(dist.location, setup_base)
- for key in all_eggs
- for dist in all_eggs[key]
- ]
- if not eggs and not self.dry_run:
- log.warn("No eggs found in %s (setup script problem?)", dist_dir)
- return eggs
- finally:
- _rmtree(dist_dir)
- log.set_verbosity(self.verbose) # restore our log verbosity
-
- def _set_fetcher_options(self, base):
- """
- When easy_install is about to run bdist_egg on a source dist, that
- source dist might have 'setup_requires' directives, requiring
- additional fetching. Ensure the fetcher options given to easy_install
- are available to that command as well.
- """
- # find the fetch options from easy_install and write them out
- # to the setup.cfg file.
- ei_opts = self.distribution.get_option_dict('easy_install').copy()
- fetch_directives = (
- 'find_links',
- 'site_dirs',
- 'index_url',
- 'optimize',
- 'allow_hosts',
- )
- fetch_options = {}
- for key, val in ei_opts.items():
- if key not in fetch_directives:
- continue
- fetch_options[key] = val[1]
- # create a settings dictionary suitable for `edit_config`
- settings = dict(easy_install=fetch_options)
- cfg_filename = os.path.join(base, 'setup.cfg')
- setopt.edit_config(cfg_filename, settings)
-
- def update_pth(self, dist) -> None: # noqa: C901 # is too complex (11) # FIXME
- if self.pth_file is None:
- return
-
- for d in self.pth_file[dist.key]: # drop old entries
- if not self.multi_version and d.location == dist.location:
- continue
-
- log.info("Removing %s from easy-install.pth file", d)
- self.pth_file.remove(d)
- if d.location in self.shadow_path:
- self.shadow_path.remove(d.location)
-
- if not self.multi_version:
- if dist.location in self.pth_file.paths:
- log.info(
- "%s is already the active version in easy-install.pth",
- dist,
- )
- else:
- log.info("Adding %s to easy-install.pth file", dist)
- self.pth_file.add(dist) # add new entry
- if dist.location not in self.shadow_path:
- self.shadow_path.append(dist.location)
-
- if self.dry_run:
- return
-
- self.pth_file.save()
-
- if dist.key != 'setuptools':
- return
-
- # Ensure that setuptools itself never becomes unavailable!
- # XXX should this check for latest version?
- filename = os.path.join(self.install_dir, 'setuptools.pth')
- if os.path.islink(filename):
- os.unlink(filename)
-
- with open(filename, 'wt', encoding=py312.PTH_ENCODING) as f:
- # ^-- Python<3.13 require encoding="locale" instead of "utf-8",
- # see python/cpython#77102.
- f.write(self.pth_file.make_relative(dist.location) + '\n')
-
- def unpack_progress(self, src, dst):
- # Progress filter for unpacking
- log.debug("Unpacking %s to %s", src, dst)
- return dst # only unpack-and-compile skips files for dry run
-
- def unpack_and_compile(self, egg_path, destination) -> None:
- to_compile = []
- to_chmod = []
-
- def pf(src, dst):
- if dst.endswith('.py') and not src.startswith('EGG-INFO/'):
- to_compile.append(dst)
- elif dst.endswith('.dll') or dst.endswith('.so'):
- to_chmod.append(dst)
- self.unpack_progress(src, dst)
- return not self.dry_run and dst or None
-
- unpack_archive(egg_path, destination, pf)
- self.byte_compile(to_compile)
- if not self.dry_run:
- for f in to_chmod:
- mode = ((os.stat(f)[stat.ST_MODE]) | 0o555) & 0o7755
- chmod(f, mode)
-
- def byte_compile(self, to_compile) -> None:
- if sys.dont_write_bytecode:
- return
-
- from distutils.util import byte_compile
-
- try:
- # try to make the byte compile messages quieter
- log.set_verbosity(self.verbose - 1)
-
- byte_compile(to_compile, optimize=0, force=True, dry_run=self.dry_run)
- if self.optimize:
- byte_compile(
- to_compile,
- optimize=self.optimize,
- force=True,
- dry_run=self.dry_run,
- )
- finally:
- log.set_verbosity(self.verbose) # restore original verbosity
-
- __no_default_msg = textwrap.dedent(
- """
- bad install directory or PYTHONPATH
-
- You are attempting to install a package to a directory that is not
- on PYTHONPATH and which Python does not read ".pth" files from. The
- installation directory you specified (via --install-dir, --prefix, or
- the distutils default setting) was:
-
- %s
-
- and your PYTHONPATH environment variable currently contains:
-
- %r
-
- Here are some of your options for correcting the problem:
-
- * You can choose a different installation directory, i.e., one that is
- on PYTHONPATH or supports .pth files
-
- * You can add the installation directory to the PYTHONPATH environment
- variable. (It must then also be on PYTHONPATH whenever you run
- Python and want to use the package(s) you are installing.)
-
- * You can set up the installation directory to support ".pth" files by
- using one of the approaches described here:
-
- https://setuptools.pypa.io/en/latest/deprecated/easy_install.html#custom-installation-locations
-
-
- Please make the appropriate changes for your system and try again.
- """
- ).strip()
-
- def create_home_path(self) -> None:
- """Create directories under ~."""
- if not self.user:
- return
- home = convert_path(os.path.expanduser("~"))
- for path in only_strs(self.config_vars.values()):
- if path.startswith(home) and not os.path.isdir(path):
- self.debug_print(f"os.makedirs('{path}', 0o700)")
- os.makedirs(path, 0o700)
-
- INSTALL_SCHEMES = dict(
- posix=dict(
- install_dir='$base/lib/python$py_version_short/site-packages',
- script_dir='$base/bin',
- ),
- )
-
- DEFAULT_SCHEME = dict(
- install_dir='$base/Lib/site-packages',
- script_dir='$base/Scripts',
- )
-
- def _expand(self, *attrs):
- config_vars = self.get_finalized_command('install').config_vars
-
- if self.prefix:
- # Set default install_dir/scripts from --prefix
- config_vars = dict(config_vars)
- config_vars['base'] = self.prefix
- scheme = self.INSTALL_SCHEMES.get(os.name, self.DEFAULT_SCHEME)
- for attr, val in scheme.items():
- if getattr(self, attr, None) is None:
- setattr(self, attr, val)
-
- from distutils.util import subst_vars
-
- for attr in attrs:
- val = getattr(self, attr)
- if val is not None:
- val = subst_vars(val, config_vars)
- if os.name == 'posix':
- val = os.path.expanduser(val)
- setattr(self, attr, val)
-
-
-def _pythonpath():
- items = os.environ.get('PYTHONPATH', '').split(os.pathsep)
- return filter(None, items)
-
-
-def get_site_dirs():
- """
- Return a list of 'site' dirs
- """
-
- sitedirs = []
-
- # start with PYTHONPATH
- sitedirs.extend(_pythonpath())
-
- prefixes = [sys.prefix]
- if sys.exec_prefix != sys.prefix:
- prefixes.append(sys.exec_prefix)
- for prefix in prefixes:
- if not prefix:
- continue
-
- if sys.platform in ('os2emx', 'riscos'):
- sitedirs.append(os.path.join(prefix, "Lib", "site-packages"))
- elif os.sep == '/':
- sitedirs.extend([
- os.path.join(
- prefix,
- "lib",
- f"python{sys.version_info.major}.{sys.version_info.minor}",
- "site-packages",
- ),
- os.path.join(prefix, "lib", "site-python"),
- ])
- else:
- sitedirs.extend([
- prefix,
- os.path.join(prefix, "lib", "site-packages"),
- ])
- if sys.platform != 'darwin':
- continue
-
- # for framework builds *only* we add the standard Apple
- # locations. Currently only per-user, but /Library and
- # /Network/Library could be added too
- if 'Python.framework' not in prefix:
- continue
-
- home = os.environ.get('HOME')
- if not home:
- continue
-
- home_sp = os.path.join(
- home,
- 'Library',
- 'Python',
- f'{sys.version_info.major}.{sys.version_info.minor}',
- 'site-packages',
- )
- sitedirs.append(home_sp)
- lib_paths = get_path('purelib'), get_path('platlib')
-
- sitedirs.extend(s for s in lib_paths if s not in sitedirs)
-
- if site.ENABLE_USER_SITE:
- sitedirs.append(site.USER_SITE)
-
- with contextlib.suppress(AttributeError):
- sitedirs.extend(site.getsitepackages())
-
- return list(map(normalize_path, sitedirs))
-
-
-def expand_paths(inputs): # noqa: C901 # is too complex (11) # FIXME
- """Yield sys.path directories that might contain "old-style" packages"""
-
- seen = set()
-
- for dirname in inputs:
- dirname = normalize_path(dirname)
- if dirname in seen:
- continue
-
- seen.add(dirname)
- if not os.path.isdir(dirname):
- continue
-
- files = os.listdir(dirname)
- yield dirname, files
-
- for name in files:
- if not name.endswith('.pth'):
- # We only care about the .pth files
- continue
- if name in ('easy-install.pth', 'setuptools.pth'):
- # Ignore .pth files that we control
- continue
-
- # Read the .pth file
- content = _read_pth(os.path.join(dirname, name))
- lines = list(yield_lines(content))
-
- # Yield existing non-dupe, non-import directory lines from it
- for line in lines:
- if line.startswith("import"):
- continue
-
- line = normalize_path(line.rstrip())
- if line in seen:
- continue
-
- seen.add(line)
- if not os.path.isdir(line):
- continue
-
- yield line, os.listdir(line)
-
-
-def extract_wininst_cfg(dist_filename):
- """Extract configuration data from a bdist_wininst .exe
-
- Returns a configparser.RawConfigParser, or None
- """
- f = open(dist_filename, 'rb')
- try:
- endrec = zipfile._EndRecData(f)
- if endrec is None:
- return None
-
- prepended = (endrec[9] - endrec[5]) - endrec[6]
- if prepended < 12: # no wininst data here
- return None
- f.seek(prepended - 12)
-
- tag, cfglen, _bmlen = struct.unpack("<iii", f.read(12))
- if tag not in (0x1234567A, 0x1234567B):
- return None # not a valid tag
-
- f.seek(prepended - (12 + cfglen))
- init = {'version': '', 'target_version': ''}
- cfg = configparser.RawConfigParser(init)
- try:
- part = f.read(cfglen)
- # Read up to the first null byte.
- config = part.split(b'\0', 1)[0]
- # Now the config is in bytes, but for RawConfigParser, it should
- # be text, so decode it.
- config = config.decode(sys.getfilesystemencoding())
- cfg.read_file(io.StringIO(config))
- except configparser.Error:
- return None
- if not cfg.has_section('metadata') or not cfg.has_section('Setup'):
- return None
- return cfg
-
- finally:
- f.close()
-
-
-def get_exe_prefixes(exe_filename):
- """Get exe->egg path translations for a given .exe file"""
-
- prefixes = [
- ('PURELIB/', ''),
- ('PLATLIB/pywin32_system32', ''),
- ('PLATLIB/', ''),
- ('SCRIPTS/', 'EGG-INFO/scripts/'),
- ('DATA/lib/site-packages', ''),
- ]
- z = zipfile.ZipFile(exe_filename)
- try:
- for info in z.infolist():
- name = info.filename
- parts = name.split('/')
- if len(parts) == 3 and parts[2] == 'PKG-INFO':
- if parts[1].endswith('.egg-info'):
- prefixes.insert(0, ('/'.join(parts[:2]), 'EGG-INFO/'))
- break
- if len(parts) != 2 or not name.endswith('.pth'):
- continue
- if name.endswith('-nspkg.pth'):
- continue
- if parts[0].upper() in ('PURELIB', 'PLATLIB'):
- contents = z.read(name).decode()
- for pth in yield_lines(contents):
- pth = pth.strip().replace('\\', '/')
- if not pth.startswith('import'):
- prefixes.append(((f'{parts[0]}/{pth}/'), ''))
- finally:
- z.close()
- prefixes = [(x.lower(), y) for x, y in prefixes]
- prefixes.sort()
- prefixes.reverse()
- return prefixes
-
-
-class PthDistributions(Environment):
- """A .pth file with Distribution paths in it"""
-
- def __init__(self, filename, sitedirs=()) -> None:
- self.filename = filename
- self.sitedirs = list(map(normalize_path, sitedirs))
- self.basedir = normalize_path(os.path.dirname(self.filename))
- self.paths, self.dirty = self._load()
- # keep a copy if someone manually updates the paths attribute on the instance
- self._init_paths = self.paths[:]
- super().__init__([], None, None)
- for path in yield_lines(self.paths):
- list(map(self.add, find_distributions(path, True)))
-
- def _load_raw(self):
- paths = []
- dirty = saw_import = False
- seen = set(self.sitedirs)
- content = _read_pth(self.filename)
- for line in content.splitlines():
- path = line.rstrip()
- # still keep imports and empty/commented lines for formatting
- paths.append(path)
- if line.startswith(('import ', 'from ')):
- saw_import = True
- continue
- stripped_path = path.strip()
- if not stripped_path or stripped_path.startswith('#'):
- continue
- # skip non-existent paths, in case somebody deleted a package
- # manually, and duplicate paths as well
- normalized_path = normalize_path(os.path.join(self.basedir, path))
- if normalized_path in seen or not os.path.exists(normalized_path):
- log.debug("cleaned up dirty or duplicated %r", path)
- dirty = True
- paths.pop()
- continue
- seen.add(normalized_path)
- # remove any trailing empty/blank line
- while paths and not paths[-1].strip():
- paths.pop()
- dirty = True
- return paths, dirty or (paths and saw_import)
-
- def _load(self):
- if os.path.isfile(self.filename):
- return self._load_raw()
- return [], False
-
- def save(self) -> None:
- """Write changed .pth file back to disk"""
- # first reload the file
- last_paths, last_dirty = self._load()
- # and check that there are no difference with what we have.
- # there can be difference if someone else has written to the file
- # since we first loaded it.
- # we don't want to lose the eventual new paths added since then.
- for path in last_paths[:]:
- if path not in self.paths:
- self.paths.append(path)
- log.info("detected new path %r", path)
- last_dirty = True
- else:
- last_paths.remove(path)
- # also, re-check that all paths are still valid before saving them
- for path in self.paths[:]:
- if path not in last_paths and not path.startswith((
- 'import ',
- 'from ',
- '#',
- )):
- absolute_path = os.path.join(self.basedir, path)
- if not os.path.exists(absolute_path):
- self.paths.remove(path)
- log.info("removing now non-existent path %r", path)
- last_dirty = True
-
- self.dirty |= last_dirty or self.paths != self._init_paths
- if not self.dirty:
- return
-
- rel_paths = list(map(self.make_relative, self.paths))
- if rel_paths:
- log.debug("Saving %s", self.filename)
- lines = self._wrap_lines(rel_paths)
- data = '\n'.join(lines) + '\n'
- if os.path.islink(self.filename):
- os.unlink(self.filename)
- with open(self.filename, 'wt', encoding=py312.PTH_ENCODING) as f:
- # ^-- Python<3.13 require encoding="locale" instead of "utf-8",
- # see python/cpython#77102.
- f.write(data)
- elif os.path.exists(self.filename):
- log.debug("Deleting empty %s", self.filename)
- os.unlink(self.filename)
-
- self.dirty = False
- self._init_paths[:] = self.paths[:]
-
- @staticmethod
- def _wrap_lines(lines):
- return lines
-
- def add(self, dist) -> None:
- """Add `dist` to the distribution map"""
- new_path = dist.location not in self.paths and (
- dist.location not in self.sitedirs
- or
- # account for '.' being in PYTHONPATH
- dist.location == os.getcwd()
- )
- if new_path:
- self.paths.append(dist.location)
- self.dirty = True
- super().add(dist)
-
- def remove(self, dist) -> None:
- """Remove `dist` from the distribution map"""
- while dist.location in self.paths:
- self.paths.remove(dist.location)
- self.dirty = True
- super().remove(dist)
-
- def make_relative(self, path):
- npath, last = os.path.split(normalize_path(path))
- baselen = len(self.basedir)
- parts = [last]
- sep = os.altsep == '/' and '/' or os.sep
- while len(npath) >= baselen:
- if npath == self.basedir:
- parts.append(os.curdir)
- parts.reverse()
- return sep.join(parts)
- npath, last = os.path.split(npath)
- parts.append(last)
- else:
- return path
-
-
-class RewritePthDistributions(PthDistributions):
- @classmethod
- def _wrap_lines(cls, lines):
- yield cls.prelude
- yield from lines
- yield cls.postlude
-
- prelude = _one_liner(
- """
- import sys
- sys.__plen = len(sys.path)
- """
- )
- postlude = _one_liner(
- """
- import sys
- new = sys.path[sys.__plen:]
- del sys.path[sys.__plen:]
- p = getattr(sys, '__egginsert', 0)
- sys.path[p:p] = new
- sys.__egginsert = p + len(new)
- """
- )
-
-
-if os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite':
- PthDistributions = RewritePthDistributions # type: ignore[misc] # Overwriting type
-
-
-def _first_line_re():
- """
- Return a regular expression based on first_line_re suitable for matching
- strings.
- """
- if isinstance(first_line_re.pattern, str):
- return first_line_re
-
- # first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern.
- return re.compile(first_line_re.pattern.decode())
-
-
-def update_dist_caches(dist_path, fix_zipimporter_caches):
- """
- Fix any globally cached `dist_path` related data
-
- `dist_path` should be a path of a newly installed egg distribution (zipped
- or unzipped).
-
- sys.path_importer_cache contains finder objects that have been cached when
- importing data from the original distribution. Any such finders need to be
- cleared since the replacement distribution might be packaged differently,
- e.g. a zipped egg distribution might get replaced with an unzipped egg
- folder or vice versa. Having the old finders cached may then cause Python
- to attempt loading modules from the replacement distribution using an
- incorrect loader.
-
- zipimport.zipimporter objects are Python loaders charged with importing
- data packaged inside zip archives. If stale loaders referencing the
- original distribution, are left behind, they can fail to load modules from
- the replacement distribution. E.g. if an old zipimport.zipimporter instance
- is used to load data from a new zipped egg archive, it may cause the
- operation to attempt to locate the requested data in the wrong location -
- one indicated by the original distribution's zip archive directory
- information. Such an operation may then fail outright, e.g. report having
- read a 'bad local file header', or even worse, it may fail silently &
- return invalid data.
-
- zipimport._zip_directory_cache contains cached zip archive directory
- information for all existing zipimport.zipimporter instances and all such
- instances connected to the same archive share the same cached directory
- information.
-
- If asked, and the underlying Python implementation allows it, we can fix
- all existing zipimport.zipimporter instances instead of having to track
- them down and remove them one by one, by updating their shared cached zip
- archive directory information. This, of course, assumes that the
- replacement distribution is packaged as a zipped egg.
-
- If not asked to fix existing zipimport.zipimporter instances, we still do
- our best to clear any remaining zipimport.zipimporter related cached data
- that might somehow later get used when attempting to load data from the new
- distribution and thus cause such load operations to fail. Note that when
- tracking down such remaining stale data, we can not catch every conceivable
- usage from here, and we clear only those that we know of and have found to
- cause problems if left alive. Any remaining caches should be updated by
- whomever is in charge of maintaining them, i.e. they should be ready to
- handle us replacing their zip archives with new distributions at runtime.
-
- """
- # There are several other known sources of stale zipimport.zipimporter
- # instances that we do not clear here, but might if ever given a reason to
- # do so:
- # * Global setuptools pkg_resources.working_set (a.k.a. 'master working
- # set') may contain distributions which may in turn contain their
- # zipimport.zipimporter loaders.
- # * Several zipimport.zipimporter loaders held by local variables further
- # up the function call stack when running the setuptools installation.
- # * Already loaded modules may have their __loader__ attribute set to the
- # exact loader instance used when importing them. Python 3.4 docs state
- # that this information is intended mostly for introspection and so is
- # not expected to cause us problems.
- normalized_path = normalize_path(dist_path)
- _uncache(normalized_path, sys.path_importer_cache)
- if fix_zipimporter_caches:
- _replace_zip_directory_cache_data(normalized_path)
- else:
- # Here, even though we do not want to fix existing and now stale
- # zipimporter cache information, we still want to remove it. Related to
- # Python's zip archive directory information cache, we clear each of
- # its stale entries in two phases:
- # 1. Clear the entry so attempting to access zip archive information
- # via any existing stale zipimport.zipimporter instances fails.
- # 2. Remove the entry from the cache so any newly constructed
- # zipimport.zipimporter instances do not end up using old stale
- # zip archive directory information.
- # This whole stale data removal step does not seem strictly necessary,
- # but has been left in because it was done before we started replacing
- # the zip archive directory information cache content if possible, and
- # there are no relevant unit tests that we can depend on to tell us if
- # this is really needed.
- _remove_and_clear_zip_directory_cache_data(normalized_path)
-
-
-def _collect_zipimporter_cache_entries(normalized_path, cache):
- """
- Return zipimporter cache entry keys related to a given normalized path.
-
- Alternative path spellings (e.g. those using different character case or
- those using alternative path separators) related to the same path are
- included. Any sub-path entries are included as well, i.e. those
- corresponding to zip archives embedded in other zip archives.
-
- """
- result = []
- prefix_len = len(normalized_path)
- for p in cache:
- np = normalize_path(p)
- if np.startswith(normalized_path) and np[prefix_len : prefix_len + 1] in (
- os.sep,
- '',
- ):
- result.append(p)
- return result
-
-
-def _update_zipimporter_cache(normalized_path, cache, updater=None):
- """
- Update zipimporter cache data for a given normalized path.
-
- Any sub-path entries are processed as well, i.e. those corresponding to zip
- archives embedded in other zip archives.
-
- Given updater is a callable taking a cache entry key and the original entry
- (after already removing the entry from the cache), and expected to update
- the entry and possibly return a new one to be inserted in its place.
- Returning None indicates that the entry should not be replaced with a new
- one. If no updater is given, the cache entries are simply removed without
- any additional processing, the same as if the updater simply returned None.
-
- """
- for p in _collect_zipimporter_cache_entries(normalized_path, cache):
- # N.B. pypy's custom zipimport._zip_directory_cache implementation does
- # not support the complete dict interface:
- # * Does not support item assignment, thus not allowing this function
- # to be used only for removing existing cache entries.
- # * Does not support the dict.pop() method, forcing us to use the
- # get/del patterns instead. For more detailed information see the
- # following links:
- # https://github.com/pypa/setuptools/issues/202#issuecomment-202913420
- # https://foss.heptapod.net/pypy/pypy/-/blob/144c4e65cb6accb8e592f3a7584ea38265d1873c/pypy/module/zipimport/interp_zipimport.py
- old_entry = cache[p]
- del cache[p]
- new_entry = updater and updater(p, old_entry)
- if new_entry is not None:
- cache[p] = new_entry
-
-
-def _uncache(normalized_path, cache):
- _update_zipimporter_cache(normalized_path, cache)
-
-
-def _remove_and_clear_zip_directory_cache_data(normalized_path):
- def clear_and_remove_cached_zip_archive_directory_data(path, old_entry):
- old_entry.clear()
-
- _update_zipimporter_cache(
- normalized_path,
- zipimport._zip_directory_cache,
- updater=clear_and_remove_cached_zip_archive_directory_data,
+ warnings.SetuptoolsDeprecationWarning.emit(
+ summary="easy_install module is deprecated",
+ details="Avoid accessing attributes of setuptools.command.easy_install.",
+ due_date=(2025, 10, 31),
+ see_url="https://github.com/pypa/setuptools/issues/4976",
)
-
-
-# PyPy Python implementation does not allow directly writing to the
-# zipimport._zip_directory_cache and so prevents us from attempting to correct
-# its content. The best we can do there is clear the problematic cache content
-# and have PyPy repopulate it as needed. The downside is that if there are any
-# stale zipimport.zipimporter instances laying around, attempting to use them
-# will fail due to not having its zip archive directory information available
-# instead of being automatically corrected to use the new correct zip archive
-# directory information.
-if '__pypy__' in sys.builtin_module_names:
- _replace_zip_directory_cache_data = _remove_and_clear_zip_directory_cache_data
-else:
-
- def _replace_zip_directory_cache_data(normalized_path):
- def replace_cached_zip_archive_directory_data(path, old_entry):
- # N.B. In theory, we could load the zip directory information just
- # once for all updated path spellings, and then copy it locally and
- # update its contained path strings to contain the correct
- # spelling, but that seems like a way too invasive move (this cache
- # structure is not officially documented anywhere and could in
- # theory change with new Python releases) for no significant
- # benefit.
- old_entry.clear()
- zipimport.zipimporter(path)
- old_entry.update(zipimport._zip_directory_cache[path])
- return old_entry
-
- _update_zipimporter_cache(
- normalized_path,
- zipimport._zip_directory_cache,
- updater=replace_cached_zip_archive_directory_data,
- )
-
-
-def is_python(text, filename='<string>'):
- "Is this string a valid Python script?"
- try:
- compile(text, filename, 'exec')
- except (SyntaxError, TypeError):
- return False
- else:
- return True
-
-
-def is_sh(executable):
- """Determine if the specified executable is a .sh (contains a #! line)"""
- try:
- with open(executable, encoding='latin-1') as fp:
- magic = fp.read(2)
- except OSError:
- return executable
- return magic == '#!'
-
-
-def nt_quote_arg(arg):
- """Quote a command line argument according to Windows parsing rules"""
- return subprocess.list2cmdline([arg])
-
-
-def is_python_script(script_text, filename):
- """Is this text, as a whole, a Python script? (as opposed to shell/bat/etc."""
- if filename.endswith('.py') or filename.endswith('.pyw'):
- return True # extension says it's Python
- if is_python(script_text, filename):
- return True # it's syntactically valid Python
- if script_text.startswith('#!'):
- # It begins with a '#!' line, so check if 'python' is in it somewhere
- return 'python' in script_text.splitlines()[0].lower()
-
- return False # Not any Python I can recognize
-
-
-class _SplitArgs(TypedDict, total=False):
- comments: bool
- posix: bool
-
-
-class CommandSpec(list):
- """
- A command spec for a #! header, specified as a list of arguments akin to
- those passed to Popen.
- """
-
- options: list[str] = []
- split_args = _SplitArgs()
-
- @classmethod
- def best(cls):
- """
- Choose the best CommandSpec class based on environmental conditions.
- """
- return cls
-
- @classmethod
- def _sys_executable(cls):
- _default = os.path.normpath(sys.executable)
- return os.environ.get('__PYVENV_LAUNCHER__', _default)
-
- @classmethod
- def from_param(cls, param: Self | str | Iterable[str] | None) -> Self:
- """
- Construct a CommandSpec from a parameter to build_scripts, which may
- be None.
- """
- if isinstance(param, cls):
- return param
- if isinstance(param, str):
- return cls.from_string(param)
- if isinstance(param, Iterable):
- return cls(param)
- if param is None:
- return cls.from_environment()
- raise TypeError(f"Argument has an unsupported type {type(param)}")
-
- @classmethod
- def from_environment(cls):
- return cls([cls._sys_executable()])
-
- @classmethod
- def from_string(cls, string: str) -> Self:
- """
- Construct a command spec from a simple string representing a command
- line parseable by shlex.split.
- """
- items = shlex.split(string, **cls.split_args)
- return cls(items)
-
- def install_options(self, script_text: str):
- self.options = shlex.split(self._extract_options(script_text))
- cmdline = subprocess.list2cmdline(self)
- if not isascii(cmdline):
- self.options[:0] = ['-x']
-
- @staticmethod
- def _extract_options(orig_script):
- """
- Extract any options from the first line of the script.
- """
- first = (orig_script + '\n').splitlines()[0]
- match = _first_line_re().match(first)
- options = match.group(1) or '' if match else ''
- return options.strip()
-
- def as_header(self):
- return self._render(self + list(self.options))
-
- @staticmethod
- def _strip_quotes(item):
- _QUOTES = '"\''
- for q in _QUOTES:
- if item.startswith(q) and item.endswith(q):
- return item[1:-1]
- return item
-
- @staticmethod
- def _render(items):
- cmdline = subprocess.list2cmdline(
- CommandSpec._strip_quotes(item.strip()) for item in items
- )
- return '#!' + cmdline + '\n'
-
-
-# For pbr compat; will be removed in a future version.
-sys_executable = CommandSpec._sys_executable()
-
-
-class WindowsCommandSpec(CommandSpec):
- split_args = _SplitArgs(posix=False)
-
-
-class ScriptWriter:
- """
- Encapsulates behavior around writing entry point scripts for console and
- gui apps.
- """
-
- template = textwrap.dedent(
- r"""
- # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
- import re
- import sys
-
- # for compatibility with easy_install; see #2198
- __requires__ = %(spec)r
-
- try:
- from importlib.metadata import distribution
- except ImportError:
- try:
- from importlib_metadata import distribution
- except ImportError:
- from pkg_resources import load_entry_point
-
-
- def importlib_load_entry_point(spec, group, name):
- dist_name, _, _ = spec.partition('==')
- matches = (
- entry_point
- for entry_point in distribution(dist_name).entry_points
- if entry_point.group == group and entry_point.name == name
- )
- return next(matches).load()
-
-
- globals().setdefault('load_entry_point', importlib_load_entry_point)
-
-
- if __name__ == '__main__':
- sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
- sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)())
- """
- ).lstrip()
-
- command_spec_class = CommandSpec
-
- @classmethod
- def get_args(cls, dist, header=None):
- """
- Yield write_script() argument tuples for a distribution's
- console_scripts and gui_scripts entry points.
- """
- if header is None:
- header = cls.get_header()
- spec = str(dist.as_requirement())
- for type_ in 'console', 'gui':
- group = type_ + '_scripts'
- for name in dist.get_entry_map(group).keys():
- cls._ensure_safe_name(name)
- script_text = cls.template % locals()
- args = cls._get_script_args(type_, name, header, script_text)
- yield from args
-
- @staticmethod
- def _ensure_safe_name(name):
- """
- Prevent paths in *_scripts entry point names.
- """
- has_path_sep = re.search(r'[\\/]', name)
- if has_path_sep:
- raise ValueError("Path separators not allowed in script names")
-
- @classmethod
- def best(cls):
- """
- Select the best ScriptWriter for this environment.
- """
- if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):
- return WindowsScriptWriter.best()
- else:
- return cls
-
- @classmethod
- def _get_script_args(cls, type_, name, header, script_text):
- # Simply write the stub with no extension.
- yield (name, header + script_text)
-
- @classmethod
- def get_header(
- cls,
- script_text: str = "",
- executable: str | CommandSpec | Iterable[str] | None = None,
- ) -> str:
- """Create a #! line, getting options (if any) from script_text"""
- cmd = cls.command_spec_class.best().from_param(executable)
- cmd.install_options(script_text)
- return cmd.as_header()
-
-
-class WindowsScriptWriter(ScriptWriter):
- command_spec_class = WindowsCommandSpec
-
- @classmethod
- def best(cls):
- """
- Select the best ScriptWriter suitable for Windows
- """
- writer_lookup = dict(
- executable=WindowsExecutableLauncherWriter,
- natural=cls,
- )
- # for compatibility, use the executable launcher by default
- launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable')
- return writer_lookup[launcher]
-
- @classmethod
- def _get_script_args(cls, type_, name, header, script_text):
- "For Windows, add a .py extension"
- ext = dict(console='.pya', gui='.pyw')[type_]
- if ext not in os.environ['PATHEXT'].lower().split(';'):
- msg = (
- "{ext} not listed in PATHEXT; scripts will not be "
- "recognized as executables."
- ).format(**locals())
- SetuptoolsWarning.emit(msg)
- old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
- old.remove(ext)
- header = cls._adjust_header(type_, header)
- blockers = [name + x for x in old]
- yield name + ext, header + script_text, 't', blockers
-
- @classmethod
- def _adjust_header(cls, type_, orig_header):
- """
- Make sure 'pythonw' is used for gui and 'python' is used for
- console (regardless of what sys.executable is).
- """
- pattern = 'pythonw.exe'
- repl = 'python.exe'
- if type_ == 'gui':
- pattern, repl = repl, pattern
- pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)
- new_header = pattern_ob.sub(string=orig_header, repl=repl)
- return new_header if cls._use_header(new_header) else orig_header
-
- @staticmethod
- def _use_header(new_header):
- """
- Should _adjust_header use the replaced header?
-
- On non-windows systems, always use. On
- Windows systems, only use the replaced header if it resolves
- to an executable on the system.
- """
- clean_header = new_header[2:-1].strip('"')
- return sys.platform != 'win32' or shutil.which(clean_header)
-
-
-class WindowsExecutableLauncherWriter(WindowsScriptWriter):
- @classmethod
- def _get_script_args(cls, type_, name, header, script_text):
- """
- For Windows, add a .py extension and an .exe launcher
- """
- if type_ == 'gui':
- launcher_type = 'gui'
- ext = '-script.pyw'
- old = ['.pyw']
- else:
- launcher_type = 'cli'
- ext = '-script.py'
- old = ['.py', '.pyc', '.pyo']
- hdr = cls._adjust_header(type_, header)
- blockers = [name + x for x in old]
- yield (name + ext, hdr + script_text, 't', blockers)
- yield (
- name + '.exe',
- get_win_launcher(launcher_type),
- 'b', # write in binary mode
- )
- if not is_64bit():
- # install a manifest for the launcher to prevent Windows
- # from detecting it as an installer (which it will for
- # launchers like easy_install.exe). Consider only
- # adding a manifest for launchers detected as installers.
- # See Distribute #143 for details.
- m_name = name + '.exe.manifest'
- yield (m_name, load_launcher_manifest(name), 't')
-
-
-def get_win_launcher(type):
- """
- Load the Windows launcher (executable) suitable for launching a script.
-
- `type` should be either 'cli' or 'gui'
-
- Returns the executable as a byte string.
- """
- launcher_fn = f'{type}.exe'
- if is_64bit():
- if get_platform() == "win-arm64":
- launcher_fn = launcher_fn.replace(".", "-arm64.")
- else:
- launcher_fn = launcher_fn.replace(".", "-64.")
- else:
- launcher_fn = launcher_fn.replace(".", "-32.")
- return resource_string('setuptools', launcher_fn)
-
-
-def load_launcher_manifest(name):
- manifest = pkg_resources.resource_string(__name__, 'launcher manifest.xml')
- return manifest.decode('utf-8') % vars()
-
-
-def current_umask():
- tmp = os.umask(0o022)
- os.umask(tmp)
- return tmp
-
-
-def only_strs(values):
- """
- Exclude non-str values. Ref #3063.
- """
- return filter(lambda val: isinstance(val, str), values)
-
-
-def _read_pth(fullname: str) -> str:
- # Python<3.13 require encoding="locale" instead of "utf-8", see python/cpython#77102
- # In the case old versions of setuptools are producing `pth` files with
- # different encodings that might be problematic... So we fallback to "locale".
-
- try:
- with open(fullname, encoding=py312.PTH_ENCODING) as f:
- return f.read()
- except UnicodeDecodeError: # pragma: no cover
- # This error may only happen for Python >= 3.13
- # TODO: Possible deprecation warnings to be added in the future:
- # ``.pth file {fullname!r} is not UTF-8.``
- # Your environment contain {fullname!r} that cannot be read as UTF-8.
- # This is likely to have been produced with an old version of setuptools.
- # Please be mindful that this is deprecated and in the future, non-utf8
- # .pth files may cause setuptools to fail.
- with open(fullname, encoding=py39.LOCALE_ENCODING) as f:
- return f.read()
-
-
-class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning):
- _SUMMARY = "easy_install command is deprecated."
- _DETAILS = """
- Please avoid running ``setup.py`` and ``easy_install``.
- Instead, use pypa/build, pypa/installer or other
- standards-based tools.
- """
- _SEE_URL = "https://github.com/pypa/setuptools/issues/917"
- # _DUE_DATE not defined yet
+ return attr
diff --git a/contrib/python/setuptools/py3/setuptools/command/editable_wheel.py b/contrib/python/setuptools/py3/setuptools/command/editable_wheel.py
index 1a544ec2581..c7725708175 100644
--- a/contrib/python/setuptools/py3/setuptools/command/editable_wheel.py
+++ b/contrib/python/setuptools/py3/setuptools/command/editable_wheel.py
@@ -29,10 +29,10 @@ from typing import TYPE_CHECKING, Protocol, TypeVar, cast
from .. import Command, _normalization, _path, _shutil, errors, namespaces
from .._path import StrPath
-from ..compat import py312
+from ..compat import py310, py312
from ..discovery import find_package_path
from ..dist import Distribution
-from ..warnings import InformationOnly, SetuptoolsDeprecationWarning, SetuptoolsWarning
+from ..warnings import InformationOnly, SetuptoolsDeprecationWarning
from .build import build as build_cls
from .build_py import build_py as build_py_cls
from .dist_info import dist_info as dist_info_cls
@@ -137,10 +137,14 @@ class editable_wheel(Command):
bdist_wheel.write_wheelfile(self.dist_info_dir)
self._create_wheel_file(bdist_wheel)
- except Exception:
- traceback.print_exc()
+ except Exception as ex:
project = self.distribution.name or self.distribution.get_name()
- _DebuggingTips.emit(project=project)
+ py310.add_note(
+ ex,
+ f"An error occurred when building editable wheel for {project}.\n"
+ "See debugging tips in: "
+ "https://setuptools.pypa.io/en/latest/userguide/development_mode.html#debugging-tips",
+ )
raise
def _ensure_dist_info(self):
@@ -211,6 +215,11 @@ class editable_wheel(Command):
install.install_headers = headers
install.install_data = data
+ # For portability, ensure scripts are built with #!python shebang
+ # pypa/setuptools#4863
+ build_scripts = dist.get_command_obj("build_scripts")
+ build_scripts.executable = 'python'
+
install_scripts = cast(
install_scripts_cls, dist.get_command_obj("install_scripts")
)
@@ -897,29 +906,3 @@ def _finder_template(
class LinksNotSupported(errors.FileError):
"""File system does not seem to support either symlinks or hard links."""
-
-
-class _DebuggingTips(SetuptoolsWarning):
- _SUMMARY = "Problem in editable installation."
- _DETAILS = """
- An error happened while installing `{project}` in editable mode.
-
- The following steps are recommended to help debug this problem:
-
- - Try to install the project normally, without using the editable mode.
- Does the error still persist?
- (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 persists 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
- """
- _SEE_DOCS = "userguide/development_mode.html"
diff --git a/contrib/python/setuptools/py3/setuptools/command/egg_info.py b/contrib/python/setuptools/py3/setuptools/command/egg_info.py
index f77631168fc..7e00ae2cea7 100644
--- a/contrib/python/setuptools/py3/setuptools/command/egg_info.py
+++ b/contrib/python/setuptools/py3/setuptools/command/egg_info.py
@@ -655,8 +655,6 @@ def write_pkg_info(cmd, basename, filename) -> None:
metadata.name, oldname = cmd.egg_name, metadata.name
try:
- # write unescaped data to PKG-INFO, so older pkg_resources
- # can still parse it
metadata.write_pkg_info(cmd.egg_info)
finally:
metadata.name, metadata.version = oldname, oldver
diff --git a/contrib/python/setuptools/py3/setuptools/command/install.py b/contrib/python/setuptools/py3/setuptools/command/install.py
index 15ef3646888..19ca601458f 100644
--- a/contrib/python/setuptools/py3/setuptools/command/install.py
+++ b/contrib/python/setuptools/py3/setuptools/command/install.py
@@ -1,16 +1,12 @@
from __future__ import annotations
-import glob
import inspect
import platform
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any, ClassVar, cast
-
-import setuptools
+from typing import TYPE_CHECKING, Any, ClassVar
from ..dist import Distribution
from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
-from .bdist_egg import bdist_egg as bdist_egg_cls
import distutils.command.install as orig
from distutils.errors import DistutilsArgError
@@ -26,7 +22,7 @@ def __getattr__(name: str): # pragma: no cover
if name == "_install":
SetuptoolsDeprecationWarning.emit(
"`setuptools.command._install` was an internal implementation detail "
- + "that was left in for numpy<1.9 support.",
+ "that was left in for numpy<1.9 support.",
due_date=(2025, 5, 2), # Originally added on 2024-11-01
)
return orig.install
@@ -67,9 +63,7 @@ class install(orig.install):
standards-based tools.
""",
see_url="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html",
- # TODO: Document how to bootstrap setuptools without install
- # (e.g. by unzipping the wheel file)
- # and then add a due_date to this warning.
+ due_date=(2025, 10, 31),
)
super().initialize_options()
@@ -97,19 +91,6 @@ class install(orig.install):
self.extra_dirs = ''
return None
- def run(self):
- # Explicit request for old-style install? Just do it
- if self.old_and_unmanageable or self.single_version_externally_managed:
- return super().run()
-
- if not self._called_from_setup(inspect.currentframe()):
- # Run in backward-compatibility mode to support bdist_* commands.
- super().run()
- else:
- self.do_egg_install()
-
- return None
-
@staticmethod
def _called_from_setup(run_frame):
"""
@@ -143,39 +124,6 @@ class install(orig.install):
return False
- def do_egg_install(self) -> None:
- easy_install = self.distribution.get_command_class('easy_install')
-
- cmd = cast(
- # We'd want to cast easy_install as type[easy_install_cls] but a bug in
- # mypy makes it think easy_install() returns a Command on Python 3.12+
- # https://github.com/python/mypy/issues/18088
- easy_install_cls,
- easy_install( # type: ignore[call-arg]
- self.distribution,
- args="x",
- root=self.root,
- record=self.record,
- ),
- )
- cmd.ensure_finalized() # finalize before bdist_egg munges install cmd
- cmd.always_copy_from = '.' # make sure local-dir eggs get installed
-
- # pick up setup-dir .egg files only: no .egg-info
- cmd.package_index.scan(glob.glob('*.egg'))
-
- self.run_command('bdist_egg')
- bdist_egg = cast(bdist_egg_cls, self.distribution.get_command_obj('bdist_egg'))
- args = [bdist_egg.egg_output]
-
- if setuptools.bootstrap_install_from:
- # Bootstrap self-installation of setuptools
- args.insert(0, setuptools.bootstrap_install_from)
-
- cmd.args = args
- cmd.run(show_deprecation=False)
- setuptools.bootstrap_install_from = None
-
# XXX Python 3.1 doesn't see _nc if this is inside the class
install.sub_commands = [
diff --git a/contrib/python/setuptools/py3/setuptools/command/install_scripts.py b/contrib/python/setuptools/py3/setuptools/command/install_scripts.py
index 4401cf693d2..537181e3215 100644
--- a/contrib/python/setuptools/py3/setuptools/command/install_scripts.py
+++ b/contrib/python/setuptools/py3/setuptools/command/install_scripts.py
@@ -32,20 +32,14 @@ class install_scripts(orig.install_scripts):
def _install_ep_scripts(self):
# Delay import side-effects
- from pkg_resources import Distribution, PathMetadata
-
- from . import easy_install as ei
+ from .. import _scripts
+ from .._importlib import metadata
ei_cmd = self.get_finalized_command("egg_info")
- dist = Distribution(
- ei_cmd.egg_base,
- PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),
- ei_cmd.egg_name,
- ei_cmd.egg_version,
- )
+ dist = metadata.Distribution.at(path=ei_cmd.egg_info)
bs_cmd = self.get_finalized_command('build_scripts')
exec_param = getattr(bs_cmd, 'executable', None)
- writer = ei.ScriptWriter
+ writer = _scripts.ScriptWriter
if exec_param == sys.executable:
# In case the path to the Python executable contains a space, wrap
# it so it's not split up.
@@ -58,7 +52,7 @@ class install_scripts(orig.install_scripts):
def write_script(self, script_name, contents, mode: str = "t", *ignored) -> None:
"""Write an executable file to the scripts directory"""
- from setuptools.command.easy_install import chmod, current_umask
+ from .._shutil import attempt_chmod_verbose as chmod, current_umask
log.info("Installing %s script to %s", script_name, self.install_dir)
target = os.path.join(self.install_dir, script_name)
diff --git a/contrib/python/setuptools/py3/setuptools/command/sdist.py b/contrib/python/setuptools/py3/setuptools/command/sdist.py
index 9631cf31147..1aed1d5e4e6 100644
--- a/contrib/python/setuptools/py3/setuptools/command/sdist.py
+++ b/contrib/python/setuptools/py3/setuptools/command/sdist.py
@@ -30,7 +30,7 @@ class sdist(orig.sdist):
(
'keep-temp',
'k',
- "keep the distribution tree around after creating " + "archive file(s)",
+ "keep the distribution tree around after creating archive file(s)",
),
(
'dist-dir=',
diff --git a/contrib/python/setuptools/py3/setuptools/compat/py310.py b/contrib/python/setuptools/py3/setuptools/compat/py310.py
index b3912f8e02a..58a4d9f3660 100644
--- a/contrib/python/setuptools/py3/setuptools/compat/py310.py
+++ b/contrib/python/setuptools/py3/setuptools/compat/py310.py
@@ -7,3 +7,14 @@ if sys.version_info >= (3, 11):
import tomllib
else: # pragma: no cover
import tomli as tomllib
+
+
+if sys.version_info >= (3, 11):
+
+ def add_note(ex, note):
+ ex.add_note(note)
+
+else: # pragma: no cover
+
+ def add_note(ex, note):
+ vars(ex).setdefault('__notes__', []).append(note)
diff --git a/contrib/python/setuptools/py3/setuptools/dist.py b/contrib/python/setuptools/py3/setuptools/dist.py
index 8d972cc49bd..f06298c8681 100644
--- a/contrib/python/setuptools/py3/setuptools/dist.py
+++ b/contrib/python/setuptools/py3/setuptools/dist.py
@@ -46,8 +46,6 @@ from distutils.util import strtobool
if TYPE_CHECKING:
from typing_extensions import TypeAlias
- from pkg_resources import Distribution as _pkg_resources_Distribution
-
__all__ = ['Distribution']
@@ -471,12 +469,13 @@ class Distribution(_Distribution):
cls, patterns: list[str], enforce_match: bool = True
) -> Iterator[str]:
"""
- >>> list(Distribution._expand_patterns(['LICENSE']))
- ['LICENSE']
+ >>> getfixture('sample_project_cwd')
+ >>> list(Distribution._expand_patterns(['LICENSE.txt']))
+ ['LICENSE.txt']
>>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))
- ['pyproject.toml', 'LICENSE']
- >>> list(Distribution._expand_patterns(['setuptools/**/pyprojecttoml.py']))
- ['setuptools/config/pyprojecttoml.py']
+ ['pyproject.toml', 'LICENSE.txt']
+ >>> list(Distribution._expand_patterns(['src/**/*.dat']))
+ ['src/sample/package_data.dat']
"""
return (
path.replace(os.sep, "/")
@@ -488,8 +487,9 @@ class Distribution(_Distribution):
@staticmethod
def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]:
r"""
- >>> Distribution._find_pattern("LICENSE")
- ['LICENSE']
+ >>> getfixture('sample_project_cwd')
+ >>> Distribution._find_pattern("LICENSE.txt")
+ ['LICENSE.txt']
>>> Distribution._find_pattern("/LICENSE.MIT")
Traceback (most recent call last):
...
@@ -759,9 +759,7 @@ class Distribution(_Distribution):
self._finalize_license_expression()
self._finalize_license_files()
- def fetch_build_eggs(
- self, requires: _StrOrIter
- ) -> list[_pkg_resources_Distribution]:
+ def fetch_build_eggs(self, requires: _StrOrIter) -> list[metadata.Distribution]:
"""Resolve pre-setup requirements"""
from .installer import _fetch_build_eggs
diff --git a/contrib/python/setuptools/py3/setuptools/installer.py b/contrib/python/setuptools/py3/setuptools/installer.py
index 64bc2def078..2c26e3a1f4e 100644
--- a/contrib/python/setuptools/py3/setuptools/installer.py
+++ b/contrib/python/setuptools/py3/setuptools/installer.py
@@ -1,16 +1,17 @@
from __future__ import annotations
import glob
+import itertools
import os
import subprocess
import sys
import tempfile
-from functools import partial
-from pkg_resources import Distribution
+import packaging.requirements
+import packaging.utils
from . import _reqs
-from ._reqs import _StrOrIter
+from ._importlib import metadata
from .warnings import SetuptoolsDeprecationWarning
from .wheel import Wheel
@@ -35,25 +36,38 @@ def fetch_build_egg(dist, req):
return _fetch_build_egg_no_warn(dist, req)
-def _fetch_build_eggs(dist, requires: _StrOrIter) -> list[Distribution]:
- import pkg_resources # Delay import to avoid unnecessary side-effects
+def _present(req):
+ return any(_dist_matches_req(dist, req) for dist in metadata.distributions())
+
+def _fetch_build_eggs(dist, requires: _reqs._StrOrIter) -> list[metadata.Distribution]:
_DeprecatedInstaller.emit(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,
+ parsed_reqs = _reqs.parse(requires)
+
+ missing_reqs = itertools.filterfalse(_present, parsed_reqs)
+
+ needed_reqs = (
+ req for req in missing_reqs if not req.marker or req.marker.evaluate()
)
+ resolved_dists = [_fetch_build_egg_no_warn(dist, req) for req in needed_reqs]
for dist in resolved_dists:
- pkg_resources.working_set.add(dist, replace=True)
+ # dist.locate_file('') is the directory containing EGG-INFO, where the importabl
+ # contents can be found.
+ sys.path.insert(0, str(dist.locate_file('')))
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
+def _dist_matches_req(egg_dist, req):
+ return (
+ packaging.utils.canonicalize_name(egg_dist.name)
+ == packaging.utils.canonicalize_name(req.name)
+ and egg_dist.version in req.specifier
+ )
+
+def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) # FIXME
# Ignore environment markers; if supplied, it is required.
req = strip_marker(req)
# Take easy_install options into account, but do not override relevant
@@ -78,9 +92,9 @@ def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) #
if dist.dependency_links:
find_links.extend(dist.dependency_links)
eggs_dir = os.path.realpath(dist.get_egg_cache_dir())
- environment = pkg_resources.Environment()
- for egg_dist in pkg_resources.find_distributions(eggs_dir):
- if egg_dist in req and environment.can_add(egg_dist):
+ cached_dists = metadata.Distribution.discover(path=glob.glob(f'{eggs_dir}/*.egg'))
+ for egg_dist in cached_dists:
+ if _dist_matches_req(egg_dist, req):
return egg_dist
with tempfile.TemporaryDirectory() as tmpdir:
cmd = [
@@ -110,12 +124,7 @@ def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) #
wheel = Wheel(glob.glob(os.path.join(tmpdir, '*.whl'))[0])
dist_location = os.path.join(eggs_dir, wheel.egg_name())
wheel.install_as_egg(dist_location)
- dist_metadata = pkg_resources.PathMetadata(
- dist_location, os.path.join(dist_location, 'EGG-INFO')
- )
- return pkg_resources.Distribution.from_filename(
- dist_location, metadata=dist_metadata
- )
+ return metadata.Distribution.at(dist_location + '/EGG-INFO')
def strip_marker(req):
@@ -124,20 +133,16 @@ 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 = packaging.requirements.Requirement(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:
+ metadata.distribution('wheel')
+ except metadata.PackageNotFoundError:
dist.announce('WARNING: The wheel package is not available.', log.WARN)
@@ -147,4 +152,4 @@ class _DeprecatedInstaller(SetuptoolsDeprecationWarning):
Requirements should be satisfied by a PEP 517 installer.
If you are using pip, you can try `pip install --use-pep517`.
"""
- # _DUE_DATE not decided yet
+ _DUE_DATE = 2025, 10, 31
diff --git a/contrib/python/setuptools/py3/setuptools/package_index.py b/contrib/python/setuptools/py3/setuptools/package_index.py
deleted file mode 100644
index 3500c2d86f1..00000000000
--- a/contrib/python/setuptools/py3/setuptools/package_index.py
+++ /dev/null
@@ -1,1179 +0,0 @@
-"""PyPI and direct package downloading."""
-
-from __future__ import annotations
-
-import base64
-import configparser
-import hashlib
-import html
-import http.client
-import io
-import itertools
-import os
-import re
-import shutil
-import socket
-import subprocess
-import sys
-import urllib.error
-import urllib.parse
-import urllib.request
-from fnmatch import translate
-from functools import wraps
-from typing import NamedTuple
-
-from more_itertools import unique_everseen
-
-import setuptools
-from pkg_resources import (
- BINARY_DIST,
- CHECKOUT_DIST,
- DEVELOP_DIST,
- EGG_DIST,
- SOURCE_DIST,
- Distribution,
- Environment,
- Requirement,
- find_distributions,
- normalize_path,
- parse_version,
- safe_name,
- safe_version,
- to_filename,
-)
-from setuptools.wheel import Wheel
-
-from .unicode_utils import _cfg_read_utf8_with_fallback, _read_utf8_with_fallback
-
-from distutils import log
-from distutils.errors import DistutilsError
-
-EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
-HREF = re.compile(r"""href\s*=\s*['"]?([^'"> ]+)""", re.I)
-PYPI_MD5 = re.compile(
- r'<a href="([^"#]+)">([^<]+)</a>\n\s+\(<a (?:title="MD5 hash"\n\s+)'
- r'href="[^?]+\?:action=show_md5&amp;digest=([0-9a-f]{32})">md5</a>\)'
-)
-URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match
-EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split()
-
-__all__ = [
- 'PackageIndex',
- 'distros_for_url',
- 'parse_bdist_wininst',
- 'interpret_distro_name',
-]
-
-_SOCKET_TIMEOUT = 15
-
-user_agent = f"setuptools/{setuptools.__version__} Python-urllib/{sys.version_info.major}.{sys.version_info.minor}"
-
-
-def parse_requirement_arg(spec):
- try:
- return Requirement.parse(spec)
- except ValueError as e:
- raise DistutilsError(
- f"Not a URL, existing file, or requirement spec: {spec!r}"
- ) from e
-
-
-def parse_bdist_wininst(name):
- """Return (base,pyversion) or (None,None) for possible .exe name"""
-
- lower = name.lower()
- base, py_ver, plat = None, None, None
-
- if lower.endswith('.exe'):
- if lower.endswith('.win32.exe'):
- base = name[:-10]
- plat = 'win32'
- elif lower.startswith('.win32-py', -16):
- py_ver = name[-7:-4]
- base = name[:-16]
- plat = 'win32'
- elif lower.endswith('.win-amd64.exe'):
- base = name[:-14]
- plat = 'win-amd64'
- elif lower.startswith('.win-amd64-py', -20):
- py_ver = name[-7:-4]
- base = name[:-20]
- plat = 'win-amd64'
- return base, py_ver, plat
-
-
-def egg_info_for_url(url):
- parts = urllib.parse.urlparse(url)
- _scheme, server, path, _parameters, _query, fragment = parts
- base = urllib.parse.unquote(path.split('/')[-1])
- if server == 'sourceforge.net' and base == 'download': # XXX Yuck
- base = urllib.parse.unquote(path.split('/')[-2])
- if '#' in base:
- base, fragment = base.split('#', 1)
- return base, fragment
-
-
-def distros_for_url(url, metadata=None):
- """Yield egg or source distribution objects that might be found at a URL"""
- base, fragment = egg_info_for_url(url)
- yield from distros_for_location(url, base, metadata)
- if fragment:
- match = EGG_FRAGMENT.match(fragment)
- if match:
- yield from interpret_distro_name(
- url, match.group(1), metadata, precedence=CHECKOUT_DIST
- )
-
-
-def distros_for_location(location, basename, metadata=None):
- """Yield egg or source distribution objects based on basename"""
- if basename.endswith('.egg.zip'):
- basename = basename[:-4] # strip the .zip
- if basename.endswith('.egg') and '-' in basename:
- # only one, unambiguous interpretation
- return [Distribution.from_location(location, basename, metadata)]
- if basename.endswith('.whl') and '-' in basename:
- wheel = Wheel(basename)
- if not wheel.is_compatible():
- return []
- return [
- Distribution(
- location=location,
- project_name=wheel.project_name,
- version=wheel.version,
- # Increase priority over eggs.
- precedence=EGG_DIST + 1,
- )
- ]
- if basename.endswith('.exe'):
- win_base, py_ver, platform = parse_bdist_wininst(basename)
- if win_base is not None:
- return interpret_distro_name(
- location, win_base, metadata, py_ver, BINARY_DIST, platform
- )
- # Try source distro extensions (.zip, .tgz, etc.)
- #
- for ext in EXTENSIONS:
- if basename.endswith(ext):
- basename = basename[: -len(ext)]
- return interpret_distro_name(location, basename, metadata)
- return [] # no extension matched
-
-
-def distros_for_filename(filename, metadata=None):
- """Yield possible egg or source distribution objects based on a filename"""
- return distros_for_location(
- normalize_path(filename), os.path.basename(filename), metadata
- )
-
-
-def interpret_distro_name(
- location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None
-):
- """Generate the interpretation of a source distro name
-
- Note: if `location` is a filesystem filename, you should call
- ``pkg_resources.normalize_path()`` on it before passing it to this
- routine!
- """
-
- parts = basename.split('-')
- if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]):
- # it is a bdist_dumb, not an sdist -- bail out
- return
-
- # find the pivot (p) that splits the name from the version.
- # infer the version as the first item that has a digit.
- for p in range(len(parts)):
- if parts[p][:1].isdigit():
- break
- else:
- p = len(parts)
-
- yield Distribution(
- location,
- metadata,
- '-'.join(parts[:p]),
- '-'.join(parts[p:]),
- py_version=py_version,
- precedence=precedence,
- platform=platform,
- )
-
-
-def unique_values(func):
- """
- Wrap a function returning an iterable such that the resulting iterable
- only ever yields unique items.
- """
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- return unique_everseen(func(*args, **kwargs))
-
- return wrapper
-
-
-REL = re.compile(r"""<([^>]*\srel\s{0,10}=\s{0,10}['"]?([^'" >]+)[^>]*)>""", re.I)
-"""
-Regex for an HTML tag with 'rel="val"' attributes.
-"""
-
-
-@unique_values
-def find_external_links(url, page):
- """Find rel="homepage" and rel="download" links in `page`, yielding URLs"""
-
- for match in REL.finditer(page):
- tag, rel = match.groups()
- rels = set(map(str.strip, rel.lower().split(',')))
- if 'homepage' in rels or 'download' in rels:
- for match in HREF.finditer(tag):
- yield urllib.parse.urljoin(url, htmldecode(match.group(1)))
-
- for tag in ("<th>Home Page", "<th>Download URL"):
- pos = page.find(tag)
- if pos != -1:
- match = HREF.search(page, pos)
- if match:
- yield urllib.parse.urljoin(url, htmldecode(match.group(1)))
-
-
-class ContentChecker:
- """
- A null content checker that defines the interface for checking content
- """
-
- def feed(self, block):
- """
- Feed a block of data to the hash.
- """
- return
-
- def is_valid(self):
- """
- Check the hash. Return False if validation fails.
- """
- return True
-
- def report(self, reporter, template):
- """
- Call reporter with information about the checker (hash name)
- substituted into the template.
- """
- return
-
-
-class HashChecker(ContentChecker):
- pattern = re.compile(
- r'(?P<hash_name>sha1|sha224|sha384|sha256|sha512|md5)='
- r'(?P<expected>[a-f0-9]+)'
- )
-
- def __init__(self, hash_name, expected) -> None:
- self.hash_name = hash_name
- self.hash = hashlib.new(hash_name)
- self.expected = expected
-
- @classmethod
- def from_url(cls, url):
- "Construct a (possibly null) ContentChecker from a URL"
- fragment = urllib.parse.urlparse(url)[-1]
- if not fragment:
- return ContentChecker()
- match = cls.pattern.search(fragment)
- if not match:
- return ContentChecker()
- return cls(**match.groupdict())
-
- def feed(self, block):
- self.hash.update(block)
-
- def is_valid(self):
- return self.hash.hexdigest() == self.expected
-
- def report(self, reporter, template):
- msg = template % self.hash_name
- return reporter(msg)
-
-
-class PackageIndex(Environment):
- """A distribution index that scans web pages for download URLs"""
-
- def __init__(
- self,
- index_url: str = "https://pypi.org/simple/",
- hosts=('*',),
- ca_bundle=None,
- verify_ssl: bool = True,
- *args,
- **kw,
- ) -> None:
- super().__init__(*args, **kw)
- self.index_url = index_url + "/"[: not index_url.endswith('/')]
- self.scanned_urls: dict = {}
- self.fetched_urls: dict = {}
- self.package_pages: dict = {}
- self.allows = re.compile('|'.join(map(translate, hosts))).match
- self.to_scan: list = []
- self.opener = urllib.request.urlopen
-
- def add(self, dist):
- # ignore invalid versions
- try:
- parse_version(dist.version)
- except Exception:
- return None
- return super().add(dist)
-
- # FIXME: 'PackageIndex.process_url' is too complex (14)
- def process_url(self, url, retrieve: bool = False) -> None: # noqa: C901
- """Evaluate a URL as a possible download, and maybe retrieve it"""
- if url in self.scanned_urls and not retrieve:
- return
- self.scanned_urls[url] = True
- if not URL_SCHEME(url):
- self.process_filename(url)
- return
- else:
- dists = list(distros_for_url(url))
- if dists:
- if not self.url_ok(url):
- return
- self.debug("Found link: %s", url)
-
- if dists or not retrieve or url in self.fetched_urls:
- list(map(self.add, dists))
- return # don't need the actual page
-
- if not self.url_ok(url):
- self.fetched_urls[url] = True
- return
-
- self.info("Reading %s", url)
- self.fetched_urls[url] = True # prevent multiple fetch attempts
- tmpl = "Download error on %s: %%s -- Some packages may not be found!"
- f = self.open_url(url, tmpl % url)
- if f is None:
- return
- if isinstance(f, urllib.error.HTTPError) and f.code == 401:
- self.info(f"Authentication error: {f.msg}")
- self.fetched_urls[f.url] = True
- if 'html' not in f.headers.get('content-type', '').lower():
- f.close() # not html, we can't process it
- return
-
- base = f.url # handle redirects
- page = f.read()
- if not isinstance(page, str):
- # In Python 3 and got bytes but want str.
- if isinstance(f, urllib.error.HTTPError):
- # Errors have no charset, assume latin1:
- charset = 'latin-1'
- else:
- charset = f.headers.get_param('charset') or 'latin-1'
- page = page.decode(charset, "ignore")
- f.close()
- for match in HREF.finditer(page):
- link = urllib.parse.urljoin(base, htmldecode(match.group(1)))
- self.process_url(link)
- if url.startswith(self.index_url) and getattr(f, 'code', None) != 404:
- page = self.process_index(url, page)
-
- def process_filename(self, fn, nested: bool = False) -> None:
- # process filenames or directories
- if not os.path.exists(fn):
- self.warn("Not found: %s", fn)
- return
-
- if os.path.isdir(fn) and not nested:
- path = os.path.realpath(fn)
- for item in os.listdir(path):
- self.process_filename(os.path.join(path, item), True)
-
- dists = distros_for_filename(fn)
- if dists:
- self.debug("Found: %s", fn)
- list(map(self.add, dists))
-
- def url_ok(self, url, fatal: bool = False) -> bool:
- s = URL_SCHEME(url)
- is_file = s and s.group(1).lower() == 'file'
- if is_file or self.allows(urllib.parse.urlparse(url)[1]):
- return True
- msg = (
- "\nNote: Bypassing %s (disallowed host; see "
- "https://setuptools.pypa.io/en/latest/deprecated/"
- "easy_install.html#restricting-downloads-with-allow-hosts for details).\n"
- )
- if fatal:
- raise DistutilsError(msg % url)
- else:
- self.warn(msg, url)
- return False
-
- def scan_egg_links(self, search_path) -> None:
- dirs = filter(os.path.isdir, search_path)
- egg_links = (
- (path, entry)
- for path in dirs
- for entry in os.listdir(path)
- if entry.endswith('.egg-link')
- )
- list(itertools.starmap(self.scan_egg_link, egg_links))
-
- def scan_egg_link(self, path, entry) -> None:
- content = _read_utf8_with_fallback(os.path.join(path, entry))
- # filter non-empty lines
- lines = list(filter(None, map(str.strip, content.splitlines())))
-
- if len(lines) != 2:
- # format is not recognized; punt
- return
-
- egg_path, _setup_path = lines
-
- for dist in find_distributions(os.path.join(path, egg_path)):
- dist.location = os.path.join(path, *lines)
- dist.precedence = SOURCE_DIST
- self.add(dist)
-
- def _scan(self, link):
- # Process a URL to see if it's for a package page
- NO_MATCH_SENTINEL = None, None
- if not link.startswith(self.index_url):
- return NO_MATCH_SENTINEL
-
- parts = list(map(urllib.parse.unquote, link[len(self.index_url) :].split('/')))
- if len(parts) != 2 or '#' in parts[1]:
- return NO_MATCH_SENTINEL
-
- # it's a package page, sanitize and index it
- pkg = safe_name(parts[0])
- ver = safe_version(parts[1])
- self.package_pages.setdefault(pkg.lower(), {})[link] = True
- return to_filename(pkg), to_filename(ver)
-
- def process_index(self, url, page):
- """Process the contents of a PyPI page"""
-
- # process an index page into the package-page index
- for match in HREF.finditer(page):
- try:
- self._scan(urllib.parse.urljoin(url, htmldecode(match.group(1))))
- except ValueError:
- pass
-
- pkg, ver = self._scan(url) # ensure this page is in the page index
- if not pkg:
- return "" # no sense double-scanning non-package pages
-
- # process individual package page
- for new_url in find_external_links(url, page):
- # Process the found URL
- base, frag = egg_info_for_url(new_url)
- if base.endswith('.py') and not frag:
- if ver:
- new_url += f'#egg={pkg}-{ver}'
- else:
- self.need_version_info(url)
- self.scan_url(new_url)
-
- return PYPI_MD5.sub(
- lambda m: '<a href="{}#md5={}">{}</a>'.format(*m.group(1, 3, 2)), page
- )
-
- def need_version_info(self, url) -> None:
- self.scan_all(
- "Page at %s links to .py file(s) without version info; an index "
- "scan is required.",
- url,
- )
-
- def scan_all(self, msg=None, *args) -> None:
- if self.index_url not in self.fetched_urls:
- if msg:
- self.warn(msg, *args)
- self.info("Scanning index of all packages (this may take a while)")
- self.scan_url(self.index_url)
-
- def find_packages(self, requirement) -> None:
- self.scan_url(self.index_url + requirement.unsafe_name + '/')
-
- if not self.package_pages.get(requirement.key):
- # Fall back to safe version of the name
- self.scan_url(self.index_url + requirement.project_name + '/')
-
- if not self.package_pages.get(requirement.key):
- # We couldn't find the target package, so search the index page too
- self.not_found_in_index(requirement)
-
- for url in list(self.package_pages.get(requirement.key, ())):
- # scan each page that might be related to the desired package
- self.scan_url(url)
-
- def obtain(self, requirement, installer=None):
- self.prescan()
- self.find_packages(requirement)
- for dist in self[requirement.key]:
- if dist in requirement:
- return dist
- self.debug("%s does not match %s", requirement, dist)
- return super().obtain(requirement, installer)
-
- def check_hash(self, checker, filename, tfp) -> None:
- """
- checker is a ContentChecker
- """
- checker.report(self.debug, f"Validating %s checksum for {filename}")
- if not checker.is_valid():
- tfp.close()
- os.unlink(filename)
- raise DistutilsError(
- f"{checker.hash.name} validation failed for {os.path.basename(filename)}; "
- "possible download problem?"
- )
-
- def add_find_links(self, urls) -> None:
- """Add `urls` to the list that will be prescanned for searches"""
- for url in urls:
- if (
- self.to_scan is None # if we have already "gone online"
- or not URL_SCHEME(url) # or it's a local file/directory
- or url.startswith('file:')
- or list(distros_for_url(url)) # or a direct package link
- ):
- # then go ahead and process it now
- self.scan_url(url)
- else:
- # otherwise, defer retrieval till later
- self.to_scan.append(url)
-
- def prescan(self):
- """Scan urls scheduled for prescanning (e.g. --find-links)"""
- if self.to_scan:
- list(map(self.scan_url, self.to_scan))
- self.to_scan = None # from now on, go ahead and process immediately
-
- def not_found_in_index(self, requirement) -> None:
- if self[requirement.key]: # we've seen at least one distro
- meth, msg = self.info, "Couldn't retrieve index page for %r"
- else: # no distros seen for this name, might be misspelled
- meth, msg = self.warn, "Couldn't find index page for %r (maybe misspelled?)"
- meth(msg, requirement.unsafe_name)
- self.scan_all()
-
- def download(self, spec, tmpdir):
- """Locate and/or download `spec` to `tmpdir`, returning a local path
-
- `spec` may be a ``Requirement`` object, or a string containing a URL,
- an existing local filename, or a project/version requirement spec
- (i.e. the string form of a ``Requirement`` object). If it is the URL
- of a .py file with an unambiguous ``#egg=name-version`` tag (i.e., one
- that escapes ``-`` as ``_`` throughout), a trivial ``setup.py`` is
- automatically created alongside the downloaded file.
-
- If `spec` is a ``Requirement`` object or a string containing a
- project/version requirement spec, this method returns the location of
- a matching distribution (possibly after downloading it to `tmpdir`).
- If `spec` is a locally existing file or directory name, it is simply
- returned unchanged. If `spec` is a URL, it is downloaded to a subpath
- of `tmpdir`, and the local filename is returned. Various errors may be
- raised if a problem occurs during downloading.
- """
- if not isinstance(spec, Requirement):
- scheme = URL_SCHEME(spec)
- if scheme:
- # It's a url, download it to tmpdir
- found = self._download_url(spec, tmpdir)
- base, fragment = egg_info_for_url(spec)
- if base.endswith('.py'):
- found = self.gen_setup(found, fragment, tmpdir)
- return found
- elif os.path.exists(spec):
- # Existing file or directory, just return it
- return spec
- else:
- spec = parse_requirement_arg(spec)
- return getattr(self.fetch_distribution(spec, tmpdir), 'location', None)
-
- def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME
- self,
- requirement,
- tmpdir,
- force_scan: bool = False,
- source: bool = False,
- develop_ok: bool = False,
- local_index=None,
- ) -> Distribution | None:
- """Obtain a distribution suitable for fulfilling `requirement`
-
- `requirement` must be a ``pkg_resources.Requirement`` instance.
- If necessary, or if the `force_scan` flag is set, the requirement is
- searched for in the (online) package index as well as the locally
- installed packages. If a distribution matching `requirement` is found,
- the returned distribution's ``location`` is the value you would have
- gotten from calling the ``download()`` method with the matching
- distribution's URL or filename. If no matching distribution is found,
- ``None`` is returned.
-
- If the `source` flag is set, only source distributions and source
- checkout links will be considered. Unless the `develop_ok` flag is
- set, development and system eggs (i.e., those using the ``.egg-info``
- format) will be ignored.
- """
- # process a Requirement
- self.info("Searching for %s", requirement)
- skipped = set()
- dist = None
-
- def find(req, env: Environment | None = None):
- if env is None:
- env = self
- # Find a matching distribution; may be called more than once
-
- for dist in env[req.key]:
- if dist.precedence == DEVELOP_DIST and not develop_ok:
- if dist not in skipped:
- self.warn(
- "Skipping development or system egg: %s",
- dist,
- )
- skipped.add(dist)
- continue
-
- test = dist in req and (dist.precedence <= SOURCE_DIST or not source)
- if test:
- loc = self.download(dist.location, tmpdir)
- dist.download_location = loc
- if os.path.exists(dist.download_location):
- return dist
-
- return None
-
- if force_scan:
- self.prescan()
- self.find_packages(requirement)
- dist = find(requirement)
-
- if not dist and local_index is not None:
- dist = find(requirement, local_index)
-
- if dist is None:
- if self.to_scan is not None:
- self.prescan()
- dist = find(requirement)
-
- if dist is None and not force_scan:
- self.find_packages(requirement)
- dist = find(requirement)
-
- if dist is None:
- self.warn(
- "No local packages or working download links found for %s%s",
- (source and "a source distribution of " or ""),
- requirement,
- )
- return None
- else:
- self.info("Best match: %s", dist)
- return dist.clone(location=dist.download_location)
-
- def fetch(
- self, requirement, tmpdir, force_scan: bool = False, source: bool = False
- ) -> str | None:
- """Obtain a file suitable for fulfilling `requirement`
-
- DEPRECATED; use the ``fetch_distribution()`` method now instead. For
- backward compatibility, this routine is identical but returns the
- ``location`` of the downloaded distribution instead of a distribution
- object.
- """
- dist = self.fetch_distribution(requirement, tmpdir, force_scan, source)
- if dist is not None:
- return dist.location
- return None
-
- def gen_setup(self, filename, fragment, tmpdir):
- match = EGG_FRAGMENT.match(fragment)
- dists = (
- match
- and [
- d
- for d in interpret_distro_name(filename, match.group(1), None)
- if d.version
- ]
- or []
- )
-
- if len(dists) == 1: # unambiguous ``#egg`` fragment
- basename = os.path.basename(filename)
-
- # Make sure the file has been downloaded to the temp dir.
- if os.path.dirname(filename) != tmpdir:
- dst = os.path.join(tmpdir, basename)
- if not (os.path.exists(dst) and os.path.samefile(filename, dst)):
- shutil.copy2(filename, dst)
- filename = dst
-
- with open(os.path.join(tmpdir, 'setup.py'), 'w', encoding="utf-8") as file:
- file.write(
- "from setuptools import setup\n"
- f"setup(name={dists[0].project_name!r}, version={dists[0].version!r}, py_modules=[{os.path.splitext(basename)[0]!r}])\n"
- )
- return filename
-
- elif match:
- raise DistutilsError(
- f"Can't unambiguously interpret project/version identifier {fragment!r}; "
- "any dashes in the name or version should be escaped using "
- f"underscores. {dists!r}"
- )
- else:
- raise DistutilsError(
- "Can't process plain .py files without an '#egg=name-version'"
- " suffix to enable automatic setup script generation."
- )
-
- dl_blocksize = 8192
-
- def _download_to(self, url, filename):
- self.info("Downloading %s", url)
- # Download the file
- fp = None
- try:
- checker = HashChecker.from_url(url)
- fp = self.open_url(url)
- if isinstance(fp, urllib.error.HTTPError):
- raise DistutilsError(f"Can't download {url}: {fp.code} {fp.msg}")
- headers = fp.info()
- blocknum = 0
- bs = self.dl_blocksize
- size = -1
- if "content-length" in headers:
- # Some servers return multiple Content-Length headers :(
- sizes = headers.get_all('Content-Length')
- size = max(map(int, sizes))
- self.reporthook(url, filename, blocknum, bs, size)
- with open(filename, 'wb') as tfp:
- while True:
- block = fp.read(bs)
- if block:
- checker.feed(block)
- tfp.write(block)
- blocknum += 1
- self.reporthook(url, filename, blocknum, bs, size)
- else:
- break
- self.check_hash(checker, filename, tfp)
- return headers
- finally:
- if fp:
- fp.close()
-
- def reporthook(self, url, filename, blocknum, blksize, size) -> None:
- pass # no-op
-
- # FIXME:
- def open_url(self, url, warning=None): # noqa: C901 # is too complex (12)
- if url.startswith('file:'):
- return local_open(url)
- try:
- return open_with_auth(url, self.opener)
- except (ValueError, http.client.InvalidURL) as v:
- msg = ' '.join([str(arg) for arg in v.args])
- if warning:
- self.warn(warning, msg)
- else:
- raise DistutilsError(f'{url} {msg}') from v
- except urllib.error.HTTPError as v:
- return v
- except urllib.error.URLError as v:
- if warning:
- self.warn(warning, v.reason)
- else:
- raise DistutilsError(f"Download error for {url}: {v.reason}") from v
- except http.client.BadStatusLine as v:
- if warning:
- self.warn(warning, v.line)
- else:
- raise DistutilsError(
- f'{url} returned a bad status line. The server might be '
- f'down, {v.line}'
- ) from v
- except (http.client.HTTPException, OSError) as v:
- if warning:
- self.warn(warning, v)
- else:
- raise DistutilsError(f"Download error for {url}: {v}") from v
-
- @staticmethod
- def _sanitize(name):
- r"""
- Replace unsafe path directives with underscores.
-
- >>> san = PackageIndex._sanitize
- >>> san('/home/user/.ssh/authorized_keys')
- '_home_user_.ssh_authorized_keys'
- >>> san('..\\foo\\bing')
- '__foo_bing'
- >>> san('D:bar')
- 'D_bar'
- >>> san('C:\\bar')
- 'C__bar'
- >>> san('foo..bar')
- 'foo..bar'
- >>> san('D:../foo')
- 'D___foo'
- """
- pattern = '|'.join((
- # drive letters
- r':',
- # path separators
- r'[/\\]',
- # parent dirs
- r'(?:(?<=([/\\]|:))\.\.(?=[/\\]|$))|(?:^\.\.(?=[/\\]|$))',
- ))
- return re.sub(pattern, r'_', name)
-
- @classmethod
- def _resolve_download_filename(cls, url, tmpdir):
- """
- >>> import pathlib
- >>> du = PackageIndex._resolve_download_filename
- >>> root = getfixture('tmp_path')
- >>> url = 'https://files.pythonhosted.org/packages/a9/5a/0db.../setuptools-78.1.0.tar.gz'
- >>> str(pathlib.Path(du(url, root)).relative_to(root))
- 'setuptools-78.1.0.tar.gz'
- """
- name, _fragment = egg_info_for_url(url)
- name = cls._sanitize(
- name
- or
- # default if URL has no path contents
- '__downloaded__'
- )
-
- # strip any extra .zip before download
- name = re.sub(r'\.egg\.zip$', '.egg', name)
-
- return os.path.join(tmpdir, name)
-
- def _download_url(self, url, tmpdir):
- """
- Determine the download filename.
- """
- filename = self._resolve_download_filename(url, tmpdir)
- return self._download_vcs(url, filename) or self._download_other(url, filename)
-
- @staticmethod
- def _resolve_vcs(url):
- """
- >>> rvcs = PackageIndex._resolve_vcs
- >>> rvcs('git+http://foo/bar')
- 'git'
- >>> rvcs('hg+https://foo/bar')
- 'hg'
- >>> rvcs('git:myhost')
- 'git'
- >>> rvcs('hg:myhost')
- >>> rvcs('http://foo/bar')
- """
- scheme = urllib.parse.urlsplit(url).scheme
- pre, sep, _post = scheme.partition('+')
- # svn and git have their own protocol; hg does not
- allowed = set(['svn', 'git'] + ['hg'] * bool(sep))
- return next(iter({pre} & allowed), None)
-
- def _download_vcs(self, url, spec_filename):
- vcs = self._resolve_vcs(url)
- if not vcs:
- return None
- if vcs == 'svn':
- raise DistutilsError(
- f"Invalid config, SVN download is not supported: {url}"
- )
-
- filename, _, _ = spec_filename.partition('#')
- url, rev = self._vcs_split_rev_from_url(url)
-
- self.info(f"Doing {vcs} clone from {url} to {filename}")
- subprocess.check_call([vcs, 'clone', '--quiet', url, filename])
-
- co_commands = dict(
- git=[vcs, '-C', filename, 'checkout', '--quiet', rev],
- hg=[vcs, '--cwd', filename, 'up', '-C', '-r', rev, '-q'],
- )
- if rev is not None:
- self.info(f"Checking out {rev}")
- subprocess.check_call(co_commands[vcs])
-
- return filename
-
- def _download_other(self, url, filename):
- scheme = urllib.parse.urlsplit(url).scheme
- if scheme == 'file': # pragma: no cover
- return urllib.request.url2pathname(urllib.parse.urlparse(url).path)
- # raise error if not allowed
- self.url_ok(url, True)
- return self._attempt_download(url, filename)
-
- def scan_url(self, url) -> None:
- self.process_url(url, True)
-
- def _attempt_download(self, url, filename):
- headers = self._download_to(url, filename)
- if 'html' in headers.get('content-type', '').lower():
- return self._invalid_download_html(url, headers, filename)
- else:
- return filename
-
- def _invalid_download_html(self, url, headers, filename):
- os.unlink(filename)
- raise DistutilsError(f"Unexpected HTML page found at {url}")
-
- @staticmethod
- def _vcs_split_rev_from_url(url):
- """
- Given a possible VCS URL, return a clean URL and resolved revision if any.
-
- >>> vsrfu = PackageIndex._vcs_split_rev_from_url
- >>> vsrfu('git+https://github.com/pypa/[email protected]#egg-info=setuptools')
- ('https://github.com/pypa/setuptools', 'v69.0.0')
- >>> vsrfu('git+https://github.com/pypa/setuptools#egg-info=setuptools')
- ('https://github.com/pypa/setuptools', None)
- >>> vsrfu('http://foo/bar')
- ('http://foo/bar', None)
- """
- parts = urllib.parse.urlsplit(url)
-
- clean_scheme = parts.scheme.split('+', 1)[-1]
-
- # Some fragment identification fails
- no_fragment_path, _, _ = parts.path.partition('#')
-
- pre, sep, post = no_fragment_path.rpartition('@')
- clean_path, rev = (pre, post) if sep else (post, None)
-
- resolved = parts._replace(
- scheme=clean_scheme,
- path=clean_path,
- # discard the fragment
- fragment='',
- ).geturl()
-
- return resolved, rev
-
- def debug(self, msg, *args) -> None:
- log.debug(msg, *args)
-
- def info(self, msg, *args) -> None:
- log.info(msg, *args)
-
- def warn(self, msg, *args) -> None:
- log.warn(msg, *args)
-
-
-# This pattern matches a character entity reference (a decimal numeric
-# references, a hexadecimal numeric reference, or a named reference).
-entity_sub = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub
-
-
-def decode_entity(match):
- what = match.group(0)
- return html.unescape(what)
-
-
-def htmldecode(text):
- """
- Decode HTML entities in the given text.
-
- >>> htmldecode(
- ... 'https://../package_name-0.1.2.tar.gz'
- ... '?tokena=A&amp;tokenb=B">package_name-0.1.2.tar.gz')
- 'https://../package_name-0.1.2.tar.gz?tokena=A&tokenb=B">package_name-0.1.2.tar.gz'
- """
- return entity_sub(decode_entity, text)
-
-
-def socket_timeout(timeout=15):
- def _socket_timeout(func):
- def _socket_timeout(*args, **kwargs):
- old_timeout = socket.getdefaulttimeout()
- socket.setdefaulttimeout(timeout)
- try:
- return func(*args, **kwargs)
- finally:
- socket.setdefaulttimeout(old_timeout)
-
- return _socket_timeout
-
- return _socket_timeout
-
-
-def _encode_auth(auth):
- """
- Encode auth from a URL suitable for an HTTP header.
- >>> str(_encode_auth('username%3Apassword'))
- 'dXNlcm5hbWU6cGFzc3dvcmQ='
-
- Long auth strings should not cause a newline to be inserted.
- >>> long_auth = 'username:' + 'password'*10
- >>> chr(10) in str(_encode_auth(long_auth))
- False
- """
- auth_s = urllib.parse.unquote(auth)
- # convert to bytes
- auth_bytes = auth_s.encode()
- encoded_bytes = base64.b64encode(auth_bytes)
- # convert back to a string
- encoded = encoded_bytes.decode()
- # strip the trailing carriage return
- return encoded.replace('\n', '')
-
-
-class Credential(NamedTuple):
- """
- A username/password pair.
-
- Displayed separated by `:`.
- >>> str(Credential('username', 'password'))
- 'username:password'
- """
-
- username: str
- password: str
-
- def __str__(self) -> str:
- return f'{self.username}:{self.password}'
-
-
-class PyPIConfig(configparser.RawConfigParser):
- def __init__(self):
- """
- Load from ~/.pypirc
- """
- defaults = dict.fromkeys(['username', 'password', 'repository'], '')
- super().__init__(defaults)
-
- rc = os.path.join(os.path.expanduser('~'), '.pypirc')
- if os.path.exists(rc):
- _cfg_read_utf8_with_fallback(self, rc)
-
- @property
- def creds_by_repository(self):
- sections_with_repositories = [
- section
- for section in self.sections()
- if self.get(section, 'repository').strip()
- ]
-
- return dict(map(self._get_repo_cred, sections_with_repositories))
-
- def _get_repo_cred(self, section):
- repo = self.get(section, 'repository').strip()
- return repo, Credential(
- self.get(section, 'username').strip(),
- self.get(section, 'password').strip(),
- )
-
- def find_credential(self, url):
- """
- If the URL indicated appears to be a repository defined in this
- config, return the credential for that repository.
- """
- for repository, cred in self.creds_by_repository.items():
- if url.startswith(repository):
- return cred
- return None
-
-
-def open_with_auth(url, opener=urllib.request.urlopen):
- """Open a urllib2 request, handling HTTP authentication"""
-
- parsed = urllib.parse.urlparse(url)
- scheme, netloc, path, params, query, frag = parsed
-
- # Double scheme does not raise on macOS as revealed by a
- # failing test. We would expect "nonnumeric port". Refs #20.
- if netloc.endswith(':'):
- raise http.client.InvalidURL("nonnumeric port: ''")
-
- if scheme in ('http', 'https'):
- auth, address = _splituser(netloc)
- else:
- auth, address = (None, None)
-
- if not auth:
- cred = PyPIConfig().find_credential(url)
- if cred:
- auth = str(cred)
- info = cred.username, url
- log.info('Authenticating as %s for %s (from .pypirc)', *info)
-
- if auth:
- auth = "Basic " + _encode_auth(auth)
- parts = scheme, address, path, params, query, frag
- new_url = urllib.parse.urlunparse(parts)
- request = urllib.request.Request(new_url)
- request.add_header("Authorization", auth)
- else:
- request = urllib.request.Request(url)
-
- request.add_header('User-Agent', user_agent)
- fp = opener(request)
-
- if auth:
- # Put authentication info back into request URL if same host,
- # so that links found on the page will work
- s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url)
- if s2 == scheme and h2 == address:
- parts = s2, netloc, path2, param2, query2, frag2
- fp.url = urllib.parse.urlunparse(parts)
-
- return fp
-
-
-# copy of urllib.parse._splituser from Python 3.8
-# See https://github.com/python/cpython/issues/80072.
-def _splituser(host):
- """splituser('user[:passwd]@host[:port]')
- --> 'user[:passwd]', 'host[:port]'."""
- user, delim, host = host.rpartition('@')
- return (user if delim else None), host
-
-
-# adding a timeout to avoid freezing package_index
-open_with_auth = socket_timeout(_SOCKET_TIMEOUT)(open_with_auth)
-
-
-def fix_sf_url(url):
- return url # backward compatibility
-
-
-def local_open(url):
- """Read a local path, with special support for directories"""
- _scheme, _server, path, _param, _query, _frag = urllib.parse.urlparse(url)
- filename = urllib.request.url2pathname(path)
- if os.path.isfile(filename):
- return urllib.request.urlopen(url)
- elif path.endswith('/') and os.path.isdir(filename):
- files = []
- for f in os.listdir(filename):
- filepath = os.path.join(filename, f)
- if f == 'index.html':
- body = _read_utf8_with_fallback(filepath)
- break
- elif os.path.isdir(filepath):
- f += '/'
- files.append(f'<a href="{f}">{f}</a>')
- else:
- tmpl = "<html><head><title>{url}</title></head><body>{files}</body></html>"
- body = tmpl.format(url=url, files='\n'.join(files))
- status, message = 200, "OK"
- else:
- status, message, body = 404, "Path not found", "Not found"
-
- headers = {'content-type': 'text/html'}
- body_stream = io.StringIO(body)
- return urllib.error.HTTPError(url, status, message, headers, body_stream)
diff --git a/contrib/python/setuptools/py3/setuptools/sandbox.py b/contrib/python/setuptools/py3/setuptools/sandbox.py
deleted file mode 100644
index 2d84242d667..00000000000
--- a/contrib/python/setuptools/py3/setuptools/sandbox.py
+++ /dev/null
@@ -1,536 +0,0 @@
-from __future__ import annotations
-
-import builtins
-import contextlib
-import functools
-import itertools
-import operator
-import os
-import pickle
-import re
-import sys
-import tempfile
-import textwrap
-from types import TracebackType
-from typing import TYPE_CHECKING, Any, ClassVar
-
-import pkg_resources
-from pkg_resources import working_set
-
-from distutils.errors import DistutilsError
-
-if TYPE_CHECKING:
- import os as _os
-elif sys.platform.startswith('java'):
- import org.python.modules.posix.PosixModule as _os # pyright: ignore[reportMissingImports]
-else:
- _os = sys.modules[os.name]
-_open = open
-
-
-if TYPE_CHECKING:
- from typing_extensions import Self
-
-__all__ = [
- "AbstractSandbox",
- "DirectorySandbox",
- "SandboxViolation",
- "run_setup",
-]
-
-
-def _execfile(filename, globals, locals=None):
- """
- Python 3 implementation of execfile.
- """
- mode = 'rb'
- with open(filename, mode) as stream:
- script = stream.read()
- if locals is None:
- locals = globals
- code = compile(script, filename, 'exec')
- exec(code, globals, locals)
-
-
-def save_argv(repl=None):
- saved = sys.argv[:]
- if repl is not None:
- sys.argv[:] = repl
- try:
- yield saved
- finally:
- sys.argv[:] = saved
-
-
-def save_path():
- saved = sys.path[:]
- try:
- yield saved
- finally:
- sys.path[:] = saved
-
-
-def override_temp(replacement):
- """
- Monkey-patch tempfile.tempdir with replacement, ensuring it exists
- """
- os.makedirs(replacement, exist_ok=True)
-
- saved = tempfile.tempdir
-
- tempfile.tempdir = replacement
-
- try:
- yield
- finally:
- tempfile.tempdir = saved
-
-
-def pushd(target):
- saved = os.getcwd()
- os.chdir(target)
- try:
- yield saved
- finally:
- os.chdir(saved)
-
-
-class UnpickleableException(Exception):
- """
- An exception representing another Exception that could not be pickled.
- """
-
- @staticmethod
- def dump(type, exc):
- """
- Always return a dumped (pickled) type and exc. If exc can't be pickled,
- wrap it in UnpickleableException first.
- """
- try:
- return pickle.dumps(type), pickle.dumps(exc)
- except Exception:
- # get UnpickleableException inside the sandbox
- from setuptools.sandbox import UnpickleableException as cls
-
- return cls.dump(cls, cls(repr(exc)))
-
-
-class ExceptionSaver:
- """
- A Context Manager that will save an exception, serialize, and restore it
- later.
- """
-
- def __enter__(self) -> Self:
- return self
-
- def __exit__(
- self,
- type: type[BaseException] | None,
- exc: BaseException | None,
- tb: TracebackType | None,
- ) -> bool:
- if not exc:
- return False
-
- # dump the exception
- self._saved = UnpickleableException.dump(type, exc)
- self._tb = tb
-
- # suppress the exception
- return True
-
- def resume(self):
- "restore and re-raise any exception"
-
- if '_saved' not in vars(self):
- return
-
- _type, exc = map(pickle.loads, self._saved)
- raise exc.with_traceback(self._tb)
-
-
-def save_modules():
- """
- Context in which imported modules are saved.
-
- Translates exceptions internal to the context into the equivalent exception
- outside the context.
- """
- saved = sys.modules.copy()
- with ExceptionSaver() as saved_exc:
- yield saved
-
- sys.modules.update(saved)
- # remove any modules imported since
- del_modules = (
- mod_name
- for mod_name in sys.modules
- if mod_name not in saved
- # exclude any encodings modules. See #285
- and not mod_name.startswith('encodings.')
- )
- _clear_modules(del_modules)
-
- saved_exc.resume()
-
-
-def _clear_modules(module_names):
- for mod_name in list(module_names):
- del sys.modules[mod_name]
-
-
-def save_pkg_resources_state():
- saved = pkg_resources.__getstate__()
- try:
- yield saved
- finally:
- pkg_resources.__setstate__(saved)
-
-
-def setup_context(setup_dir):
- temp_dir = os.path.join(setup_dir, 'temp')
- with save_pkg_resources_state():
- with save_modules():
- with save_path():
- hide_setuptools()
- with save_argv():
- with override_temp(temp_dir):
- with pushd(setup_dir):
- # ensure setuptools commands are available
- __import__('setuptools')
- yield
-
-
-_MODULES_TO_HIDE = {
- 'setuptools',
- 'distutils',
- 'pkg_resources',
- 'Cython',
- '_distutils_hack',
-}
-
-
-def _needs_hiding(mod_name):
- """
- >>> _needs_hiding('setuptools')
- True
- >>> _needs_hiding('pkg_resources')
- True
- >>> _needs_hiding('setuptools_plugin')
- False
- >>> _needs_hiding('setuptools.__init__')
- True
- >>> _needs_hiding('distutils')
- True
- >>> _needs_hiding('os')
- False
- >>> _needs_hiding('Cython')
- True
- """
- base_module = mod_name.split('.', 1)[0]
- return base_module in _MODULES_TO_HIDE
-
-
-def hide_setuptools():
- """
- Remove references to setuptools' modules from sys.modules to allow the
- invocation to import the most appropriate setuptools. This technique is
- necessary to avoid issues such as #315 where setuptools upgrading itself
- would fail to find a function declared in the metadata.
- """
- _distutils_hack = sys.modules.get('_distutils_hack', None)
- if _distutils_hack is not None:
- _distutils_hack._remove_shim()
-
- modules = filter(_needs_hiding, sys.modules)
- _clear_modules(modules)
-
-
-def run_setup(setup_script, args):
- """Run a distutils setup script, sandboxed in its directory"""
- setup_dir = os.path.abspath(os.path.dirname(setup_script))
- with setup_context(setup_dir):
- try:
- sys.argv[:] = [setup_script] + list(args)
- sys.path.insert(0, setup_dir)
- # reset to include setup dir, w/clean callback list
- working_set.__init__()
- working_set.callbacks.append(lambda dist: dist.activate())
-
- with DirectorySandbox(setup_dir):
- ns = dict(__file__=setup_script, __name__='__main__')
- _execfile(setup_script, ns)
- except SystemExit as v:
- if v.args and v.args[0]:
- raise
- # Normal exit, just return
-
-
-class AbstractSandbox:
- """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts"""
-
- _active = False
-
- def __init__(self) -> None:
- self._attrs = [
- name
- for name in dir(_os)
- if not name.startswith('_') and hasattr(self, name)
- ]
-
- def _copy(self, source):
- for name in self._attrs:
- setattr(os, name, getattr(source, name))
-
- def __enter__(self) -> None:
- self._copy(self)
- builtins.open = self._open
- self._active = True
-
- def __exit__(
- self,
- exc_type: type[BaseException] | None,
- exc_value: BaseException | None,
- traceback: TracebackType | None,
- ):
- self._active = False
- builtins.open = _open
- self._copy(_os)
-
- def run(self, func):
- """Run 'func' under os sandboxing"""
- with self:
- return func()
-
- def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
- original = getattr(_os, name)
-
- def wrap(self, src, dst, *args, **kw):
- if self._active:
- src, dst = self._remap_pair(name, src, dst, *args, **kw)
- return original(src, dst, *args, **kw)
-
- return wrap
-
- for __name in ["rename", "link", "symlink"]:
- if hasattr(_os, __name):
- locals()[__name] = _mk_dual_path_wrapper(__name)
-
- def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
- original = original or getattr(_os, name)
-
- def wrap(self, path, *args, **kw):
- if self._active:
- path = self._remap_input(name, path, *args, **kw)
- return original(path, *args, **kw)
-
- return wrap
-
- _open = _mk_single_path_wrapper('open', _open)
- for __name in [
- "stat",
- "listdir",
- "chdir",
- "open",
- "chmod",
- "chown",
- "mkdir",
- "remove",
- "unlink",
- "rmdir",
- "utime",
- "lchown",
- "chroot",
- "lstat",
- "startfile",
- "mkfifo",
- "mknod",
- "pathconf",
- "access",
- ]:
- if hasattr(_os, __name):
- locals()[__name] = _mk_single_path_wrapper(__name)
-
- def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
- original = getattr(_os, name)
-
- def wrap(self, path, *args, **kw):
- if self._active:
- path = self._remap_input(name, path, *args, **kw)
- return self._remap_output(name, original(path, *args, **kw))
- return original(path, *args, **kw)
-
- return wrap
-
- for __name in ['readlink', 'tempnam']:
- if hasattr(_os, __name):
- locals()[__name] = _mk_single_with_return(__name)
-
- def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
- original = getattr(_os, name)
-
- def wrap(self, *args, **kw):
- retval = original(*args, **kw)
- if self._active:
- return self._remap_output(name, retval)
- return retval
-
- return wrap
-
- for __name in ['getcwd', 'tmpnam']:
- if hasattr(_os, __name):
- locals()[__name] = _mk_query(__name)
-
- def _validate_path(self, path):
- """Called to remap or validate any path, whether input or output"""
- return path
-
- def _remap_input(self, operation, path, *args, **kw):
- """Called for path inputs"""
- return self._validate_path(path)
-
- def _remap_output(self, operation, path):
- """Called for path outputs"""
- return self._validate_path(path)
-
- def _remap_pair(self, operation, src, dst, *args, **kw):
- """Called for path pairs like rename, link, and symlink operations"""
- return (
- self._remap_input(operation + '-from', src, *args, **kw),
- self._remap_input(operation + '-to', dst, *args, **kw),
- )
-
- if TYPE_CHECKING:
- # This is a catch-all for all the dynamically created attributes.
- # This isn't public API anyway
- def __getattribute__(self, name: str) -> Any: ...
-
-
-if hasattr(os, 'devnull'):
- _EXCEPTIONS = [os.devnull]
-else:
- _EXCEPTIONS = []
-
-
-class DirectorySandbox(AbstractSandbox):
- """Restrict operations to a single subdirectory - pseudo-chroot"""
-
- write_ops: ClassVar[dict[str, None]] = dict.fromkeys([
- "open",
- "chmod",
- "chown",
- "mkdir",
- "remove",
- "unlink",
- "rmdir",
- "utime",
- "lchown",
- "chroot",
- "mkfifo",
- "mknod",
- "tempnam",
- ])
-
- _exception_patterns: list[str | re.Pattern] = []
- "exempt writing to paths that match the pattern"
-
- def __init__(self, sandbox, exceptions=_EXCEPTIONS) -> None:
- self._sandbox = os.path.normcase(os.path.realpath(sandbox))
- self._prefix = os.path.join(self._sandbox, '')
- self._exceptions = [
- os.path.normcase(os.path.realpath(path)) for path in exceptions
- ]
- AbstractSandbox.__init__(self)
-
- def _violation(self, operation, *args, **kw):
- from setuptools.sandbox import SandboxViolation
-
- raise SandboxViolation(operation, args, kw)
-
- def _open(self, path, mode='r', *args, **kw):
- if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
- self._violation("open", path, mode, *args, **kw)
- return _open(path, mode, *args, **kw)
-
- def tmpnam(self) -> None:
- self._violation("tmpnam")
-
- def _ok(self, path):
- active = self._active
- try:
- self._active = False
- realpath = os.path.normcase(os.path.realpath(path))
- return (
- self._exempted(realpath)
- or realpath == self._sandbox
- or realpath.startswith(self._prefix)
- )
- finally:
- self._active = active
-
- def _exempted(self, filepath):
- start_matches = (
- filepath.startswith(exception) for exception in self._exceptions
- )
- pattern_matches = (
- re.match(pattern, filepath) for pattern in self._exception_patterns
- )
- candidates = itertools.chain(start_matches, pattern_matches)
- return any(candidates)
-
- def _remap_input(self, operation, path, *args, **kw):
- """Called for path inputs"""
- if operation in self.write_ops and not self._ok(path):
- self._violation(operation, os.path.realpath(path), *args, **kw)
- return path
-
- def _remap_pair(self, operation, src, dst, *args, **kw):
- """Called for path pairs like rename, link, and symlink operations"""
- if not self._ok(src) or not self._ok(dst):
- self._violation(operation, src, dst, *args, **kw)
- return (src, dst)
-
- def open(self, file, flags, mode: int = 0o777, *args, **kw) -> int:
- """Called for low-level os.open()"""
- if flags & WRITE_FLAGS and not self._ok(file):
- self._violation("os.open", file, flags, mode, *args, **kw)
- return _os.open(file, flags, mode, *args, **kw)
-
-
-WRITE_FLAGS = functools.reduce(
- operator.or_,
- [
- getattr(_os, a, 0)
- for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()
- ],
-)
-
-
-class SandboxViolation(DistutilsError):
- """A setup script attempted to modify the filesystem outside the sandbox"""
-
- tmpl = textwrap.dedent(
- """
- SandboxViolation: {cmd}{args!r} {kwargs}
-
- The package setup script has attempted to modify files on your system
- that are not within the EasyInstall build area, and has been aborted.
-
- This package cannot be safely installed by EasyInstall, and may not
- support alternate installation locations even if you run its setup
- script by hand. Please inform the package's author and the EasyInstall
- maintainers to find out if a fix or workaround is available.
- """
- ).lstrip()
-
- def __str__(self) -> str:
- cmd, args, kwargs = self.args
- return self.tmpl.format(**locals())
diff --git a/contrib/python/setuptools/py3/setuptools/wheel.py b/contrib/python/setuptools/py3/setuptools/wheel.py
index c7ca43b5cfb..124e01ad2f9 100644
--- a/contrib/python/setuptools/py3/setuptools/wheel.py
+++ b/contrib/python/setuptools/py3/setuptools/wheel.py
@@ -9,6 +9,7 @@ import posixpath
import re
import zipfile
+from packaging.requirements import Requirement
from packaging.tags import sys_tags
from packaging.utils import canonicalize_name
from packaging.version import Version as parse_version
@@ -17,6 +18,8 @@ import setuptools
from setuptools.archive_util import _unpack_zipfile_obj
from setuptools.command.egg_info import _egg_basename, write_requirements
+from ._discovery import extras_from_deps
+from ._importlib import metadata
from .unicode_utils import _read_utf8_with_fallback
from distutils.util import get_platform
@@ -133,8 +136,6 @@ 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')
@@ -148,30 +149,10 @@ class Wheel:
raise ValueError(f'unsupported wheel format version: {wheel_version}')
# Extract to target directory.
_unpack_zipfile_obj(zf, destination_eggdir)
- # Convert metadata.
dist_info = os.path.join(destination_eggdir, dist_info)
- dist = pkg_resources.Distribution.from_location(
- destination_eggdir,
- dist_info,
- metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),
+ install_requires, extras_require = Wheel._convert_requires(
+ destination_eggdir, dist_info
)
-
- # Note: Evaluate and strip markers now,
- # as it's difficult to convert back from the syntax:
- # foobar; "linux" in sys_platform and extra == 'test'
- def raw_req(req):
- req.marker = None
- return str(req)
-
- install_requires = list(map(raw_req, dist.requires()))
- extras_require = {
- extra: [
- req
- for req in map(raw_req, dist.requires((extra,)))
- if req not in install_requires
- ]
- for extra in dist.extras
- }
os.rename(dist_info, egg_info)
os.rename(
os.path.join(egg_info, 'METADATA'),
@@ -191,6 +172,50 @@ class Wheel:
)
@staticmethod
+ def _convert_requires(destination_eggdir, dist_info):
+ md = metadata.Distribution.at(dist_info).metadata
+ deps = md.get_all('Requires-Dist') or []
+ reqs = list(map(Requirement, deps))
+
+ extras = extras_from_deps(deps)
+
+ # Note: Evaluate and strip markers now,
+ # as it's difficult to convert back from the syntax:
+ # foobar; "linux" in sys_platform and extra == 'test'
+ def raw_req(req):
+ req = Requirement(str(req))
+ req.marker = None
+ return str(req)
+
+ def eval(req, **env):
+ return not req.marker or req.marker.evaluate(env)
+
+ def for_extra(req):
+ try:
+ markers = req.marker._markers
+ except AttributeError:
+ markers = ()
+ return set(
+ marker[2].value
+ for marker in markers
+ if isinstance(marker, tuple) and marker[0].value == 'extra'
+ )
+
+ install_requires = list(
+ map(raw_req, filter(eval, itertools.filterfalse(for_extra, reqs)))
+ )
+ extras_require = {
+ extra: list(
+ map(
+ raw_req,
+ (req for req in reqs if for_extra(req) and eval(req, extra=extra)),
+ )
+ )
+ for extra in extras
+ }
+ return install_requires, extras_require
+
+ @staticmethod
def _move_data_entries(destination_eggdir, dist_data):
"""Move data entries to their correct location."""
dist_data = os.path.join(destination_eggdir, dist_data)
diff --git a/contrib/python/setuptools/py3/ya.make b/contrib/python/setuptools/py3/ya.make
index e36854d6165..0cd5b87eff5 100644
--- a/contrib/python/setuptools/py3/ya.make
+++ b/contrib/python/setuptools/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(79.0.1)
+VERSION(80.9.0)
LICENSE(MIT)
@@ -35,6 +35,7 @@ PY_SRCS(
pkg_resources/__init__.py
setuptools/__init__.py
setuptools/_core_metadata.py
+ setuptools/_discovery.py
setuptools/_distutils/__init__.py
setuptools/_distutils/_log.py
setuptools/_distutils/_macos_compat.py
@@ -101,6 +102,7 @@ PY_SRCS(
setuptools/_normalization.py
setuptools/_path.py
setuptools/_reqs.py
+ setuptools/_scripts.py
setuptools/_shutil.py
setuptools/_static.py
setuptools/archive_util.py
@@ -158,8 +160,6 @@ PY_SRCS(
setuptools/monkey.py
setuptools/msvc.py
setuptools/namespaces.py
- setuptools/package_index.py
- setuptools/sandbox.py
setuptools/unicode_utils.py
setuptools/version.py
setuptools/warnings.py