diff options
| author | robot-piglet <[email protected]> | 2026-01-14 19:52:35 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-01-14 20:21:53 +0300 |
| commit | d980d7a650f4a3cfe8d1b7f030847b03d5c511e4 (patch) | |
| tree | 2abf6c93dd93742d418aed2930f57ee56b35d1bc /contrib/python/setuptools | |
| parent | e24dde6b64a154b0296225e86e4b68bdf668b64c (diff) | |
Intermediate changes
commit_hash:df4e6068190137786e6f2f5bf3604e06432cba52
Diffstat (limited to 'contrib/python/setuptools')
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&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&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 |
