diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2025-01-24 00:01:17 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2025-01-24 00:13:36 +0300 |
commit | e0834724754ae9e26fd14e27027e69cc22d1939f (patch) | |
tree | f2d16bc64fc4307f957913b1050a2916fafdf1b9 | |
parent | 707a3047759771809e881e169d40f37de19b8a7c (diff) | |
download | ydb-e0834724754ae9e26fd14e27027e69cc22d1939f.tar.gz |
Intermediate changes
commit_hash:e49cc600846762b0cef5d379812c52e8cb8f52f1
8 files changed, 323 insertions, 56 deletions
diff --git a/contrib/python/setuptools/py3/.dist-info/METADATA b/contrib/python/setuptools/py3/.dist-info/METADATA index 96aa363053..0c941771c5 100644 --- a/contrib/python/setuptools/py3/.dist-info/METADATA +++ b/contrib/python/setuptools/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.2 Name: setuptools -Version: 75.7.0 +Version: 75.8.0 Summary: Easily download, build, install, upgrade, and uninstall Python packages Author-email: Python Packaging Authority <distutils-sig@python.org> Project-URL: Source, https://github.com/pypa/setuptools diff --git a/contrib/python/setuptools/py3/setuptools/_core_metadata.py b/contrib/python/setuptools/py3/setuptools/_core_metadata.py index 642b80df31..850cc409f7 100644 --- a/contrib/python/setuptools/py3/setuptools/_core_metadata.py +++ b/contrib/python/setuptools/py3/setuptools/_core_metadata.py @@ -19,6 +19,7 @@ from packaging.utils import canonicalize_name, canonicalize_version from packaging.version import Version from . import _normalization, _reqs +from ._static import is_static from .warnings import SetuptoolsDeprecationWarning from distutils.util import rfc822_escape @@ -27,7 +28,7 @@ from distutils.util import rfc822_escape def get_metadata_version(self): mv = getattr(self, 'metadata_version', None) if mv is None: - mv = Version('2.1') + mv = Version('2.2') self.metadata_version = mv return mv @@ -207,6 +208,10 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME self._write_list(file, 'License-File', self.license_files or []) _write_requirements(self, file) + for field, attr in _POSSIBLE_DYNAMIC_FIELDS.items(): + if (val := getattr(self, attr, None)) and not is_static(val): + write_field('Dynamic', field) + long_description = self.get_long_description() if long_description: file.write(f"\n{long_description}") @@ -284,3 +289,33 @@ def _distribution_fullname(name: str, version: str) -> str: canonicalize_name(name).replace('-', '_'), canonicalize_version(version, strip_trailing_zero=False), ) + + +_POSSIBLE_DYNAMIC_FIELDS = { + # Core Metadata Field x related Distribution attribute + "author": "author", + "author-email": "author_email", + "classifier": "classifiers", + "description": "long_description", + "description-content-type": "long_description_content_type", + "download-url": "download_url", + "home-page": "url", + "keywords": "keywords", + "license": "license", + # "license-file": "license_files", # XXX: does PEP 639 exempt Dynamic ?? + "maintainer": "maintainer", + "maintainer-email": "maintainer_email", + "obsoletes": "obsoletes", + # "obsoletes-dist": "obsoletes_dist", # NOT USED + "platform": "platforms", + "project-url": "project_urls", + "provides": "provides", + # "provides-dist": "provides_dist", # NOT USED + "provides-extra": "extras_require", + "requires": "requires", + "requires-dist": "install_requires", + # "requires-external": "requires_external", # NOT USED + "requires-python": "python_requires", + "summary": "description", + # "supported-platform": "supported_platforms", # NOT USED +} diff --git a/contrib/python/setuptools/py3/setuptools/_static.py b/contrib/python/setuptools/py3/setuptools/_static.py new file mode 100644 index 0000000000..075a0bcddf --- /dev/null +++ b/contrib/python/setuptools/py3/setuptools/_static.py @@ -0,0 +1,188 @@ +from functools import wraps +from typing import TypeVar + +import packaging.specifiers + +from .warnings import SetuptoolsDeprecationWarning + + +class Static: + """ + Wrapper for built-in object types that are allow setuptools to identify + static core metadata (in opposition to ``Dynamic``, as defined :pep:`643`). + + The trick is to mark values with :class:`Static` when they come from + ``pyproject.toml`` or ``setup.cfg``, so if any plugin overwrite the value + with a built-in, setuptools will be able to recognise the change. + + We inherit from built-in classes, so that we don't need to change the existing + code base to deal with the new types. + We also should strive for immutability objects to avoid changes after the + initial parsing. + """ + + _mutated_: bool = False # TODO: Remove after deprecation warning is solved + + +def _prevent_modification(target: type, method: str, copying: str) -> None: + """ + Because setuptools is very flexible we cannot fully prevent + plugins and user customisations from modifying static values that were + parsed from config files. + But we can attempt to block "in-place" mutations and identify when they + were done. + """ + fn = getattr(target, method, None) + if fn is None: + return + + @wraps(fn) + def _replacement(self: Static, *args, **kwargs): + # TODO: After deprecation period raise NotImplementedError instead of warning + # which obviated the existence and checks of the `_mutated_` attribute. + self._mutated_ = True + SetuptoolsDeprecationWarning.emit( + "Direct modification of value will be disallowed", + f""" + In an effort to implement PEP 643, direct/in-place changes of static values + that come from configuration files are deprecated. + If you need to modify this value, please first create a copy with {copying} + and make sure conform to all relevant standards when overriding setuptools + functionality (https://packaging.python.org/en/latest/specifications/). + """, + due_date=(2025, 10, 10), # Initially introduced in 2024-09-06 + ) + return fn(self, *args, **kwargs) + + _replacement.__doc__ = "" # otherwise doctest may fail. + setattr(target, method, _replacement) + + +class Str(str, Static): + pass + + +class Tuple(tuple, Static): + pass + + +class List(list, Static): + """ + :meta private: + >>> x = List([1, 2, 3]) + >>> is_static(x) + True + >>> x += [0] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + SetuptoolsDeprecationWarning: Direct modification ... + >>> is_static(x) # no longer static after modification + False + >>> y = list(x) + >>> y.clear() + >>> y + [] + >>> y == x + False + >>> is_static(List(y)) + True + """ + + +# Make `List` immutable-ish +# (certain places of setuptools/distutils issue a warn if we use tuple instead of list) +for _method in ( + '__delitem__', + '__iadd__', + '__setitem__', + 'append', + 'clear', + 'extend', + 'insert', + 'remove', + 'reverse', + 'pop', +): + _prevent_modification(List, _method, "`list(value)`") + + +class Dict(dict, Static): + """ + :meta private: + >>> x = Dict({'a': 1, 'b': 2}) + >>> is_static(x) + True + >>> x['c'] = 0 # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + SetuptoolsDeprecationWarning: Direct modification ... + >>> x._mutated_ + True + >>> is_static(x) # no longer static after modification + False + >>> y = dict(x) + >>> y.popitem() + ('b', 2) + >>> y == x + False + >>> is_static(Dict(y)) + True + """ + + +# Make `Dict` immutable-ish (we cannot inherit from types.MappingProxyType): +for _method in ( + '__delitem__', + '__ior__', + '__setitem__', + 'clear', + 'pop', + 'popitem', + 'setdefault', + 'update', +): + _prevent_modification(Dict, _method, "`dict(value)`") + + +class SpecifierSet(packaging.specifiers.SpecifierSet, Static): + """Not exactly a built-in type but useful for ``requires-python``""" + + +T = TypeVar("T") + + +def noop(value: T) -> T: + """ + >>> noop(42) + 42 + """ + return value + + +_CONVERSIONS = {str: Str, tuple: Tuple, list: List, dict: Dict} + + +def attempt_conversion(value: T) -> T: + """ + >>> is_static(attempt_conversion("hello")) + True + >>> is_static(object()) + False + """ + return _CONVERSIONS.get(type(value), noop)(value) # type: ignore[call-overload] + + +def is_static(value: object) -> bool: + """ + >>> is_static(a := Dict({'a': 1})) + True + >>> is_static(dict(a)) + False + >>> is_static(b := List([1, 2, 3])) + True + >>> is_static(list(b)) + False + """ + return isinstance(value, Static) and not value._mutated_ + + +EMPTY_LIST = List() +EMPTY_DICT = Dict() diff --git a/contrib/python/setuptools/py3/setuptools/config/_apply_pyprojecttoml.py b/contrib/python/setuptools/py3/setuptools/config/_apply_pyprojecttoml.py index c4bbcff730..331596bdd7 100644 --- a/contrib/python/setuptools/py3/setuptools/config/_apply_pyprojecttoml.py +++ b/contrib/python/setuptools/py3/setuptools/config/_apply_pyprojecttoml.py @@ -20,6 +20,7 @@ from itertools import chain from types import MappingProxyType from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union +from .. import _static from .._path import StrPath from ..errors import RemovedConfigError from ..extension import Extension @@ -65,10 +66,11 @@ def apply(dist: Distribution, config: dict, filename: StrPath) -> Distribution: def _apply_project_table(dist: Distribution, config: dict, root_dir: StrPath): - project_table = config.get("project", {}).copy() - if not project_table: + orig_config = config.get("project", {}) + if not orig_config: return # short-circuit + project_table = {k: _static.attempt_conversion(v) for k, v in orig_config.items()} _handle_missing_dynamic(dist, project_table) _unify_entry_points(project_table) @@ -98,7 +100,11 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath): raise RemovedConfigError("\n".join([cleandoc(msg), suggestion])) norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key) - _set_config(dist, norm_key, value) + corresp = TOOL_TABLE_CORRESPONDENCE.get(norm_key, norm_key) + if callable(corresp): + corresp(dist, value) + else: + _set_config(dist, corresp, value) _copy_command_options(config, dist, filename) @@ -143,7 +149,7 @@ def _guess_content_type(file: str) -> str | None: return None if ext in _CONTENT_TYPES: - return _CONTENT_TYPES[ext] + return _static.Str(_CONTENT_TYPES[ext]) valid = ", ".join(f"{k} ({v})" for k, v in _CONTENT_TYPES.items()) msg = f"only the following file extensions are recognized: {valid}." @@ -165,10 +171,11 @@ def _long_description( text = val.get("text") or expand.read_files(file, root_dir) ctype = val["content-type"] - _set_config(dist, "long_description", text) + # XXX: Is it completely safe to assume static? + _set_config(dist, "long_description", _static.Str(text)) if ctype: - _set_config(dist, "long_description_content_type", ctype) + _set_config(dist, "long_description_content_type", _static.Str(ctype)) if file: dist._referenced_files.add(file) @@ -178,10 +185,12 @@ def _license(dist: Distribution, val: dict, root_dir: StrPath | None): from setuptools.config import expand if "file" in val: - _set_config(dist, "license", expand.read_files([val["file"]], root_dir)) + # XXX: Is it completely safe to assume static? + value = expand.read_files([val["file"]], root_dir) + _set_config(dist, "license", _static.Str(value)) dist._referenced_files.add(val["file"]) else: - _set_config(dist, "license", val["text"]) + _set_config(dist, "license", _static.Str(val["text"])) def _people(dist: Distribution, val: list[dict], _root_dir: StrPath | None, kind: str): @@ -197,9 +206,9 @@ def _people(dist: Distribution, val: list[dict], _root_dir: StrPath | None, kind email_field.append(str(addr)) if field: - _set_config(dist, kind, ", ".join(field)) + _set_config(dist, kind, _static.Str(", ".join(field))) if email_field: - _set_config(dist, f"{kind}_email", ", ".join(email_field)) + _set_config(dist, f"{kind}_email", _static.Str(", ".join(email_field))) def _project_urls(dist: Distribution, val: dict, _root_dir: StrPath | None): @@ -207,9 +216,7 @@ def _project_urls(dist: Distribution, val: dict, _root_dir: StrPath | None): def _python_requires(dist: Distribution, val: str, _root_dir: StrPath | None): - from packaging.specifiers import SpecifierSet - - _set_config(dist, "python_requires", SpecifierSet(val)) + _set_config(dist, "python_requires", _static.SpecifierSet(val)) def _dependencies(dist: Distribution, val: list, _root_dir: StrPath | None): @@ -237,9 +244,14 @@ def _noop(_dist: Distribution, val: _T) -> _T: return val +def _identity(val: _T) -> _T: + return val + + def _unify_entry_points(project_table: dict): project = project_table - entry_points = project.pop("entry-points", project.pop("entry_points", {})) + given = project.pop("entry-points", project.pop("entry_points", {})) + entry_points = dict(given) # Avoid problems with static renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"} for key, value in list(project.items()): # eager to allow modifications norm_key = json_compatible_key(key) @@ -333,6 +345,14 @@ def _get_previous_gui_scripts(dist: Distribution) -> list | None: return value.get("gui_scripts") +def _set_static_list_metadata(attr: str, dist: Distribution, val: list) -> None: + """Apply distutils metadata validation but preserve "static" behaviour""" + meta = dist.metadata + setter, getter = getattr(meta, f"set_{attr}"), getattr(meta, f"get_{attr}") + setter(val) + setattr(meta, attr, _static.List(getter())) + + def _attrgetter(attr): """ Similar to ``operator.attrgetter`` but returns None if ``attr`` is not found @@ -386,6 +406,12 @@ TOOL_TABLE_REMOVALS = { See https://packaging.python.org/en/latest/guides/packaging-namespace-packages/. """, } +TOOL_TABLE_CORRESPONDENCE = { + # Fields with corresponding core metadata need to be marked as static: + "obsoletes": partial(_set_static_list_metadata, "obsoletes"), + "provides": partial(_set_static_list_metadata, "provides"), + "platforms": partial(_set_static_list_metadata, "platforms"), +} SETUPTOOLS_PATCHES = { "long_description_content_type", @@ -422,17 +448,17 @@ _PREVIOUSLY_DEFINED = { _RESET_PREVIOUSLY_DEFINED: dict = { # Fix improper setting: given in `setup.py`, but not listed in `dynamic` # dict: pyproject name => value to which reset - "license": {}, - "authors": [], - "maintainers": [], - "keywords": [], - "classifiers": [], - "urls": {}, - "entry-points": {}, - "scripts": {}, - "gui-scripts": {}, - "dependencies": [], - "optional-dependencies": {}, + "license": _static.EMPTY_DICT, + "authors": _static.EMPTY_LIST, + "maintainers": _static.EMPTY_LIST, + "keywords": _static.EMPTY_LIST, + "classifiers": _static.EMPTY_LIST, + "urls": _static.EMPTY_DICT, + "entry-points": _static.EMPTY_DICT, + "scripts": _static.EMPTY_DICT, + "gui-scripts": _static.EMPTY_DICT, + "dependencies": _static.EMPTY_LIST, + "optional-dependencies": _static.EMPTY_DICT, } diff --git a/contrib/python/setuptools/py3/setuptools/config/expand.py b/contrib/python/setuptools/py3/setuptools/config/expand.py index ccb5d63cd2..531f965013 100644 --- a/contrib/python/setuptools/py3/setuptools/config/expand.py +++ b/contrib/python/setuptools/py3/setuptools/config/expand.py @@ -34,6 +34,7 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import TYPE_CHECKING, Any, Callable, TypeVar +from .. import _static from .._path import StrPath, same_path as _same_path from ..discovery import find_package_path from ..warnings import SetuptoolsWarning @@ -181,7 +182,9 @@ def read_attr( spec = _find_spec(module_name, path) try: - return getattr(StaticModule(module_name, spec), attr_name) + value = getattr(StaticModule(module_name, spec), attr_name) + # XXX: Is marking as static contents coming from modules too optimistic? + return _static.attempt_conversion(value) except Exception: # fallback to evaluate module module = _load_spec(spec, module_name) diff --git a/contrib/python/setuptools/py3/setuptools/config/setupcfg.py b/contrib/python/setuptools/py3/setuptools/config/setupcfg.py index 4615815b6b..633aa9d45d 100644 --- a/contrib/python/setuptools/py3/setuptools/config/setupcfg.py +++ b/contrib/python/setuptools/py3/setuptools/config/setupcfg.py @@ -21,9 +21,9 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, cas from packaging.markers import default_environment as marker_env from packaging.requirements import InvalidRequirement, Requirement -from packaging.specifiers import SpecifierSet from packaging.version import InvalidVersion, Version +from .. import _static from .._path import StrPath from ..errors import FileError, OptionError from ..warnings import SetuptoolsDeprecationWarning @@ -367,7 +367,7 @@ class ConfigHandler(Generic[Target]): f'Only strings are accepted for the {key} field, ' 'files are not accepted' ) - return value + return _static.Str(value) return parser @@ -390,12 +390,13 @@ class ConfigHandler(Generic[Target]): return value if not value.startswith(include_directive): - return value + return _static.Str(value) spec = value[len(include_directive) :] filepaths = [path.strip() for path in spec.split(',')] self._referenced_files.update(filepaths) - return expand.read_files(filepaths, root_dir) + # XXX: Is marking as static contents coming from files too optimistic? + return _static.Str(expand.read_files(filepaths, root_dir)) def _parse_attr(self, value, package_dir, root_dir: StrPath): """Represents value as a module attribute. @@ -409,7 +410,7 @@ class ConfigHandler(Generic[Target]): """ attr_directive = 'attr:' if not value.startswith(attr_directive): - return value + return _static.Str(value) attr_desc = value.replace(attr_directive, '') @@ -548,23 +549,29 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]): @property def parsers(self): """Metadata item name to parser function mapping.""" - parse_list = self._parse_list + parse_list_static = self._get_parser_compound(self._parse_list, _static.List) + parse_dict_static = self._get_parser_compound(self._parse_dict, _static.Dict) parse_file = partial(self._parse_file, root_dir=self.root_dir) - parse_dict = self._parse_dict exclude_files_parser = self._exclude_files_parser return { - 'platforms': parse_list, - 'keywords': parse_list, - 'provides': parse_list, - 'obsoletes': parse_list, - 'classifiers': self._get_parser_compound(parse_file, parse_list), + 'author': _static.Str, + 'author_email': _static.Str, + 'maintainer': _static.Str, + 'maintainer_email': _static.Str, + 'platforms': parse_list_static, + 'keywords': parse_list_static, + 'provides': parse_list_static, + 'obsoletes': parse_list_static, + 'classifiers': self._get_parser_compound(parse_file, parse_list_static), 'license': exclude_files_parser('license'), - 'license_files': parse_list, + 'license_files': parse_list_static, 'description': parse_file, 'long_description': parse_file, - 'version': self._parse_version, - 'project_urls': parse_dict, + 'long_description_content_type': _static.Str, + 'version': self._parse_version, # Cannot be marked as dynamic + 'url': _static.Str, + 'project_urls': parse_dict_static, } def _parse_version(self, value): @@ -620,20 +627,20 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): _warn_accidental_env_marker_misconfig(label, value, parsed) # Filter it to only include lines that are not comments. `parse_list` # will have stripped each line and filtered out empties. - return [line for line in parsed if not line.startswith("#")] + return _static.List(line for line in parsed if not line.startswith("#")) + # ^-- Use `_static.List` to mark a non-`Dynamic` Core Metadata @property def parsers(self): """Metadata item name to parser function mapping.""" parse_list = self._parse_list parse_bool = self._parse_bool - parse_dict = self._parse_dict parse_cmdclass = self._parse_cmdclass return { 'zip_safe': parse_bool, 'include_package_data': parse_bool, - 'package_dir': parse_dict, + 'package_dir': self._parse_dict, 'scripts': parse_list, 'eager_resources': parse_list, 'dependency_links': parse_list, @@ -643,14 +650,14 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): "consider using implicit namespaces instead (PEP 420).", # TODO: define due date, see setuptools.dist:check_nsp. ), - 'install_requires': partial( + 'install_requires': partial( # Core Metadata self._parse_requirements_list, "install_requires" ), 'setup_requires': self._parse_list_semicolon, 'packages': self._parse_packages, 'entry_points': self._parse_file_in_root, 'py_modules': parse_list, - 'python_requires': SpecifierSet, + 'python_requires': _static.SpecifierSet, # Core Metadata 'cmdclass': parse_cmdclass, } @@ -727,7 +734,7 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): """ self['exclude_package_data'] = self._parse_package_data(section_options) - def parse_section_extras_require(self, section_options) -> None: + def parse_section_extras_require(self, section_options) -> None: # Core Metadata """Parses `extras_require` configuration file section. :param dict section_options: @@ -737,7 +744,8 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]): lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v), ) - self['extras_require'] = parsed + self['extras_require'] = _static.Dict(parsed) + # ^-- Use `_static.Dict` to mark a non-`Dynamic` Core Metadata def parse_section_data_files(self, section_options) -> None: """Parses `data_files` configuration file section. diff --git a/contrib/python/setuptools/py3/setuptools/dist.py b/contrib/python/setuptools/py3/setuptools/dist.py index f878b2fa45..0249651267 100644 --- a/contrib/python/setuptools/py3/setuptools/dist.py +++ b/contrib/python/setuptools/py3/setuptools/dist.py @@ -19,6 +19,7 @@ from packaging.version import Version from . import ( _entry_points, _reqs, + _static, command as _, # noqa: F401 # imported for side-effects ) from ._importlib import metadata @@ -391,10 +392,15 @@ class Distribution(_Distribution): """Make sure requirement-related attributes exist and are normalized""" install_requires = getattr(self, "install_requires", None) or [] extras_require = getattr(self, "extras_require", None) or {} - self.install_requires = list(map(str, _reqs.parse(install_requires))) - self.extras_require = { - k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items() - } + + # Preserve the "static"-ness of values parsed from config files + list_ = _static.List if _static.is_static(install_requires) else list + self.install_requires = list_(map(str, _reqs.parse(install_requires))) + + dict_ = _static.Dict if _static.is_static(extras_require) else dict + self.extras_require = dict_( + (k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items() + ) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/contrib/python/setuptools/py3/ya.make b/contrib/python/setuptools/py3/ya.make index 85aa187c37..ab5826a9bf 100644 --- a/contrib/python/setuptools/py3/ya.make +++ b/contrib/python/setuptools/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(75.7.0) +VERSION(75.8.0) LICENSE(MIT) @@ -93,6 +93,7 @@ PY_SRCS( setuptools/_path.py setuptools/_reqs.py setuptools/_shutil.py + setuptools/_static.py setuptools/archive_util.py setuptools/build_meta.py setuptools/command/__init__.py |