diff options
| author | robot-piglet <[email protected]> | 2026-02-06 17:53:44 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-02-06 19:01:13 +0300 |
| commit | 3c539da5e7db7e675a202a8551cb3657ec64c193 (patch) | |
| tree | 4cddb3c545c5a4bf4f599a2369d7945d136ae63f /contrib/python/packaging | |
| parent | 3373df6e8cc97499d33a588fe2890571ee447e62 (diff) | |
Intermediate changes
commit_hash:c4352485eb2978cb2b34e9c09ab3231db1290c81
Diffstat (limited to 'contrib/python/packaging')
20 files changed, 1563 insertions, 478 deletions
diff --git a/contrib/python/packaging/py3/.dist-info/METADATA b/contrib/python/packaging/py3/.dist-info/METADATA index 10b290a6cd1..3200e601f97 100644 --- a/contrib/python/packaging/py3/.dist-info/METADATA +++ b/contrib/python/packaging/py3/.dist-info/METADATA @@ -1,14 +1,13 @@ Metadata-Version: 2.4 Name: packaging -Version: 25.0 +Version: 26.0 Summary: Core utilities for Python packages Author-email: Donald Stufft <[email protected]> Requires-Python: >=3.8 Description-Content-Type: text/x-rst +License-Expression: Apache-2.0 OR BSD-2-Clause Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License -Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only @@ -18,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Typing :: Typed @@ -42,7 +42,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`). .. end-intro The ``packaging`` project includes the following: version handling, specifiers, -markers, requirements, tags, utilities. +markers, requirements, tags, metadata, lockfiles, utilities. Documentation ------------- @@ -54,6 +54,8 @@ The `documentation`_ provides information and the API for the following: - Markers - Requirements - Tags +- Metadata +- Lockfiles - Utilities Installation diff --git a/contrib/python/packaging/py3/README.rst b/contrib/python/packaging/py3/README.rst index 4e01206a144..ba3fa462bf1 100644 --- a/contrib/python/packaging/py3/README.rst +++ b/contrib/python/packaging/py3/README.rst @@ -13,7 +13,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`). .. end-intro The ``packaging`` project includes the following: version handling, specifiers, -markers, requirements, tags, utilities. +markers, requirements, tags, metadata, lockfiles, utilities. Documentation ------------- @@ -25,6 +25,8 @@ The `documentation`_ provides information and the API for the following: - Markers - Requirements - Tags +- Metadata +- Lockfiles - Utilities Installation diff --git a/contrib/python/packaging/py3/packaging/__init__.py b/contrib/python/packaging/py3/packaging/__init__.py index d45c22cfd88..21695a74b51 100644 --- a/contrib/python/packaging/py3/packaging/__init__.py +++ b/contrib/python/packaging/py3/packaging/__init__.py @@ -6,7 +6,7 @@ __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "25.0" +__version__ = "26.0" __author__ = "Donald Stufft and individual contributors" __email__ = "[email protected]" diff --git a/contrib/python/packaging/py3/packaging/_elffile.py b/contrib/python/packaging/py3/packaging/_elffile.py index 7a5afc33b0a..497b0645217 100644 --- a/contrib/python/packaging/py3/packaging/_elffile.py +++ b/contrib/python/packaging/py3/packaging/_elffile.py @@ -4,7 +4,6 @@ ELF file parser. This provides a class ``ELFFile`` that parses an ELF executable in a similar interface to ``ZipFile``. Only the read interface is implemented. -Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html """ diff --git a/contrib/python/packaging/py3/packaging/_manylinux.py b/contrib/python/packaging/py3/packaging/_manylinux.py index 95f55762e86..0e79e8a882b 100644 --- a/contrib/python/packaging/py3/packaging/_manylinux.py +++ b/contrib/python/packaging/py3/packaging/_manylinux.py @@ -15,6 +15,16 @@ EF_ARM_ABIMASK = 0xFF000000 EF_ARM_ABI_VER5 = 0x05000000 EF_ARM_ABI_FLOAT_HARD = 0x00000400 +_ALLOWED_ARCHS = { + "x86_64", + "aarch64", + "ppc64", + "ppc64le", + "s390x", + "loongarch64", + "riscv64", +} + # `os.PathLike` not a generic type until Python 3.9, so sticking with `str` # as the type for `path` until then. @@ -57,16 +67,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: return _is_linux_armhf(executable) if "i686" in archs: return _is_linux_i686(executable) - allowed_archs = { - "x86_64", - "aarch64", - "ppc64", - "ppc64le", - "s390x", - "loongarch64", - "riscv64", - } - return any(arch in allowed_archs for arch in archs) + return any(arch in _ALLOWED_ARCHS for arch in archs) # If glibc ever changes its major version, we need to know what the last @@ -106,7 +107,7 @@ def _glibc_version_string_ctypes() -> str | None: Fallback implementation of glibc_version_string using ctypes. """ try: - import ctypes + import ctypes # noqa: PLC0415 except ImportError: return None @@ -150,7 +151,7 @@ def _glibc_version_string() -> str | None: return _glibc_version_string_confstr() or _glibc_version_string_ctypes() -def _parse_glibc_version(version_str: str) -> tuple[int, int]: +def _parse_glibc_version(version_str: str) -> _GLibCVersion: """Parse glibc version. We use a regexp instead of str.split because we want to discard any @@ -165,15 +166,15 @@ def _parse_glibc_version(version_str: str) -> tuple[int, int]: RuntimeWarning, stacklevel=2, ) - return -1, -1 - return int(m.group("major")), int(m.group("minor")) + return _GLibCVersion(-1, -1) + return _GLibCVersion(int(m.group("major")), int(m.group("minor"))) @functools.lru_cache -def _get_glibc_version() -> tuple[int, int]: +def _get_glibc_version() -> _GLibCVersion: version_str = _glibc_version_string() if version_str is None: - return (-1, -1) + return _GLibCVersion(-1, -1) return _parse_glibc_version(version_str) @@ -184,7 +185,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool: return False # Check for presence of _manylinux module. try: - import _manylinux + import _manylinux # noqa: PLC0415 except ImportError: return True if hasattr(_manylinux, "manylinux_compatible"): @@ -192,25 +193,26 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool: if result is not None: return bool(result) return True - if version == _GLibCVersion(2, 5): - if hasattr(_manylinux, "manylinux1_compatible"): - return bool(_manylinux.manylinux1_compatible) - if version == _GLibCVersion(2, 12): - if hasattr(_manylinux, "manylinux2010_compatible"): - return bool(_manylinux.manylinux2010_compatible) - if version == _GLibCVersion(2, 17): - if hasattr(_manylinux, "manylinux2014_compatible"): - return bool(_manylinux.manylinux2014_compatible) + if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"): + return bool(_manylinux.manylinux1_compatible) + if version == _GLibCVersion(2, 12) and hasattr( + _manylinux, "manylinux2010_compatible" + ): + return bool(_manylinux.manylinux2010_compatible) + if version == _GLibCVersion(2, 17) and hasattr( + _manylinux, "manylinux2014_compatible" + ): + return bool(_manylinux.manylinux2014_compatible) return True -_LEGACY_MANYLINUX_MAP = { +_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = { # CentOS 7 w/ glibc 2.17 (PEP 599) - (2, 17): "manylinux2014", + _GLibCVersion(2, 17): "manylinux2014", # CentOS 6 w/ glibc 2.12 (PEP 571) - (2, 12): "manylinux2010", + _GLibCVersion(2, 12): "manylinux2010", # CentOS 5 w/ glibc 2.5 (PEP 513) - (2, 5): "manylinux1", + _GLibCVersion(2, 5): "manylinux1", } @@ -252,11 +254,9 @@ def platform_tags(archs: Sequence[str]) -> Iterator[str]: min_minor = -1 for glibc_minor in range(glibc_max.minor, min_minor, -1): glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) - tag = "manylinux_{}_{}".format(*glibc_version) if _is_compatible(arch, glibc_version): - yield f"{tag}_{arch}" - # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. - if glibc_version in _LEGACY_MANYLINUX_MAP: - legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] - if _is_compatible(arch, glibc_version): + yield "manylinux_{}_{}_{}".format(*glibc_version, arch) + + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version): yield f"{legacy_tag}_{arch}" diff --git a/contrib/python/packaging/py3/packaging/_musllinux.py b/contrib/python/packaging/py3/packaging/_musllinux.py index d2bf30b5631..4e8116a79ca 100644 --- a/contrib/python/packaging/py3/packaging/_musllinux.py +++ b/contrib/python/packaging/py3/packaging/_musllinux.py @@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None: return None if ld is None or "musl" not in ld: return None - proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) + proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True) return _parse_musl_version(proc.stderr) diff --git a/contrib/python/packaging/py3/packaging/_parser.py b/contrib/python/packaging/py3/packaging/_parser.py index 0007c0aa64a..f6c1f5cd226 100644 --- a/contrib/python/packaging/py3/packaging/_parser.py +++ b/contrib/python/packaging/py3/packaging/_parser.py @@ -7,12 +7,14 @@ the implementation. from __future__ import annotations import ast -from typing import NamedTuple, Sequence, Tuple, Union +from typing import List, Literal, NamedTuple, Sequence, Tuple, Union from ._tokenizer import DEFAULT_RULES, Tokenizer class Node: + __slots__ = ("value",) + def __init__(self, value: str) -> None: self.value = value @@ -20,31 +22,38 @@ class Node: return self.value def __repr__(self) -> str: - return f"<{self.__class__.__name__}('{self}')>" + return f"<{self.__class__.__name__}({self.value!r})>" def serialize(self) -> str: raise NotImplementedError class Variable(Node): + __slots__ = () + def serialize(self) -> str: return str(self) class Value(Node): + __slots__ = () + def serialize(self) -> str: return f'"{self}"' class Op(Node): + __slots__ = () + def serialize(self) -> str: return str(self) +MarkerLogical = Literal["and", "or"] MarkerVar = Union[Variable, Value] MarkerItem = Tuple[MarkerVar, Op, MarkerVar] MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]] -MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]] +MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]] class ParsedRequirement(NamedTuple): @@ -111,7 +120,9 @@ def _parse_requirement_details( return (url, specifier, marker) marker = _parse_requirement_marker( - tokenizer, span_start=url_start, after="URL and whitespace" + tokenizer, + span_start=url_start, + expected="semicolon (after URL and whitespace)", ) else: specifier_start = tokenizer.position @@ -124,10 +135,10 @@ def _parse_requirement_details( marker = _parse_requirement_marker( tokenizer, span_start=specifier_start, - after=( - "version specifier" + expected=( + "comma (within version specifier), semicolon (after version specifier)" if specifier - else "name and no valid version specifier" + else "semicolon (after name with no version specifier)" ), ) @@ -135,7 +146,7 @@ def _parse_requirement_details( def _parse_requirement_marker( - tokenizer: Tokenizer, *, span_start: int, after: str + tokenizer: Tokenizer, *, span_start: int, expected: str ) -> MarkerList: """ requirement_marker = SEMICOLON marker WS? @@ -143,8 +154,9 @@ def _parse_requirement_marker( if not tokenizer.check("SEMICOLON"): tokenizer.raise_syntax_error( - f"Expected end or semicolon (after {after})", + f"Expected {expected} or end", span_start=span_start, + span_end=None, ) tokenizer.read() @@ -307,7 +319,7 @@ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem: return (marker_var_left, marker_op, marker_var_right) -def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: +def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503 """ marker_var = VARIABLE | QUOTED_STRING """ diff --git a/contrib/python/packaging/py3/packaging/_structures.py b/contrib/python/packaging/py3/packaging/_structures.py index 90a6465f968..225e2eee012 100644 --- a/contrib/python/packaging/py3/packaging/_structures.py +++ b/contrib/python/packaging/py3/packaging/_structures.py @@ -2,8 +2,13 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. +import typing + class InfinityType: + __slots__ = () + def __repr__(self) -> str: return "Infinity" @@ -32,7 +37,10 @@ class InfinityType: Infinity = InfinityType() class NegativeInfinityType: + __slots__ = () + def __repr__(self) -> str: return "-Infinity" diff --git a/contrib/python/packaging/py3/packaging/_tokenizer.py b/contrib/python/packaging/py3/packaging/_tokenizer.py index d28a9b6cf5d..e6d20dd3f56 100644 --- a/contrib/python/packaging/py3/packaging/_tokenizer.py +++ b/contrib/python/packaging/py3/packaging/_tokenizer.py @@ -3,7 +3,7 @@ from __future__ import annotations import contextlib import re from dataclasses import dataclass -from typing import Iterator, NoReturn +from typing import Generator, Mapping, NoReturn from .specifiers import Specifier @@ -33,16 +33,16 @@ class ParserSyntaxError(Exception): def __str__(self) -> str: marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" - return "\n ".join([self.message, self.source, marker]) + return f"{self.message}\n {self.source}\n {marker}" -DEFAULT_RULES: dict[str, str | re.Pattern[str]] = { - "LEFT_PARENTHESIS": r"\(", - "RIGHT_PARENTHESIS": r"\)", - "LEFT_BRACKET": r"\[", - "RIGHT_BRACKET": r"\]", - "SEMICOLON": r";", - "COMMA": r",", +DEFAULT_RULES: dict[str, re.Pattern[str]] = { + "LEFT_PARENTHESIS": re.compile(r"\("), + "RIGHT_PARENTHESIS": re.compile(r"\)"), + "LEFT_BRACKET": re.compile(r"\["), + "RIGHT_BRACKET": re.compile(r"\]"), + "SEMICOLON": re.compile(r";"), + "COMMA": re.compile(r","), "QUOTED_STRING": re.compile( r""" ( @@ -53,10 +53,10 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = { """, re.VERBOSE, ), - "OP": r"(===|==|~=|!=|<=|>=|<|>)", - "BOOLOP": r"\b(or|and)\b", - "IN": r"\bin\b", - "NOT": r"\bnot\b", + "OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"), + "BOOLOP": re.compile(r"\b(or|and)\b"), + "IN": re.compile(r"\bin\b"), + "NOT": re.compile(r"\bnot\b"), "VARIABLE": re.compile( r""" \b( @@ -78,13 +78,13 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = { Specifier._operator_regex_str + Specifier._version_regex_str, re.VERBOSE | re.IGNORECASE, ), - "AT": r"\@", - "URL": r"[^ \t]+", - "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", - "VERSION_PREFIX_TRAIL": r"\.\*", - "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", - "WS": r"[ \t]+", - "END": r"$", + "AT": re.compile(r"\@"), + "URL": re.compile(r"[^ \t]+"), + "IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"), + "VERSION_PREFIX_TRAIL": re.compile(r"\.\*"), + "VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"), + "WS": re.compile(r"[ \t]+"), + "END": re.compile(r"$"), } @@ -99,12 +99,10 @@ class Tokenizer: self, source: str, *, - rules: dict[str, str | re.Pattern[str]], + rules: Mapping[str, re.Pattern[str]], ) -> None: self.source = source - self.rules: dict[str, re.Pattern[str]] = { - name: re.compile(pattern) for name, pattern in rules.items() - } + self.rules = rules self.next_token: Token | None = None self.position = 0 @@ -174,7 +172,7 @@ class Tokenizer: @contextlib.contextmanager def enclosing_tokens( self, open_token: str, close_token: str, *, around: str - ) -> Iterator[None]: + ) -> Generator[None, None, None]: if self.check(open_token): open_position = self.position self.read() diff --git a/contrib/python/packaging/py3/packaging/licenses/__init__.py b/contrib/python/packaging/py3/packaging/licenses/__init__.py index 6f7f9e6289d..335b275fa75 100644 --- a/contrib/python/packaging/py3/packaging/licenses/__init__.py +++ b/contrib/python/packaging/py3/packaging/licenses/__init__.py @@ -34,7 +34,7 @@ from __future__ import annotations import re from typing import NewType, cast -from packaging.licenses._spdx import EXCEPTIONS, LICENSES +from ._spdx import EXCEPTIONS, LICENSES __all__ = [ "InvalidLicenseExpression", @@ -80,16 +80,21 @@ def canonicalize_license_expression( tokens = license_expression.split() - # Rather than implementing boolean logic, we create an expression that Python can - # parse. Everything that is not involved with the grammar itself is treated as - # `False` and the expression should evaluate as such. + # Rather than implementing a parenthesis/boolean logic parser, create an + # expression that Python can parse. Everything that is not involved with the + # grammar itself is replaced with the placeholder `False` and the resultant + # expression should become a valid Python expression. python_tokens = [] for token in tokens: if token not in {"or", "and", "with", "(", ")"}: python_tokens.append("False") elif token == "with": python_tokens.append("or") - elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}: + elif ( + token == "(" + and python_tokens + and python_tokens[-1] not in {"or", "and", "("} + ) or (token == ")" and python_tokens and python_tokens[-1] == "("): message = f"Invalid license expression: {raw_license_expression!r}" raise InvalidLicenseExpression(message) else: @@ -97,11 +102,8 @@ def canonicalize_license_expression( python_expression = " ".join(python_tokens) try: - invalid = eval(python_expression, globals(), locals()) - except Exception: - invalid = True - - if invalid is not False: + compile(python_expression, "", "eval") + except SyntaxError: message = f"Invalid license expression: {raw_license_expression!r}" raise InvalidLicenseExpression(message) from None @@ -140,6 +142,6 @@ def canonicalize_license_expression( normalized_expression = " ".join(normalized_tokens) return cast( - NormalizedLicenseExpression, + "NormalizedLicenseExpression", normalized_expression.replace("( ", "(").replace(" )", ")"), ) diff --git a/contrib/python/packaging/py3/packaging/licenses/_spdx.py b/contrib/python/packaging/py3/packaging/licenses/_spdx.py index eac22276a34..a277af28220 100644 --- a/contrib/python/packaging/py3/packaging/licenses/_spdx.py +++ b/contrib/python/packaging/py3/packaging/licenses/_spdx.py @@ -12,7 +12,7 @@ class SPDXException(TypedDict): deprecated: bool -VERSION = '3.25.0' +VERSION = '3.27.0' LICENSES: dict[str, SPDXLicense] = { '0bsd': {'id': '0BSD', 'deprecated': False}, @@ -46,6 +46,7 @@ LICENSES: dict[str, SPDXLicense] = { 'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False}, 'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False}, 'any-osi': {'id': 'any-OSI', 'deprecated': False}, + 'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False}, 'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False}, 'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False}, 'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False}, @@ -61,6 +62,8 @@ LICENSES: dict[str, SPDXLicense] = { 'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False}, 'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False}, 'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False}, + 'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False}, + 'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False}, 'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False}, 'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False}, 'baekmuk': {'id': 'Baekmuk', 'deprecated': False}, @@ -75,6 +78,7 @@ LICENSES: dict[str, SPDXLicense] = { 'blessing': {'id': 'blessing', 'deprecated': False}, 'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False}, 'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False}, + 'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False}, 'borceux': {'id': 'Borceux', 'deprecated': False}, 'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False}, 'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False}, @@ -85,6 +89,7 @@ LICENSES: dict[str, SPDXLicense] = { 'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True}, 'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True}, 'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False}, + 'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False}, 'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False}, 'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False}, 'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False}, @@ -176,6 +181,8 @@ LICENSES: dict[str, SPDXLicense] = { 'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False}, 'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False}, 'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False}, + 'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False}, + 'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False}, 'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False}, 'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False}, 'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False}, @@ -215,6 +222,7 @@ LICENSES: dict[str, SPDXLicense] = { 'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False}, 'cronyx': {'id': 'Cronyx', 'deprecated': False}, 'crossword': {'id': 'Crossword', 'deprecated': False}, + 'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False}, 'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False}, 'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False}, 'cube': {'id': 'Cube', 'deprecated': False}, @@ -226,7 +234,9 @@ LICENSES: dict[str, SPDXLicense] = { 'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False}, 'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False}, 'doc': {'id': 'DOC', 'deprecated': False}, + 'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False}, 'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False}, + 'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False}, 'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False}, 'dotseqn': {'id': 'Dotseqn', 'deprecated': False}, 'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False}, @@ -263,12 +273,17 @@ LICENSES: dict[str, SPDXLicense] = { 'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False}, 'fsful': {'id': 'FSFUL', 'deprecated': False}, 'fsfullr': {'id': 'FSFULLR', 'deprecated': False}, + 'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False}, 'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False}, + 'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False}, + 'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False}, 'ftl': {'id': 'FTL', 'deprecated': False}, 'furuseth': {'id': 'Furuseth', 'deprecated': False}, 'fwlw': {'id': 'fwlw', 'deprecated': False}, + 'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False}, 'gcr-docs': {'id': 'GCR-docs', 'deprecated': False}, 'gd': {'id': 'GD', 'deprecated': False}, + 'generic-xts': {'id': 'generic-xts', 'deprecated': False}, 'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True}, 'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False}, 'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False}, @@ -320,6 +335,7 @@ LICENSES: dict[str, SPDXLicense] = { 'gtkbook': {'id': 'gtkbook', 'deprecated': False}, 'gutmann': {'id': 'Gutmann', 'deprecated': False}, 'haskellreport': {'id': 'HaskellReport', 'deprecated': False}, + 'hdf5': {'id': 'HDF5', 'deprecated': False}, 'hdparm': {'id': 'hdparm', 'deprecated': False}, 'hidapi': {'id': 'HIDAPI', 'deprecated': False}, 'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False}, @@ -360,6 +376,7 @@ LICENSES: dict[str, SPDXLicense] = { 'imlib2': {'id': 'Imlib2', 'deprecated': False}, 'info-zip': {'id': 'Info-ZIP', 'deprecated': False}, 'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False}, + 'innosetup': {'id': 'InnoSetup', 'deprecated': False}, 'intel': {'id': 'Intel', 'deprecated': False}, 'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False}, 'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False}, @@ -369,6 +386,7 @@ LICENSES: dict[str, SPDXLicense] = { 'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False}, 'jam': {'id': 'Jam', 'deprecated': False}, 'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False}, + 'jove': {'id': 'jove', 'deprecated': False}, 'jpl-image': {'id': 'JPL-image', 'deprecated': False}, 'jpnic': {'id': 'JPNIC', 'deprecated': False}, 'json': {'id': 'JSON', 'deprecated': False}, @@ -394,6 +412,7 @@ LICENSES: dict[str, SPDXLicense] = { 'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False}, 'lgpllr': {'id': 'LGPLLR', 'deprecated': False}, 'libpng': {'id': 'Libpng', 'deprecated': False}, + 'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False}, 'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False}, 'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False}, 'libtiff': {'id': 'libtiff', 'deprecated': False}, @@ -424,14 +443,17 @@ LICENSES: dict[str, SPDXLicense] = { 'magaz': {'id': 'magaz', 'deprecated': False}, 'mailprio': {'id': 'mailprio', 'deprecated': False}, 'makeindex': {'id': 'MakeIndex', 'deprecated': False}, + 'man2html': {'id': 'man2html', 'deprecated': False}, 'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False}, 'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False}, 'metamail': {'id': 'metamail', 'deprecated': False}, 'minpack': {'id': 'Minpack', 'deprecated': False}, + 'mips': {'id': 'MIPS', 'deprecated': False}, 'miros': {'id': 'MirOS', 'deprecated': False}, 'mit': {'id': 'MIT', 'deprecated': False}, 'mit-0': {'id': 'MIT-0', 'deprecated': False}, 'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False}, + 'mit-click': {'id': 'MIT-Click', 'deprecated': False}, 'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False}, 'mit-enna': {'id': 'MIT-enna', 'deprecated': False}, 'mit-feh': {'id': 'MIT-feh', 'deprecated': False}, @@ -472,6 +494,7 @@ LICENSES: dict[str, SPDXLicense] = { 'netcdf': {'id': 'NetCDF', 'deprecated': False}, 'newsletr': {'id': 'Newsletr', 'deprecated': False}, 'ngpl': {'id': 'NGPL', 'deprecated': False}, + 'ngrep': {'id': 'ngrep', 'deprecated': False}, 'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False}, 'nist-pd': {'id': 'NIST-PD', 'deprecated': False}, 'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False}, @@ -486,6 +509,7 @@ LICENSES: dict[str, SPDXLicense] = { 'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False}, 'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False}, 'nrl': {'id': 'NRL', 'deprecated': False}, + 'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False}, 'ntp': {'id': 'NTP', 'deprecated': False}, 'ntp-0': {'id': 'NTP-0', 'deprecated': False}, 'nunit': {'id': 'Nunit', 'deprecated': True}, @@ -580,6 +604,7 @@ LICENSES: dict[str, SPDXLicense] = { 'schemereport': {'id': 'SchemeReport', 'deprecated': False}, 'sendmail': {'id': 'Sendmail', 'deprecated': False}, 'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False}, + 'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False}, 'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False}, 'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False}, 'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False}, @@ -592,10 +617,12 @@ LICENSES: dict[str, SPDXLicense] = { 'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False}, 'sl': {'id': 'SL', 'deprecated': False}, 'sleepycat': {'id': 'Sleepycat', 'deprecated': False}, + 'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False}, 'smlnj': {'id': 'SMLNJ', 'deprecated': False}, 'smppl': {'id': 'SMPPL', 'deprecated': False}, 'snia': {'id': 'SNIA', 'deprecated': False}, 'snprintf': {'id': 'snprintf', 'deprecated': False}, + 'sofa': {'id': 'SOFA', 'deprecated': False}, 'softsurfer': {'id': 'softSurfer', 'deprecated': False}, 'soundex': {'id': 'Soundex', 'deprecated': False}, 'spencer-86': {'id': 'Spencer-86', 'deprecated': False}, @@ -609,6 +636,7 @@ LICENSES: dict[str, SPDXLicense] = { 'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False}, 'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True}, 'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False}, + 'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False}, 'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False}, 'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False}, 'sunpro': {'id': 'SunPro', 'deprecated': False}, @@ -620,12 +648,14 @@ LICENSES: dict[str, SPDXLicense] = { 'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False}, 'termreadkey': {'id': 'TermReadKey', 'deprecated': False}, 'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False}, + 'thirdeye': {'id': 'ThirdEye', 'deprecated': False}, 'threeparttable': {'id': 'threeparttable', 'deprecated': False}, 'tmate': {'id': 'TMate', 'deprecated': False}, 'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False}, 'tosl': {'id': 'TOSL', 'deprecated': False}, 'tpdl': {'id': 'TPDL', 'deprecated': False}, 'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False}, + 'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False}, 'ttwl': {'id': 'TTWL', 'deprecated': False}, 'ttyp0': {'id': 'TTYP0', 'deprecated': False}, 'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False}, @@ -641,6 +671,8 @@ LICENSES: dict[str, SPDXLicense] = { 'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False}, 'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False}, 'unlicense': {'id': 'Unlicense', 'deprecated': False}, + 'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False}, + 'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False}, 'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False}, 'urt-rle': {'id': 'URT-RLE', 'deprecated': False}, 'vim': {'id': 'Vim', 'deprecated': False}, @@ -654,6 +686,7 @@ LICENSES: dict[str, SPDXLicense] = { 'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False}, 'wsuipa': {'id': 'Wsuipa', 'deprecated': False}, 'wtfpl': {'id': 'WTFPL', 'deprecated': False}, + 'wwl': {'id': 'wwl', 'deprecated': False}, 'wxwindows': {'id': 'wxWindows', 'deprecated': True}, 'x11': {'id': 'X11', 'deprecated': False}, 'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False}, @@ -695,9 +728,11 @@ EXCEPTIONS: dict[str, SPDXException] = { 'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False}, 'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False}, 'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False}, + 'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False}, 'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False}, 'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False}, 'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False}, + 'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False}, 'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False}, 'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False}, 'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False}, @@ -714,13 +749,16 @@ EXCEPTIONS: dict[str, SPDXException] = { 'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False}, 'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False}, 'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False}, + 'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False}, 'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False}, 'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False}, 'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False}, 'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False}, 'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False}, 'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False}, + 'harbour-exception': {'id': 'harbour-exception', 'deprecated': False}, 'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False}, + 'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False}, 'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False}, 'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False}, 'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False}, @@ -730,12 +768,14 @@ EXCEPTIONS: dict[str, SPDXException] = { 'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False}, 'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False}, 'mif-exception': {'id': 'mif-exception', 'deprecated': False}, + 'mxml-exception': {'id': 'mxml-exception', 'deprecated': False}, 'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True}, 'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False}, 'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False}, 'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False}, 'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False}, 'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False}, + 'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False}, 'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False}, 'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False}, 'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False}, diff --git a/contrib/python/packaging/py3/packaging/markers.py b/contrib/python/packaging/py3/packaging/markers.py index e7cea57297a..ca3706fe492 100644 --- a/contrib/python/packaging/py3/packaging/markers.py +++ b/contrib/python/packaging/py3/packaging/markers.py @@ -8,7 +8,7 @@ import operator import os import platform import sys -from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast +from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast from ._parser import MarkerAtom, MarkerList, Op, Value, Variable from ._parser import parse_marker as _parse_marker @@ -17,6 +17,7 @@ from .specifiers import InvalidSpecifier, Specifier from .utils import canonicalize_name __all__ = [ + "Environment", "EvaluateContext", "InvalidMarker", "Marker", @@ -28,6 +29,12 @@ __all__ = [ Operator = Callable[[str, Union[str, AbstractSet[str]]], bool] EvaluateContext = Literal["metadata", "lock_file", "requirement"] MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} +MARKERS_REQUIRING_VERSION = { + "implementation_version", + "platform_release", + "python_full_version", + "python_version", +} class InvalidMarker(ValueError): @@ -121,20 +128,28 @@ class Environment(TypedDict): """ -def _normalize_extra_values(results: Any) -> Any: +def _normalize_extras( + result: MarkerList | MarkerAtom | str, +) -> MarkerList | MarkerAtom | str: + if not isinstance(result, tuple): + return result + + lhs, op, rhs = result + if isinstance(lhs, Variable) and lhs.value == "extra": + normalized_extra = canonicalize_name(rhs.value) + rhs = Value(normalized_extra) + elif isinstance(rhs, Variable) and rhs.value == "extra": + normalized_extra = canonicalize_name(lhs.value) + lhs = Value(normalized_extra) + return lhs, op, rhs + + +def _normalize_extra_values(results: MarkerList) -> MarkerList: """ Normalize extra values. """ - if isinstance(results[0], tuple): - lhs, op, rhs = results[0] - if isinstance(lhs, Variable) and lhs.value == "extra": - normalized_extra = canonicalize_name(rhs.value) - rhs = Value(normalized_extra) - elif isinstance(rhs, Variable) and rhs.value == "extra": - normalized_extra = canonicalize_name(lhs.value) - lhs = Value(normalized_extra) - results[0] = lhs, op, rhs - return results + + return [_normalize_extras(r) for r in results] def _format_marker( @@ -168,25 +183,26 @@ def _format_marker( _operators: dict[str, Operator] = { "in": lambda lhs, rhs: lhs in rhs, "not in": lambda lhs, rhs: lhs not in rhs, - "<": operator.lt, - "<=": operator.le, + "<": lambda _lhs, _rhs: False, + "<=": operator.eq, "==": operator.eq, "!=": operator.ne, - ">=": operator.ge, - ">": operator.gt, + ">=": operator.eq, + ">": lambda _lhs, _rhs: False, } -def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool: - if isinstance(rhs, str): +def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool: + op_str = op.serialize() + if key in MARKERS_REQUIRING_VERSION: try: - spec = Specifier("".join([op.serialize(), rhs])) + spec = Specifier(f"{op_str}{rhs}") except InvalidSpecifier: pass else: return spec.contains(lhs, prereleases=True) - oper: Operator | None = _operators.get(op.serialize()) + oper: Operator | None = _operators.get(op_str) if oper is None: raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") @@ -196,13 +212,14 @@ def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool: def _normalize( lhs: str, rhs: str | AbstractSet[str], key: str ) -> tuple[str, str | AbstractSet[str]]: - # PEP 685 – Comparison of extra names for optional distribution dependencies + # PEP 685 - Comparison of extra names for optional distribution dependencies # https://peps.python.org/pep-0685/ # > When comparing extra names, tools MUST normalize the names being # > compared using the semantics outlined in PEP 503 for names if key == "extra": assert isinstance(rhs, str), "extra value must be a string" - return (canonicalize_name(lhs), canonicalize_name(rhs)) + # Both sides are normalized at this point already + return (lhs, rhs) if key in MARKERS_ALLOWING_SET: if isinstance(rhs, str): # pragma: no cover return (canonicalize_name(lhs), canonicalize_name(rhs)) @@ -219,8 +236,6 @@ def _evaluate_markers( groups: list[list[bool]] = [[]] for marker in markers: - assert isinstance(marker, (list, tuple, str)) - if isinstance(marker, list): groups[-1].append(_evaluate_markers(marker, environment)) elif isinstance(marker, tuple): @@ -234,13 +249,16 @@ def _evaluate_markers( lhs_value = lhs.value environment_key = rhs.value rhs_value = environment[environment_key] + assert isinstance(lhs_value, str), "lhs must be a string" lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) - groups[-1].append(_eval_op(lhs_value, op, rhs_value)) - else: - assert marker in ["and", "or"] - if marker == "or": - groups.append([]) + groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key)) + elif marker == "or": + groups.append([]) + elif marker == "and": + pass + else: # pragma: nocover + raise TypeError(f"Unexpected marker {marker!r}") return any(all(item) for item in groups) @@ -276,6 +294,11 @@ class Marker: # Note: We create a Marker object without calling this constructor in # packaging.requirements.Requirement. If any additional logic is # added here, make sure to mirror/adapt Requirement. + + # If this fails and throws an error, the repr still expects _markers to + # be defined. + self._markers: MarkerList = [] + try: self._markers = _normalize_extra_values(_parse_marker(marker)) # The attribute `_markers` can be described in terms of a recursive type: @@ -301,12 +324,12 @@ class Marker: return _format_marker(self._markers) def __repr__(self) -> str: - return f"<Marker('{self}')>" + return f"<{self.__class__.__name__}('{self}')>" def __hash__(self) -> int: - return hash((self.__class__.__name__, str(self))) + return hash(str(self)) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, Marker): return NotImplemented @@ -314,7 +337,7 @@ class Marker: def evaluate( self, - environment: dict[str, str] | None = None, + environment: Mapping[str, str | AbstractSet[str]] | None = None, context: EvaluateContext = "metadata", ) -> bool: """Evaluate a marker. @@ -337,12 +360,15 @@ class Marker: ) elif context == "metadata": current_environment["extra"] = "" + if environment is not None: current_environment.update(environment) - # The API used to allow setting extra to None. We need to handle this - # case for backwards compatibility. - if "extra" in current_environment and current_environment["extra"] is None: - current_environment["extra"] = "" + if "extra" in current_environment: + # The API used to allow setting extra to None. We need to handle + # this case for backwards compatibility. Also skip running + # normalize name if extra is empty. + extra = cast("str | None", current_environment["extra"]) + current_environment["extra"] = canonicalize_name(extra) if extra else "" return _evaluate_markers( self._markers, _repair_python_full_version(current_environment) @@ -356,7 +382,7 @@ def _repair_python_full_version( Work around platform.python_version() returning something that is not PEP 440 compliant for non-tagged Python builds. """ - python_full_version = cast(str, env["python_full_version"]) + python_full_version = cast("str", env["python_full_version"]) if python_full_version.endswith("+"): env["python_full_version"] = f"{python_full_version}local" return env diff --git a/contrib/python/packaging/py3/packaging/metadata.py b/contrib/python/packaging/py3/packaging/metadata.py index 3bd8602d36c..253f6b1b7eb 100644 --- a/contrib/python/packaging/py3/packaging/metadata.py +++ b/contrib/python/packaging/py3/packaging/metadata.py @@ -5,6 +5,7 @@ import email.header import email.message import email.parser import email.policy +import keyword import pathlib import sys import typing @@ -19,13 +20,15 @@ from typing import ( from . import licenses, requirements, specifiers, utils from . import version as version_module -from .licenses import NormalizedLicenseExpression + +if typing.TYPE_CHECKING: + from .licenses import NormalizedLicenseExpression T = typing.TypeVar("T") if sys.version_info >= (3, 11): # pragma: no cover - ExceptionGroup = ExceptionGroup + ExceptionGroup = ExceptionGroup # noqa: F821 else: # pragma: no cover class ExceptionGroup(Exception): @@ -126,13 +129,19 @@ class RawMetadata(TypedDict, total=False): # Metadata 2.3 - PEP 685 # No new fields were added in PEP 685, just some edge case were - # tightened up to provide better interoptability. + # tightened up to provide better interoperability. # Metadata 2.4 - PEP 639 license_expression: str license_files: list[str] + # Metadata 2.5 - PEP 794 + import_names: list[str] + import_namespaces: list[str] + +# 'keywords' is special as it's a string in the core metadata spec, but we +# represent it as a list. _STRING_FIELDS = { "author", "author_email", @@ -165,6 +174,8 @@ _LIST_FIELDS = { "requires_dist", "requires_external", "supported_platforms", + "import_names", + "import_namespaces", } _DICT_FIELDS = { @@ -193,24 +204,23 @@ def _parse_project_urls(data: list[str]) -> dict[str, str]: # be the missing value, then they'd have multiple '' values that # overwrite each other in a accumulating dict. # - # The other potentional issue is that it's possible to have the + # The other potential issue is that it's possible to have the # same label multiple times in the metadata, with no solid "right" # answer with what to do in that case. As such, we'll do the only - # thing we can, which is treat the field as unparseable and add it + # thing we can, which is treat the field as unparsable and add it # to our list of unparsed fields. - parts = [p.strip() for p in pair.split(",", 1)] - parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items - + # # TODO: The spec doesn't say anything about if the keys should be # considered case sensitive or not... logically they should # be case-preserving and case-insensitive, but doing that # would open up more cases where we might have duplicate # entries. - label, url = parts + label, _, url = (s.strip() for s in pair.partition(",")) + if label in urls: # The label already exists in our set of urls, so this field - # is unparseable, and we can just add the whole thing to our - # unparseable data and stop processing it. + # is unparsable, and we can just add the whole thing to our + # unparsable data and stop processing it. raise KeyError("duplicate labels in project urls") urls[label] = url @@ -257,6 +267,8 @@ _EMAIL_TO_RAW_MAPPING = { "download-url": "download_url", "dynamic": "dynamic", "home-page": "home_page", + "import-name": "import_names", + "import-namespace": "import_namespaces", "keywords": "keywords", "license": "license", "license-expression": "license_expression", @@ -283,6 +295,45 @@ _EMAIL_TO_RAW_MAPPING = { _RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()} +# This class is for writing RFC822 messages +class RFC822Policy(email.policy.EmailPolicy): + """ + This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse`` + implementation that handles multi-line values, and some nice defaults. + """ + + utf8 = True + mangle_from_ = False + max_line_length = 0 + + def header_store_parse(self, name: str, value: str) -> tuple[str, str]: + size = len(name) + 2 + value = value.replace("\n", "\n" + " " * size) + return (name, value) + + +# This class is for writing RFC822 messages +class RFC822Message(email.message.EmailMessage): + """ + This is :class:`email.message.EmailMessage` with two small changes: it defaults to + our `RFC822Policy`, and it correctly writes unicode when being called + with `bytes()`. + """ + + def __init__(self) -> None: + super().__init__(policy=RFC822Policy()) + + def as_bytes( + self, unixfrom: bool = False, policy: email.policy.Policy | None = None + ) -> bytes: + """ + Return the bytes representation of the message. + + This handles unicode encoding. + """ + return self.as_string(unixfrom, policy=policy).encode("utf-8") + + def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``). @@ -310,10 +361,10 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: # We have to wrap parsed.keys() in a set, because in the case of multiple # values for a key (a list), the key will appear multiple times in the # list of keys, but we're avoiding that by using get_all(). - for name in frozenset(parsed.keys()): + for name_with_case in frozenset(parsed.keys()): # Header names in RFC are case insensitive, so we'll normalize to all # lower case to make comparisons easier. - name = name.lower() + name = name_with_case.lower() # We use get_all() here, even for fields that aren't multiple use, # because otherwise someone could have e.g. two Name fields, and we @@ -349,16 +400,16 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: # can be independently encoded, so we'll need to check each # of them. chunks: list[tuple[bytes, str | None]] = [] - for bin, encoding in email.header.decode_header(h): + for binary, _encoding in email.header.decode_header(h): try: - bin.decode("utf8", "strict") + binary.decode("utf8", "strict") except UnicodeDecodeError: # Enable mojibake. encoding = "latin1" valid_encoding = False else: encoding = "utf8" - chunks.append((bin, encoding)) + chunks.append((binary, encoding)) # Turn our chunks back into a Header object, then let that # Header object do the right thing to turn them into a @@ -397,6 +448,11 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: # of unparsed stuff. if raw_name in _STRING_FIELDS and len(value) == 1: raw[raw_name] = value[0] + # If this is import_names, we need to special case the empty field + # case, which converts to an empty list instead of None. We can't let + # the empty case slip through, as it will fail validation. + elif raw_name == "import_names" and value == [""]: + raw[raw_name] = [] # If this is one of our list of string fields, then we can just assign # the value, since email *only* has strings, and our get_all() call # above ensures that this is a list. @@ -424,7 +480,7 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: except KeyError: unparsed[name] = value # Nothing that we've done has managed to parse this, so it'll just - # throw it in our unparseable data and move on. + # throw it in our unparsable data and move on. else: unparsed[name] = value @@ -441,9 +497,9 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: else: if payload: # Check to see if we've already got a description, if so then both - # it, and this body move to unparseable. + # it, and this body move to unparsable. if "description" in raw: - description_header = cast(str, raw.pop("description")) + description_header = cast("str", raw.pop("description")) unparsed.setdefault("description", []).extend( [description_header, payload] ) @@ -456,15 +512,15 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: # literal key names, but we're computing our key names on purpose, but the # way this function is implemented, our `TypedDict` can only have valid key # names. - return cast(RawMetadata, raw), unparsed + return cast("RawMetadata", raw), unparsed _NOT_FOUND = object() # Keep the two values in sync. -_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"] -_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"] +_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] +_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] _REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) @@ -519,7 +575,7 @@ class _Validator(Generic[T]): except KeyError: pass - return cast(T, value) + return cast("T", value) def _invalid_metadata( self, msg: str, cause: Exception | None = None @@ -534,7 +590,7 @@ class _Validator(Generic[T]): # Implicitly makes Metadata-Version required. if value not in _VALID_METADATA_VERSIONS: raise self._invalid_metadata(f"{value!r} is not a valid metadata version") - return cast(_MetadataVersion, value) + return cast("_MetadataVersion", value) def _process_name(self, value: str) -> str: if not value: @@ -647,9 +703,7 @@ class _Validator(Generic[T]): else: return reqs - def _process_license_expression( - self, value: str - ) -> NormalizedLicenseExpression | None: + def _process_license_expression(self, value: str) -> NormalizedLicenseExpression: try: return licenses.canonicalize_license_expression(value) except ValueError as exc: @@ -683,6 +737,30 @@ class _Validator(Generic[T]): paths.append(path) return paths + def _process_import_names(self, value: list[str]) -> list[str]: + for import_name in value: + name, semicolon, private = import_name.partition(";") + name = name.rstrip() + for identifier in name.split("."): + if not identifier.isidentifier(): + raise self._invalid_metadata( + f"{name!r} is invalid for {{field}}; " + f"{identifier!r} is not a valid identifier" + ) + elif keyword.iskeyword(identifier): + raise self._invalid_metadata( + f"{name!r} is invalid for {{field}}; " + f"{identifier!r} is a keyword" + ) + if semicolon and private.lstrip() != "private": + raise self._invalid_metadata( + f"{import_name!r} is invalid for {{field}}; " + "the only valid option is 'private'" + ) + return value + + _process_import_namespaces = _process_import_names + class Metadata: """Representation of distribution metadata. @@ -854,9 +932,47 @@ class Metadata: """:external:ref:`core-metadata-provides-dist`""" obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2") """:external:ref:`core-metadata-obsoletes-dist`""" + import_names: _Validator[list[str] | None] = _Validator(added="2.5") + """:external:ref:`core-metadata-import-name`""" + import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5") + """:external:ref:`core-metadata-import-namespace`""" requires: _Validator[list[str] | None] = _Validator(added="1.1") """``Requires`` (deprecated)""" provides: _Validator[list[str] | None] = _Validator(added="1.1") """``Provides`` (deprecated)""" obsoletes: _Validator[list[str] | None] = _Validator(added="1.1") """``Obsoletes`` (deprecated)""" + + def as_rfc822(self) -> RFC822Message: + """ + Return an RFC822 message with the metadata. + """ + message = RFC822Message() + self._write_metadata(message) + return message + + def _write_metadata(self, message: RFC822Message) -> None: + """ + Return an RFC822 message with the metadata. + """ + for name, validator in self.__class__.__dict__.items(): + if isinstance(validator, _Validator) and name != "description": + value = getattr(self, name) + email_name = _RAW_TO_EMAIL_MAPPING[name] + if value is not None: + if email_name == "project-url": + for label, url in value.items(): + message[email_name] = f"{label}, {url}" + elif email_name == "keywords": + message[email_name] = ",".join(value) + elif email_name == "import-name" and value == []: + message[email_name] = "" + elif isinstance(value, list): + for item in value: + message[email_name] = str(item) + else: + message[email_name] = str(value) + + # The description is a special case because it is in the body of the message. + if self.description is not None: + message.set_payload(self.description) diff --git a/contrib/python/packaging/py3/packaging/pylock.py b/contrib/python/packaging/py3/packaging/pylock.py new file mode 100644 index 00000000000..a564f15246a --- /dev/null +++ b/contrib/python/packaging/py3/packaging/pylock.py @@ -0,0 +1,635 @@ +from __future__ import annotations + +import dataclasses +import logging +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from datetime import datetime +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Protocol, + TypeVar, +) + +from .markers import Marker +from .specifiers import SpecifierSet +from .utils import NormalizedName, is_normalized_name +from .version import Version + +if TYPE_CHECKING: # pragma: no cover + from pathlib import Path + + from typing_extensions import Self + +_logger = logging.getLogger(__name__) + +__all__ = [ + "Package", + "PackageArchive", + "PackageDirectory", + "PackageSdist", + "PackageVcs", + "PackageWheel", + "Pylock", + "PylockUnsupportedVersionError", + "PylockValidationError", + "is_valid_pylock_path", +] + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") + + +class _FromMappingProtocol(Protocol): # pragma: no cover + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: ... + + +_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol) + + +_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$") + + +def is_valid_pylock_path(path: Path) -> bool: + """Check if the given path is a valid pylock file path.""" + return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name)) + + +def _toml_key(key: str) -> str: + return key.replace("_", "-") + + +def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401 + if isinstance(value, (Version, Marker, SpecifierSet)): + return str(value) + if isinstance(value, Sequence) and key == "environments": + return [str(v) for v in value] + return value + + +def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]: + return { + _toml_key(key): _toml_value(key, value) + for key, value in data + if value is not None + } + + +def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None: + """Get a value from the dictionary and verify it's the expected type.""" + if (value := d.get(key)) is None: + return None + if not isinstance(value, expected_type): + raise PylockValidationError( + f"Unexpected type {type(value).__name__} " + f"(expected {expected_type.__name__})", + context=key, + ) + return value + + +def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T: + """Get a required value from the dictionary and verify it's the expected type.""" + if (value := _get(d, expected_type, key)) is None: + raise _PylockRequiredKeyError(key) + return value + + +def _get_sequence( + d: Mapping[str, Any], expected_item_type: type[_T], key: str +) -> Sequence[_T] | None: + """Get a list value from the dictionary and verify it's the expected items type.""" + if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract] + return None + if isinstance(value, (str, bytes)): + # special case: str and bytes are Sequences, but we want to reject it + raise PylockValidationError( + f"Unexpected type {type(value).__name__} (expected Sequence)", + context=key, + ) + for i, item in enumerate(value): + if not isinstance(item, expected_item_type): + raise PylockValidationError( + f"Unexpected type {type(item).__name__} " + f"(expected {expected_item_type.__name__})", + context=f"{key}[{i}]", + ) + return value + + +def _get_as( + d: Mapping[str, Any], + expected_type: type[_T], + target_type: Callable[[_T], _T2], + key: str, +) -> _T2 | None: + """Get a value from the dictionary, verify it's the expected type, + and convert to the target type. + + This assumes the target_type constructor accepts the value. + """ + if (value := _get(d, expected_type, key)) is None: + return None + try: + return target_type(value) + except Exception as e: + raise PylockValidationError(e, context=key) from e + + +def _get_required_as( + d: Mapping[str, Any], + expected_type: type[_T], + target_type: Callable[[_T], _T2], + key: str, +) -> _T2: + """Get a required value from the dict, verify it's the expected type, + and convert to the target type.""" + if (value := _get_as(d, expected_type, target_type, key)) is None: + raise _PylockRequiredKeyError(key) + return value + + +def _get_sequence_as( + d: Mapping[str, Any], + expected_item_type: type[_T], + target_item_type: Callable[[_T], _T2], + key: str, +) -> list[_T2] | None: + """Get list value from dictionary and verify expected items type.""" + if (value := _get_sequence(d, expected_item_type, key)) is None: + return None + result = [] + try: + for item in value: + typed_item = target_item_type(item) + result.append(typed_item) + except Exception as e: + raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e + return result + + +def _get_object( + d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str +) -> _FromMappingProtocolT | None: + """Get a dictionary value from the dictionary and convert it to a dataclass.""" + if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract] + return None + try: + return target_type._from_dict(value) + except Exception as e: + raise PylockValidationError(e, context=key) from e + + +def _get_sequence_of_objects( + d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str +) -> list[_FromMappingProtocolT] | None: + """Get a list value from the dictionary and convert its items to a dataclass.""" + if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract] + return None + result: list[_FromMappingProtocolT] = [] + try: + for item in value: + typed_item = target_item_type._from_dict(item) + result.append(typed_item) + except Exception as e: + raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e + return result + + +def _get_required_sequence_of_objects( + d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str +) -> Sequence[_FromMappingProtocolT]: + """Get a required list value from the dictionary and convert its items to a + dataclass.""" + if (result := _get_sequence_of_objects(d, target_item_type, key)) is None: + raise _PylockRequiredKeyError(key) + return result + + +def _validate_normalized_name(name: str) -> NormalizedName: + """Validate that a string is a NormalizedName.""" + if not is_normalized_name(name): + raise PylockValidationError(f"Name {name!r} is not normalized") + return NormalizedName(name) + + +def _validate_path_url(path: str | None, url: str | None) -> None: + if not path and not url: + raise PylockValidationError("path or url must be provided") + + +def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]: + if not hashes: + raise PylockValidationError("At least one hash must be provided") + if not all(isinstance(hash_val, str) for hash_val in hashes.values()): + raise PylockValidationError("Hash values must be strings") + return hashes + + +class PylockValidationError(Exception): + """Raised when when input data is not spec-compliant.""" + + context: str | None = None + message: str + + def __init__( + self, + cause: str | Exception, + *, + context: str | None = None, + ) -> None: + if isinstance(cause, PylockValidationError): + if cause.context: + self.context = ( + f"{context}.{cause.context}" if context else cause.context + ) + else: + self.context = context + self.message = cause.message + else: + self.context = context + self.message = str(cause) + + def __str__(self) -> str: + if self.context: + return f"{self.message} in {self.context!r}" + return self.message + + +class _PylockRequiredKeyError(PylockValidationError): + def __init__(self, key: str) -> None: + super().__init__("Missing required value", context=key) + + +class PylockUnsupportedVersionError(PylockValidationError): + """Raised when encountering an unsupported `lock_version`.""" + + +@dataclass(frozen=True, init=False) +class PackageVcs: + type: str + url: str | None = None + path: str | None = None + requested_revision: str | None = None + commit_id: str # type: ignore[misc] + subdirectory: str | None = None + + def __init__( + self, + *, + type: str, + url: str | None = None, + path: str | None = None, + requested_revision: str | None = None, + commit_id: str, + subdirectory: str | None = None, + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "type", type) + object.__setattr__(self, "url", url) + object.__setattr__(self, "path", path) + object.__setattr__(self, "requested_revision", requested_revision) + object.__setattr__(self, "commit_id", commit_id) + object.__setattr__(self, "subdirectory", subdirectory) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + package_vcs = cls( + type=_get_required(d, str, "type"), + url=_get(d, str, "url"), + path=_get(d, str, "path"), + requested_revision=_get(d, str, "requested-revision"), + commit_id=_get_required(d, str, "commit-id"), + subdirectory=_get(d, str, "subdirectory"), + ) + _validate_path_url(package_vcs.path, package_vcs.url) + return package_vcs + + +@dataclass(frozen=True, init=False) +class PackageDirectory: + path: str + editable: bool | None = None + subdirectory: str | None = None + + def __init__( + self, + *, + path: str, + editable: bool | None = None, + subdirectory: str | None = None, + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "path", path) + object.__setattr__(self, "editable", editable) + object.__setattr__(self, "subdirectory", subdirectory) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + return cls( + path=_get_required(d, str, "path"), + editable=_get(d, bool, "editable"), + subdirectory=_get(d, str, "subdirectory"), + ) + + +@dataclass(frozen=True, init=False) +class PackageArchive: + url: str | None = None + path: str | None = None + size: int | None = None + upload_time: datetime | None = None + hashes: Mapping[str, str] # type: ignore[misc] + subdirectory: str | None = None + + def __init__( + self, + *, + url: str | None = None, + path: str | None = None, + size: int | None = None, + upload_time: datetime | None = None, + hashes: Mapping[str, str], + subdirectory: str | None = None, + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "url", url) + object.__setattr__(self, "path", path) + object.__setattr__(self, "size", size) + object.__setattr__(self, "upload_time", upload_time) + object.__setattr__(self, "hashes", hashes) + object.__setattr__(self, "subdirectory", subdirectory) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + package_archive = cls( + url=_get(d, str, "url"), + path=_get(d, str, "path"), + size=_get(d, int, "size"), + upload_time=_get(d, datetime, "upload-time"), + hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] + subdirectory=_get(d, str, "subdirectory"), + ) + _validate_path_url(package_archive.path, package_archive.url) + return package_archive + + +@dataclass(frozen=True, init=False) +class PackageSdist: + name: str | None = None + upload_time: datetime | None = None + url: str | None = None + path: str | None = None + size: int | None = None + hashes: Mapping[str, str] # type: ignore[misc] + + def __init__( + self, + *, + name: str | None = None, + upload_time: datetime | None = None, + url: str | None = None, + path: str | None = None, + size: int | None = None, + hashes: Mapping[str, str], + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "name", name) + object.__setattr__(self, "upload_time", upload_time) + object.__setattr__(self, "url", url) + object.__setattr__(self, "path", path) + object.__setattr__(self, "size", size) + object.__setattr__(self, "hashes", hashes) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + package_sdist = cls( + name=_get(d, str, "name"), + upload_time=_get(d, datetime, "upload-time"), + url=_get(d, str, "url"), + path=_get(d, str, "path"), + size=_get(d, int, "size"), + hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] + ) + _validate_path_url(package_sdist.path, package_sdist.url) + return package_sdist + + +@dataclass(frozen=True, init=False) +class PackageWheel: + name: str | None = None + upload_time: datetime | None = None + url: str | None = None + path: str | None = None + size: int | None = None + hashes: Mapping[str, str] # type: ignore[misc] + + def __init__( + self, + *, + name: str | None = None, + upload_time: datetime | None = None, + url: str | None = None, + path: str | None = None, + size: int | None = None, + hashes: Mapping[str, str], + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "name", name) + object.__setattr__(self, "upload_time", upload_time) + object.__setattr__(self, "url", url) + object.__setattr__(self, "path", path) + object.__setattr__(self, "size", size) + object.__setattr__(self, "hashes", hashes) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + package_wheel = cls( + name=_get(d, str, "name"), + upload_time=_get(d, datetime, "upload-time"), + url=_get(d, str, "url"), + path=_get(d, str, "path"), + size=_get(d, int, "size"), + hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] + ) + _validate_path_url(package_wheel.path, package_wheel.url) + return package_wheel + + +@dataclass(frozen=True, init=False) +class Package: + name: NormalizedName + version: Version | None = None + marker: Marker | None = None + requires_python: SpecifierSet | None = None + dependencies: Sequence[Mapping[str, Any]] | None = None + vcs: PackageVcs | None = None + directory: PackageDirectory | None = None + archive: PackageArchive | None = None + index: str | None = None + sdist: PackageSdist | None = None + wheels: Sequence[PackageWheel] | None = None + attestation_identities: Sequence[Mapping[str, Any]] | None = None + tool: Mapping[str, Any] | None = None + + def __init__( + self, + *, + name: NormalizedName, + version: Version | None = None, + marker: Marker | None = None, + requires_python: SpecifierSet | None = None, + dependencies: Sequence[Mapping[str, Any]] | None = None, + vcs: PackageVcs | None = None, + directory: PackageDirectory | None = None, + archive: PackageArchive | None = None, + index: str | None = None, + sdist: PackageSdist | None = None, + wheels: Sequence[PackageWheel] | None = None, + attestation_identities: Sequence[Mapping[str, Any]] | None = None, + tool: Mapping[str, Any] | None = None, + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "name", name) + object.__setattr__(self, "version", version) + object.__setattr__(self, "marker", marker) + object.__setattr__(self, "requires_python", requires_python) + object.__setattr__(self, "dependencies", dependencies) + object.__setattr__(self, "vcs", vcs) + object.__setattr__(self, "directory", directory) + object.__setattr__(self, "archive", archive) + object.__setattr__(self, "index", index) + object.__setattr__(self, "sdist", sdist) + object.__setattr__(self, "wheels", wheels) + object.__setattr__(self, "attestation_identities", attestation_identities) + object.__setattr__(self, "tool", tool) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + package = cls( + name=_get_required_as(d, str, _validate_normalized_name, "name"), + version=_get_as(d, str, Version, "version"), + requires_python=_get_as(d, str, SpecifierSet, "requires-python"), + dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract] + marker=_get_as(d, str, Marker, "marker"), + vcs=_get_object(d, PackageVcs, "vcs"), + directory=_get_object(d, PackageDirectory, "directory"), + archive=_get_object(d, PackageArchive, "archive"), + index=_get(d, str, "index"), + sdist=_get_object(d, PackageSdist, "sdist"), + wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"), + attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract] + tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract] + ) + distributions = bool(package.sdist) + len(package.wheels or []) + direct_urls = ( + bool(package.vcs) + bool(package.directory) + bool(package.archive) + ) + if distributions > 0 and direct_urls > 0: + raise PylockValidationError( + "None of vcs, directory, archive must be set if sdist or wheels are set" + ) + if distributions == 0 and direct_urls != 1: + raise PylockValidationError( + "Exactly one of vcs, directory, archive must be set " + "if sdist and wheels are not set" + ) + try: + for i, attestation_identity in enumerate( # noqa: B007 + package.attestation_identities or [] + ): + _get_required(attestation_identity, str, "kind") + except Exception as e: + raise PylockValidationError( + e, context=f"attestation-identities[{i}]" + ) from e + return package + + @property + def is_direct(self) -> bool: + return not (self.sdist or self.wheels) + + +@dataclass(frozen=True, init=False) +class Pylock: + """A class representing a pylock file.""" + + lock_version: Version + environments: Sequence[Marker] | None = None + requires_python: SpecifierSet | None = None + extras: Sequence[NormalizedName] | None = None + dependency_groups: Sequence[str] | None = None + default_groups: Sequence[str] | None = None + created_by: str # type: ignore[misc] + packages: Sequence[Package] # type: ignore[misc] + tool: Mapping[str, Any] | None = None + + def __init__( + self, + *, + lock_version: Version, + environments: Sequence[Marker] | None = None, + requires_python: SpecifierSet | None = None, + extras: Sequence[NormalizedName] | None = None, + dependency_groups: Sequence[str] | None = None, + default_groups: Sequence[str] | None = None, + created_by: str, + packages: Sequence[Package], + tool: Mapping[str, Any] | None = None, + ) -> None: + # In Python 3.10+ make dataclass kw_only=True and remove __init__ + object.__setattr__(self, "lock_version", lock_version) + object.__setattr__(self, "environments", environments) + object.__setattr__(self, "requires_python", requires_python) + object.__setattr__(self, "extras", extras) + object.__setattr__(self, "dependency_groups", dependency_groups) + object.__setattr__(self, "default_groups", default_groups) + object.__setattr__(self, "created_by", created_by) + object.__setattr__(self, "packages", packages) + object.__setattr__(self, "tool", tool) + + @classmethod + def _from_dict(cls, d: Mapping[str, Any]) -> Self: + pylock = cls( + lock_version=_get_required_as(d, str, Version, "lock-version"), + environments=_get_sequence_as(d, str, Marker, "environments"), + extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"), + dependency_groups=_get_sequence(d, str, "dependency-groups"), + default_groups=_get_sequence(d, str, "default-groups"), + created_by=_get_required(d, str, "created-by"), + requires_python=_get_as(d, str, SpecifierSet, "requires-python"), + packages=_get_required_sequence_of_objects(d, Package, "packages"), + tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract] + ) + if not Version("1") <= pylock.lock_version < Version("2"): + raise PylockUnsupportedVersionError( + f"pylock version {pylock.lock_version} is not supported" + ) + if pylock.lock_version > Version("1.0"): + _logger.warning( + "pylock minor version %s is not supported", pylock.lock_version + ) + return pylock + + @classmethod + def from_dict(cls, d: Mapping[str, Any], /) -> Self: + """Create and validate a Pylock instance from a TOML dictionary. + + Raises :class:`PylockValidationError` if the input data is not + spec-compliant. + """ + return cls._from_dict(d) + + def to_dict(self) -> Mapping[str, Any]: + """Convert the Pylock instance to a TOML dictionary.""" + return dataclasses.asdict(self, dict_factory=_toml_dict_factory) + + def validate(self) -> None: + """Validate the Pylock instance against the specification. + + Raises :class:`PylockValidationError` otherwise.""" + self.from_dict(self.to_dict()) diff --git a/contrib/python/packaging/py3/packaging/requirements.py b/contrib/python/packaging/py3/packaging/requirements.py index 4e068c9567d..3079be69bf8 100644 --- a/contrib/python/packaging/py3/packaging/requirements.py +++ b/contrib/python/packaging/py3/packaging/requirements.py @@ -3,7 +3,7 @@ # for complete details. from __future__ import annotations -from typing import Any, Iterator +from typing import Iterator from ._parser import parse_requirement as _parse_requirement from ._tokenizer import ParserSyntaxError @@ -57,7 +57,7 @@ class Requirement: yield str(self.specifier) if self.url: - yield f"@ {self.url}" + yield f" @ {self.url}" if self.marker: yield " " @@ -68,17 +68,12 @@ class Requirement: return "".join(self._iter_parts(self.name)) def __repr__(self) -> str: - return f"<Requirement('{self}')>" + return f"<{self.__class__.__name__}('{self}')>" def __hash__(self) -> int: - return hash( - ( - self.__class__.__name__, - *self._iter_parts(canonicalize_name(self.name)), - ) - ) + return hash(tuple(self._iter_parts(canonicalize_name(self.name)))) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): return NotImplemented diff --git a/contrib/python/packaging/py3/packaging/specifiers.py b/contrib/python/packaging/py3/packaging/specifiers.py index c8448043006..5d26b0d1ae2 100644 --- a/contrib/python/packaging/py3/packaging/specifiers.py +++ b/contrib/python/packaging/py3/packaging/specifiers.py @@ -13,22 +13,33 @@ from __future__ import annotations import abc import itertools import re -from typing import Callable, Iterable, Iterator, TypeVar, Union +from typing import Callable, Final, Iterable, Iterator, TypeVar, Union from .utils import canonicalize_version -from .version import Version +from .version import InvalidVersion, Version UnparsedVersion = Union[Version, str] UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) CallableOperator = Callable[[Version, str], bool] -def _coerce_version(version: UnparsedVersion) -> Version: +def _coerce_version(version: UnparsedVersion) -> Version | None: if not isinstance(version, Version): - version = Version(version) + try: + version = Version(version) + except InvalidVersion: + return None return version +def _public_version(version: Version) -> Version: + return version.__replace__(local=None) + + +def _base_version(version: Version) -> Version: + return version.__replace__(pre=None, post=None, dev=None, local=None) + + class InvalidSpecifier(ValueError): """ Raised when attempting to create a :class:`Specifier` with a specifier @@ -42,6 +53,14 @@ class InvalidSpecifier(ValueError): class BaseSpecifier(metaclass=abc.ABCMeta): + __slots__ = () + __match_args__ = ("_str",) + + @property + def _str(self) -> str: + """Internal property for match_args""" + return str(self) + @abc.abstractmethod def __str__(self) -> str: """ @@ -73,7 +92,7 @@ class BaseSpecifier(metaclass=abc.ABCMeta): prereleases or it can be set to ``None`` (the default) to use default semantics. """ - @prereleases.setter + @prereleases.setter # noqa: B027 def prereleases(self, value: bool) -> None: """Setter for :attr:`prereleases`. @@ -106,6 +125,8 @@ class Specifier(BaseSpecifier): comma-separated version specifiers (which is what package metadata contains). """ + __slots__ = ("_prereleases", "_spec", "_spec_version") + _operator_regex_str = r""" (?P<operator>(~=|==|!=|<=|>=|<|>|===)) """ @@ -204,11 +225,11 @@ class Specifier(BaseSpecifier): """ _regex = re.compile( - r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$", + r"\s*" + _operator_regex_str + _version_regex_str + r"\s*", re.VERBOSE | re.IGNORECASE, ) - _operators = { + _operators: Final = { "~=": "compatible", "==": "equal", "!=": "not_equal", @@ -232,7 +253,7 @@ class Specifier(BaseSpecifier): :raises InvalidSpecifier: If the given specifier is invalid (i.e. bad syntax). """ - match = self._regex.search(spec) + match = self._regex.fullmatch(spec) if not match: raise InvalidSpecifier(f"Invalid specifier: {spec!r}") @@ -244,33 +265,62 @@ class Specifier(BaseSpecifier): # Store whether or not this Specifier should accept prereleases self._prereleases = prereleases - # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 - @property # type: ignore[override] - def prereleases(self) -> bool: + # Specifier version cache + self._spec_version: tuple[str, Version] | None = None + + def _get_spec_version(self, version: str) -> Version | None: + """One element cache, as only one spec Version is needed per Specifier.""" + if self._spec_version is not None and self._spec_version[0] == version: + return self._spec_version[1] + + version_specifier = _coerce_version(version) + if version_specifier is None: + return None + + self._spec_version = (version, version_specifier) + return version_specifier + + def _require_spec_version(self, version: str) -> Version: + """Get spec version, asserting it's valid (not for === operator). + + This method should only be called for operators where version + strings are guaranteed to be valid PEP 440 versions (not ===). + """ + spec_version = self._get_spec_version(version) + assert spec_version is not None + return spec_version + + @property + def prereleases(self) -> bool | None: # If there is an explicit prereleases set for this, then we'll just # blindly use that. if self._prereleases is not None: return self._prereleases - # Look at all of our specifiers and determine if they are inclusive - # operators, and if they are if they are including an explicit - # prerelease. - operator, version = self._spec - if operator in ["==", ">=", "<=", "~=", "===", ">", "<"]: - # The == specifier can include a trailing .*, if it does we - # want to remove before parsing. - if operator == "==" and version.endswith(".*"): - version = version[:-2] + # Only the "!=" operator does not imply prereleases when + # the version in the specifier is a prerelease. + operator, version_str = self._spec + if operator != "!=": + # The == specifier with trailing .* cannot include prereleases + # e.g. "==1.0a1.*" is not valid. + if operator == "==" and version_str.endswith(".*"): + return False + + # "===" can have arbitrary string versions, so we cannot parse + # those, we take prereleases as unknown (None) for those. + version = self._get_spec_version(version_str) + if version is None: + return None - # Parse the version, and if it is a pre-release than this - # specifier allows pre-releases. - if Version(version).is_prerelease: + # For all other operators, use the check if spec Version + # object implies pre-releases. + if version.is_prerelease: return True return False @prereleases.setter - def prereleases(self, value: bool) -> None: + def prereleases(self, value: bool | None) -> None: self._prereleases = value @property @@ -321,11 +371,17 @@ class Specifier(BaseSpecifier): @property def _canonical_spec(self) -> tuple[str, str]: + operator, version = self._spec + if operator == "===" or version.endswith(".*"): + return operator, version + + spec_version = self._require_spec_version(version) + canonical_version = canonicalize_version( - self._spec[1], - strip_trailing_zero=(self._spec[0] != "~="), + spec_version, strip_trailing_zero=(operator != "~=") ) - return self._spec[0], canonical_version + + return operator, canonical_version def __hash__(self) -> int: return hash(self._canonical_spec) @@ -390,7 +446,7 @@ class Specifier(BaseSpecifier): if spec.endswith(".*"): # In the case of prefix matching we want to ignore local segment. normalized_prospective = canonicalize_version( - prospective.public, strip_trailing_zero=False + _public_version(prospective), strip_trailing_zero=False ) # Get the normalized version string ignoring the trailing .* normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) @@ -415,13 +471,13 @@ class Specifier(BaseSpecifier): return shortened_prospective == split_spec else: # Convert our spec string into a Version - spec_version = Version(spec) + spec_version = self._require_spec_version(spec) # If the specifier does not have a local segment, then we want to # act as if the prospective version also does not have a local # segment. if not spec_version.local: - prospective = Version(prospective.public) + prospective = _public_version(prospective) return prospective == spec_version @@ -432,18 +488,18 @@ class Specifier(BaseSpecifier): # NB: Local version identifiers are NOT permitted in the version # specifier, so local version labels can be universally removed from # the prospective version. - return Version(prospective.public) <= Version(spec) + return _public_version(prospective) <= self._require_spec_version(spec) def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: # NB: Local version identifiers are NOT permitted in the version # specifier, so local version labels can be universally removed from # the prospective version. - return Version(prospective.public) >= Version(spec) + return _public_version(prospective) >= self._require_spec_version(spec) def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec_str) + spec = self._require_spec_version(spec_str) # Check to see if the prospective version is less than the spec # version. If it's not we can short circuit and just return False now @@ -455,9 +511,12 @@ class Specifier(BaseSpecifier): # includes is a pre-release version, that we do not accept pre-release # versions for the version mentioned in the specifier (e.g. <3.1 should # not match 3.1.dev0, but should match 3.0.dev0). - if not spec.is_prerelease and prospective.is_prerelease: - if Version(prospective.base_version) == Version(spec.base_version): - return False + if ( + not spec.is_prerelease + and prospective.is_prerelease + and _base_version(prospective) == _base_version(spec) + ): + return False # If we've gotten to here, it means that prospective version is both # less than the spec version *and* it's not a pre-release of the same @@ -467,7 +526,7 @@ class Specifier(BaseSpecifier): def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec_str) + spec = self._require_spec_version(spec_str) # Check to see if the prospective version is greater than the spec # version. If it's not we can short circuit and just return False now @@ -479,22 +538,26 @@ class Specifier(BaseSpecifier): # includes is a post-release version, that we do not accept # post-release versions for the version mentioned in the specifier # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). - if not spec.is_postrelease and prospective.is_postrelease: - if Version(prospective.base_version) == Version(spec.base_version): - return False + if ( + not spec.is_postrelease + and prospective.is_postrelease + and _base_version(prospective) == _base_version(spec) + ): + return False # Ensure that we do not allow a local version of the version mentioned # in the specifier, which is technically greater than, to match. - if prospective.local is not None: - if Version(prospective.base_version) == Version(spec.base_version): - return False + if prospective.local is not None and _base_version( + prospective + ) == _base_version(spec): + return False # If we've gotten to here, it means that prospective version is both # greater than the spec version *and* it's not a pre-release of the # same version in the spec. return True - def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: + def _compare_arbitrary(self, prospective: Version | str, spec: str) -> bool: return str(prospective).lower() == str(spec).lower() def __contains__(self, item: str | Version) -> bool: @@ -512,7 +575,7 @@ class Specifier(BaseSpecifier): >>> "1.0.0" in Specifier(">=1.2.3") False >>> "1.3.0a1" in Specifier(">=1.2.3") - False + True >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) True """ @@ -526,8 +589,8 @@ class Specifier(BaseSpecifier): :class:`Version` instance. :param prereleases: Whether or not to match prereleases with this Specifier. If set to - ``None`` (the default), it uses :attr:`prereleases` to determine - whether or not prereleases are allowed. + ``None`` (the default), it will follow the recommendation from + :pep:`440` and match prereleases, as there are no other versions. >>> Specifier(">=1.2.3").contains("1.2.3") True @@ -536,31 +599,14 @@ class Specifier(BaseSpecifier): >>> Specifier(">=1.2.3").contains("1.0.0") False >>> Specifier(">=1.2.3").contains("1.3.0a1") - False - >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") True - >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) + >>> Specifier(">=1.2.3", prereleases=False).contains("1.3.0a1") + False + >>> Specifier(">=1.2.3").contains("1.3.0a1") True """ - # Determine if prereleases are to be allowed or not. - if prereleases is None: - prereleases = self.prereleases - - # Normalize item to a Version, this allows us to have a shortcut for - # "2.0" in Specifier(">=2") - normalized_item = _coerce_version(item) - - # Determine if we should be supporting prereleases in this specifier - # or not, if we do not support prereleases than we can short circuit - # logic if this version is a prereleases. - if normalized_item.is_prerelease and not prereleases: - return False - - # Actually do the comparison to determine if this item is contained - # within this Specifier or not. - operator_callable: CallableOperator = self._get_operator(self.operator) - return operator_callable(normalized_item, self.version) + return bool(list(self.filter([item], prereleases=prereleases))) def filter( self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None @@ -572,13 +618,8 @@ class Specifier(BaseSpecifier): The items in the iterable will be filtered according to the specifier. :param prereleases: Whether or not to allow prereleases in the returned iterator. If set to - ``None`` (the default), it will be intelligently decide whether to allow - prereleases or not (based on the :attr:`prereleases` attribute, and - whether the only versions matching are prereleases). - - This method is smarter than just ``filter(Specifier().contains, [...])`` - because it implements the rule from :pep:`440` that a prerelease item - SHOULD be accepted if no other versions match the given specifier. + ``None`` (the default), it will follow the recommendation from :pep:`440` + and match prereleases if there are no other versions. >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) ['1.3'] @@ -591,40 +632,46 @@ class Specifier(BaseSpecifier): >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) ['1.3', '1.5a1'] """ + prereleases_versions = [] + found_non_prereleases = False - yielded = False - found_prereleases = [] + # Determine if to include prereleases by default + include_prereleases = ( + prereleases if prereleases is not None else self.prereleases + ) - kw = {"prereleases": prereleases if prereleases is not None else True} + # Get the matching operator + operator_callable = self._get_operator(self.operator) - # Attempt to iterate over all the values in the iterable and if any of - # them match, yield them. + # Filter versions for version in iterable: parsed_version = _coerce_version(version) - - if self.contains(parsed_version, **kw): - # If our version is a prerelease, and we were not set to allow - # prereleases, then we'll store it for later in case nothing - # else matches this specifier. - if parsed_version.is_prerelease and not ( - prereleases or self.prereleases + if parsed_version is None: + # === operator can match arbitrary (non-version) strings + if self.operator == "===" and self._compare_arbitrary( + version, self.version ): - found_prereleases.append(version) - # Either this is not a prerelease, or we should have been - # accepting prereleases from the beginning. - else: - yielded = True yield version + elif operator_callable(parsed_version, self.version): + # If it's not a prerelease or prereleases are allowed, yield it directly + if not parsed_version.is_prerelease or include_prereleases: + found_non_prereleases = True + yield version + # Otherwise collect prereleases for potential later use + elif prereleases is None and self._prereleases is not False: + prereleases_versions.append(version) - # Now that we've iterated over everything, determine if we've yielded - # any values, and if we have not and we have any prereleases stored up - # then we will go ahead and yield the prereleases. - if not yielded and found_prereleases: - for version in found_prereleases: - yield version + # If no non-prereleases were found and prereleases weren't + # explicitly forbidden, yield the collected prereleases + if ( + not found_non_prereleases + and prereleases is None + and self._prereleases is not False + ): + yield from prereleases_versions -_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") +_prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)") def _version_split(version: str) -> list[str]: @@ -641,7 +688,7 @@ def _version_split(version: str) -> list[str]: result.append(epoch or "0") for item in rest.split("."): - match = _prefix_regex.search(item) + match = _prefix_regex.fullmatch(item) if match: result.extend(match.groups()) else: @@ -694,6 +741,8 @@ class SpecifierSet(BaseSpecifier): specifiers (``>=3.0,!=3.1``), or no specifier at all. """ + __slots__ = ("_prereleases", "_specs") + def __init__( self, specifiers: str | Iterable[Specifier] = "", @@ -747,10 +796,13 @@ class SpecifierSet(BaseSpecifier): # Otherwise we'll see if any of the given specifiers accept # prereleases, if any of them do we'll return True, otherwise False. - return any(s.prereleases for s in self._specs) + if any(s.prereleases for s in self._specs): + return True + + return None @prereleases.setter - def prereleases(self, value: bool) -> None: + def prereleases(self, value: bool | None) -> None: self._prereleases = value def __repr__(self) -> str: @@ -810,9 +862,9 @@ class SpecifierSet(BaseSpecifier): if self._prereleases is None and other._prereleases is not None: specifier._prereleases = other._prereleases - elif self._prereleases is not None and other._prereleases is None: - specifier._prereleases = self._prereleases - elif self._prereleases == other._prereleases: + elif ( + self._prereleases is not None and other._prereleases is None + ) or self._prereleases == other._prereleases: specifier._prereleases = self._prereleases else: raise ValueError( @@ -876,7 +928,7 @@ class SpecifierSet(BaseSpecifier): >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") False >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") - False + True >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) True """ @@ -895,8 +947,11 @@ class SpecifierSet(BaseSpecifier): :class:`Version` instance. :param prereleases: Whether or not to match prereleases with this SpecifierSet. If set to - ``None`` (the default), it uses :attr:`prereleases` to determine - whether or not prereleases are allowed. + ``None`` (the default), it will follow the recommendation from :pep:`440` + and match prereleases, as there are no other versions. + :param installed: + Whether or not the item is installed. If set to ``True``, it will + accept prerelease versions even if the specifier does not allow them. >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") True @@ -905,39 +960,19 @@ class SpecifierSet(BaseSpecifier): >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") False >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") - False - >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") True + >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False).contains("1.3.0a1") + False >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) True """ - # Ensure that our item is a Version instance. - if not isinstance(item, Version): - item = Version(item) - - # Determine if we're forcing a prerelease or not, if we're not forcing - # one for this particular filter call, then we'll use whatever the - # SpecifierSet thinks for whether or not we should support prereleases. - if prereleases is None: - prereleases = self.prereleases - - # We can determine if we're going to allow pre-releases by looking to - # see if any of the underlying items supports them. If none of them do - # and this item is a pre-release then we do not allow it and we can - # short circuit that here. - # Note: This means that 1.0.dev1 would not be contained in something - # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 - if not prereleases and item.is_prerelease: - return False + version = _coerce_version(item) - if installed and item.is_prerelease: - item = Version(item.base_version) + if version is not None and installed and version.is_prerelease: + prereleases = True - # We simply dispatch to the underlying specs here to make sure that the - # given version is contained within all of them. - # Note: This use of all() here means that an empty set of specifiers - # will always return True, this is an explicit design decision. - return all(s.contains(item, prereleases=prereleases) for s in self._specs) + check_item = item if version is None else version + return bool(list(self.filter([check_item], prereleases=prereleases))) def filter( self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None @@ -949,20 +984,15 @@ class SpecifierSet(BaseSpecifier): The items in the iterable will be filtered according to the specifier. :param prereleases: Whether or not to allow prereleases in the returned iterator. If set to - ``None`` (the default), it will be intelligently decide whether to allow - prereleases or not (based on the :attr:`prereleases` attribute, and - whether the only versions matching are prereleases). - - This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` - because it implements the rule from :pep:`440` that a prerelease item - SHOULD be accepted if no other versions match the given specifier. + ``None`` (the default), it will follow the recommendation from :pep:`440` + and match prereleases if there are no other versions. >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) ['1.3'] >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) ['1.3', <Version('1.4')>] >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) - [] + ['1.5a1'] >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) ['1.3', '1.5a1'] >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) @@ -983,37 +1013,56 @@ class SpecifierSet(BaseSpecifier): # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the # SpecifierSet thinks for whether or not we should support prereleases. - if prereleases is None: + if prereleases is None and self.prereleases is not None: prereleases = self.prereleases # If we have any specifiers, then we want to wrap our iterable in the # filter method for each one, this will act as a logical AND amongst # each specifier. if self._specs: + # When prereleases is None, we need to let all versions through + # the individual filters, then decide about prereleases at the end + # based on whether any non-prereleases matched ALL specs. for spec in self._specs: - iterable = spec.filter(iterable, prereleases=bool(prereleases)) - return iter(iterable) - # If we do not have any specifiers, then we need to have a rough filter - # which will filter out any pre-releases, unless there are no final - # releases. + iterable = spec.filter( + iterable, prereleases=True if prereleases is None else prereleases + ) + + if prereleases is not None: + # If we have a forced prereleases value, + # we can immediately return the iterator. + return iter(iterable) else: - filtered: list[UnparsedVersionVar] = [] - found_prereleases: list[UnparsedVersionVar] = [] + # Handle empty SpecifierSet cases where prereleases is not None. + if prereleases is True: + return iter(iterable) - for item in iterable: - parsed_version = _coerce_version(item) + if prereleases is False: + return ( + item + for item in iterable + if (version := _coerce_version(item)) is None + or not version.is_prerelease + ) - # Store any item which is a pre-release for later unless we've - # already found a final version or we are accepting prereleases - if parsed_version.is_prerelease and not prereleases: - if not filtered: - found_prereleases.append(item) - else: - filtered.append(item) + # Finally if prereleases is None, apply PEP 440 logic: + # exclude prereleases unless there are no final releases that matched. + filtered_items: list[UnparsedVersionVar] = [] + found_prereleases: list[UnparsedVersionVar] = [] + found_final_release = False - # If we've found no items except for pre-releases, then we'll go - # ahead and use the pre-releases - if not filtered and found_prereleases and prereleases is None: - return iter(found_prereleases) + for item in iterable: + parsed_version = _coerce_version(item) + # Arbitrary strings are always included as it is not + # possible to determine if they are prereleases, + # and they have already passed all specifiers. + if parsed_version is None: + filtered_items.append(item) + found_prereleases.append(item) + elif parsed_version.is_prerelease: + found_prereleases.append(item) + else: + filtered_items.append(item) + found_final_release = True - return iter(filtered) + return iter(filtered_items if found_final_release else found_prereleases) diff --git a/contrib/python/packaging/py3/packaging/tags.py b/contrib/python/packaging/py3/packaging/tags.py index 8522f59c4f2..5ef27c897a4 100644 --- a/contrib/python/packaging/py3/packaging/tags.py +++ b/contrib/python/packaging/py3/packaging/tags.py @@ -13,6 +13,7 @@ import sys import sysconfig from importlib.machinery import EXTENSION_SUFFIXES from typing import ( + Any, Iterable, Iterator, Sequence, @@ -92,6 +93,13 @@ class Tag: def __repr__(self) -> str: return f"<{self} @ {id(self)}>" + def __setstate__(self, state: tuple[None, dict[str, Any]]) -> None: + # The cached _hash is wrong when unpickling. + _, slots = state + for k, v in slots.items(): + setattr(self, k, v) + self._hash = hash((self._interpreter, self._abi, self._platform)) + def parse_tag(tag: str) -> frozenset[Tag]: """ @@ -209,16 +217,13 @@ def cpython_tags( interpreter = f"cp{_version_nodot(python_version[:2])}" if abis is None: - if len(python_version) > 1: - abis = _cpython_abis(python_version, warn) - else: - abis = [] + abis = _cpython_abis(python_version, warn) if len(python_version) > 1 else [] abis = list(abis) # 'abi3' and 'none' are explicitly handled later. for explicit_abi in ("abi3", "none"): try: abis.remove(explicit_abi) - except ValueError: + except ValueError: # noqa: PERF203 pass platforms = list(platforms or platform_tags()) @@ -299,11 +304,8 @@ def generic_tags( if not interpreter: interp_name = interpreter_name() interp_version = interpreter_version(warn=warn) - interpreter = "".join([interp_name, interp_version]) - if abis is None: - abis = _generic_abi() - else: - abis = list(abis) + interpreter = f"{interp_name}{interp_version}" + abis = _generic_abi() if abis is None else list(abis) platforms = list(platforms or platform_tags()) if "none" not in abis: abis.append("none") @@ -424,14 +426,11 @@ def mac_platforms( text=True, ).stdout version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2]))) - else: - version = version + if arch is None: arch = _mac_arch(cpu_arch) - else: - arch = arch - if (10, 0) <= version and version < (11, 0): + if (10, 0) <= version < (11, 0): # Prior to Mac OS 11, each yearly release of Mac OS bumped the # "minor" version number. The major version was always 10. major_version = 10 @@ -622,11 +621,7 @@ def interpreter_version(*, warn: bool = False) -> str: Returns the version of the running interpreter. """ version = _get_config_var("py_version_nodot", warn=warn) - if version: - version = str(version) - else: - version = _version_nodot(sys.version_info[:2]) - return version + return str(version) if version else _version_nodot(sys.version_info[:2]) def _version_nodot(version: PythonVersion) -> str: diff --git a/contrib/python/packaging/py3/packaging/utils.py b/contrib/python/packaging/py3/packaging/utils.py index 23450953df7..c41c8137f26 100644 --- a/contrib/python/packaging/py3/packaging/utils.py +++ b/contrib/python/packaging/py3/packaging/utils.py @@ -4,7 +4,6 @@ from __future__ import annotations -import functools import re from typing import NewType, Tuple, Union, cast @@ -34,28 +33,29 @@ class InvalidSdistFilename(ValueError): # Core metadata spec for `Name` -_validate_regex = re.compile( - r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE -) -_canonicalize_regex = re.compile(r"[-_.]+") -_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") +_validate_regex = re.compile(r"[A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9]", re.IGNORECASE) +_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]") # PEP 427: The build number must start with a digit. _build_tag_regex = re.compile(r"(\d+)(.*)") def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: - if validate and not _validate_regex.match(name): + if validate and not _validate_regex.fullmatch(name): raise InvalidName(f"name is invalid: {name!r}") - # This is taken from PEP 503. - value = _canonicalize_regex.sub("-", name).lower() - return cast(NormalizedName, value) + # Ensure all ``.`` and ``_`` are ``-`` + # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 + # Much faster than re, and even faster than str.translate + value = name.lower().replace("_", "-").replace(".", "-") + # Condense repeats (faster than regex) + while "--" in value: + value = value.replace("--", "-") + return cast("NormalizedName", value) def is_normalized_name(name: str) -> bool: - return _normalized_regex.match(name) is not None + return _normalized_regex.fullmatch(name) is not None def canonicalize_version( version: Version | str, *, strip_trailing_zero: bool = True ) -> str: @@ -78,17 +78,12 @@ def canonicalize_version( >>> canonicalize_version('foo bar baz') 'foo bar baz' """ - return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version) - - -@canonicalize_version.register -def _(version: str, *, strip_trailing_zero: bool = True) -> str: - try: - parsed = Version(version) - except InvalidVersion: - # Legacy versions cannot be normalized - return version - return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero) + if isinstance(version, str): + try: + version = Version(version) + except InvalidVersion: + return str(version) + return str(_TrimmedRelease(version) if strip_trailing_zero else version) def parse_wheel_filename( @@ -127,7 +122,7 @@ def parse_wheel_filename( raise InvalidWheelFilename( f"Invalid build number: {build_part} in {filename!r}" ) - build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2))) else: build = () tags = parse_tag(parts[-1]) diff --git a/contrib/python/packaging/py3/packaging/version.py b/contrib/python/packaging/py3/packaging/version.py index c9bbda20e46..1206c462d4f 100644 --- a/contrib/python/packaging/py3/packaging/version.py +++ b/contrib/python/packaging/py3/packaging/version.py @@ -9,12 +9,59 @@ from __future__ import annotations -import itertools import re -from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union +import sys +import typing +from typing import ( + Any, + Callable, + Literal, + NamedTuple, + SupportsInt, + Tuple, + TypedDict, + Union, +) from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType +if typing.TYPE_CHECKING: + from typing_extensions import Self, Unpack + +if sys.version_info >= (3, 13): # pragma: no cover + from warnings import deprecated as _deprecated +elif typing.TYPE_CHECKING: + from typing_extensions import deprecated as _deprecated +else: # pragma: no cover + import functools + import warnings + + def _deprecated(message: str) -> object: + def decorator(func: object) -> object: + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + warnings.warn( + message, + category=DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +_LETTER_NORMALIZATION = { + "alpha": "a", + "beta": "b", + "c": "rc", + "pre": "rc", + "preview": "rc", + "rev": "post", + "r": "post", +} + __all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"] LocalType = Tuple[Union[int, str], ...] @@ -35,13 +82,13 @@ CmpKey = Tuple[ VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] -class _Version(NamedTuple): - epoch: int - release: tuple[int, ...] - dev: tuple[str, int] | None - pre: tuple[str, int] | None - post: tuple[str, int] | None - local: LocalType | None +class _VersionReplace(TypedDict, total=False): + epoch: int | None + release: tuple[int, ...] | None + pre: tuple[Literal["a", "b", "rc"], int] | None + post: int | None + dev: int | None + local: str | None def parse(version: str) -> Version: @@ -67,7 +114,15 @@ class InvalidVersion(ValueError): class _BaseVersion: - _key: tuple[Any, ...] + __slots__ = () + + # This can also be a normal member (see the packaging_legacy package); + # we are just requiring it to be readable. Actually defining a property + # has runtime effect on subclasses, so it's typing only. + if typing.TYPE_CHECKING: + + @property + def _key(self) -> tuple[Any, ...]: ... def __hash__(self) -> int: return hash(self._key) @@ -114,38 +169,56 @@ class _BaseVersion: # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse + +# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here _VERSION_PATTERN = r""" - v? + v?+ # optional leading v (?: - (?:(?P<epoch>[0-9]+)!)? # epoch - (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment + (?:(?P<epoch>[0-9]+)!)?+ # epoch + (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment (?P<pre> # pre-release - [-_\.]? + [._-]?+ (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) - [-_\.]? + [._-]?+ (?P<pre_n>[0-9]+)? - )? + )?+ (?P<post> # post release (?:-(?P<post_n1>[0-9]+)) | (?: - [-_\.]? + [._-]? (?P<post_l>post|rev|r) - [-_\.]? + [._-]? (?P<post_n2>[0-9]+)? ) - )? + )?+ (?P<dev> # dev release - [-_\.]? + [._-]?+ (?P<dev_l>dev) - [-_\.]? + [._-]?+ (?P<dev_n>[0-9]+)? - )? + )?+ ) - (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version + (?:\+ + (?P<local> # local version + [a-z0-9]+ + (?:[._-][a-z0-9]+)*+ + ) + )?+ """ -VERSION_PATTERN = _VERSION_PATTERN +_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?") + +# Possessive qualifiers were added in Python 3.11. +# CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795 +# Older PyPy also had a bug. +VERSION_PATTERN = ( + _VERSION_PATTERN_OLD + if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5)) + or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13)) + or sys.version_info < (3, 11) + else _VERSION_PATTERN +) """ A string containing the regular expression used to match a valid version. @@ -158,6 +231,82 @@ flags set. """ +# Validation pattern for local version in replace() +_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE) + + +def _validate_epoch(value: object, /) -> int: + epoch = value or 0 + if isinstance(epoch, int) and epoch >= 0: + return epoch + msg = f"epoch must be non-negative integer, got {epoch}" + raise InvalidVersion(msg) + + +def _validate_release(value: object, /) -> tuple[int, ...]: + release = (0,) if value is None else value + if ( + isinstance(release, tuple) + and len(release) > 0 + and all(isinstance(i, int) and i >= 0 for i in release) + ): + return release + msg = f"release must be a non-empty tuple of non-negative integers, got {release}" + raise InvalidVersion(msg) + + +def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None: + if value is None: + return value + if ( + isinstance(value, tuple) + and len(value) == 2 + and value[0] in ("a", "b", "rc") + and isinstance(value[1], int) + and value[1] >= 0 + ): + return value + msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}" + raise InvalidVersion(msg) + + +def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None: + if value is None: + return value + if isinstance(value, int) and value >= 0: + return ("post", value) + msg = f"post must be non-negative integer, got {value}" + raise InvalidVersion(msg) + + +def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None: + if value is None: + return value + if isinstance(value, int) and value >= 0: + return ("dev", value) + msg = f"dev must be non-negative integer, got {value}" + raise InvalidVersion(msg) + + +def _validate_local(value: object, /) -> LocalType | None: + if value is None: + return value + if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value): + return _parse_local_version(value) + msg = f"local must be a valid version string, got {value!r}" + raise InvalidVersion(msg) + + +# Backward compatibility for internals before 26.0. Do not use. +class _Version(NamedTuple): + epoch: int + release: tuple[int, ...] + dev: tuple[str, int] | None + pre: tuple[str, int] | None + post: tuple[str, int] | None + local: LocalType | None + + class Version(_BaseVersion): """This class abstracts handling of a project's versions. @@ -182,8 +331,19 @@ class Version(_BaseVersion): True """ - _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) - _key: CmpKey + __slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release") + __match_args__ = ("_str",) + + _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE) + + _epoch: int + _release: tuple[int, ...] + _dev: tuple[str, int] | None + _pre: tuple[str, int] | None + _post: tuple[str, int] | None + _local: LocalType | None + + _key_cache: CmpKey | None def __init__(self, version: str) -> None: """Initialize a Version object. @@ -195,34 +355,86 @@ class Version(_BaseVersion): If the ``version`` does not conform to PEP 440 in any way then this exception will be raised. """ - # Validate the version and parse it into pieces - match = self._regex.search(version) + match = self._regex.fullmatch(version) if not match: raise InvalidVersion(f"Invalid version: {version!r}") + self._epoch = int(match.group("epoch")) if match.group("epoch") else 0 + self._release = tuple(map(int, match.group("release").split("."))) + self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) + self._post = _parse_letter_version( + match.group("post_l"), match.group("post_n1") or match.group("post_n2") + ) + self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) + self._local = _parse_local_version(match.group("local")) + + # Key which will be used for sorting + self._key_cache = None - # Store the parsed out pieces of the version - self._version = _Version( - epoch=int(match.group("epoch")) if match.group("epoch") else 0, - release=tuple(int(i) for i in match.group("release").split(".")), - pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), - post=_parse_letter_version( - match.group("post_l"), match.group("post_n1") or match.group("post_n2") - ), - dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), - local=_parse_local_version(match.group("local")), + def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self: + epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch + release = ( + _validate_release(kwargs["release"]) + if "release" in kwargs + else self._release ) + pre = _validate_pre(kwargs["pre"]) if "pre" in kwargs else self._pre + post = _validate_post(kwargs["post"]) if "post" in kwargs else self._post + dev = _validate_dev(kwargs["dev"]) if "dev" in kwargs else self._dev + local = _validate_local(kwargs["local"]) if "local" in kwargs else self._local + + if ( + epoch == self._epoch + and release == self._release + and pre == self._pre + and post == self._post + and dev == self._dev + and local == self._local + ): + return self + + new_version = self.__class__.__new__(self.__class__) + new_version._key_cache = None + new_version._epoch = epoch + new_version._release = release + new_version._pre = pre + new_version._post = post + new_version._dev = dev + new_version._local = local + + return new_version + + @property + def _key(self) -> CmpKey: + if self._key_cache is None: + self._key_cache = _cmpkey( + self._epoch, + self._release, + self._pre, + self._post, + self._dev, + self._local, + ) + return self._key_cache - # Generate a key which will be used for sorting - self._key = _cmpkey( - self._version.epoch, - self._version.release, - self._version.pre, - self._version.post, - self._version.dev, - self._version.local, + @property + @_deprecated("Version._version is private and will be removed soon") + def _version(self) -> _Version: + return _Version( + self._epoch, self._release, self._dev, self._pre, self._post, self._local ) + @_version.setter + @_deprecated("Version._version is private and will be removed soon") + def _version(self, value: _Version) -> None: + self._epoch = value.epoch + self._release = value.release + self._dev = value.dev + self._pre = value.pre + self._post = value.post + self._local = value.local + self._key_cache = None + def __repr__(self) -> str: """A representation of the Version that shows all internal state. @@ -237,32 +449,35 @@ class Version(_BaseVersion): >>> str(Version("1.0a5")) '1.0a5' """ - parts = [] + # This is a hot function, so not calling self.base_version + version = ".".join(map(str, self.release)) # Epoch - if self.epoch != 0: - parts.append(f"{self.epoch}!") - - # Release segment - parts.append(".".join(str(x) for x in self.release)) + if self.epoch: + version = f"{self.epoch}!{version}" # Pre-release if self.pre is not None: - parts.append("".join(str(x) for x in self.pre)) + version += "".join(map(str, self.pre)) # Post-release if self.post is not None: - parts.append(f".post{self.post}") + version += f".post{self.post}" # Development release if self.dev is not None: - parts.append(f".dev{self.dev}") + version += f".dev{self.dev}" # Local version segment if self.local is not None: - parts.append(f"+{self.local}") + version += f"+{self.local}" - return "".join(parts) + return version + + @property + def _str(self) -> str: + """Internal property for match_args""" + return str(self) @property def epoch(self) -> int: @@ -273,7 +488,7 @@ class Version(_BaseVersion): >>> Version("1!2.0.0").epoch 1 """ - return self._version.epoch + return self._epoch @property def release(self) -> tuple[int, ...]: @@ -289,7 +504,7 @@ class Version(_BaseVersion): Includes trailing zeroes but not the epoch or any pre-release / development / post-release suffixes. """ - return self._version.release + return self._release @property def pre(self) -> tuple[str, int] | None: @@ -304,7 +519,7 @@ class Version(_BaseVersion): >>> Version("1.2.3rc1").pre ('rc', 1) """ - return self._version.pre + return self._pre @property def post(self) -> int | None: @@ -315,7 +530,7 @@ class Version(_BaseVersion): >>> Version("1.2.3.post1").post 1 """ - return self._version.post[1] if self._version.post else None + return self._post[1] if self._post else None @property def dev(self) -> int | None: @@ -326,7 +541,7 @@ class Version(_BaseVersion): >>> Version("1.2.3.dev1").dev 1 """ - return self._version.dev[1] if self._version.dev else None + return self._dev[1] if self._dev else None @property def local(self) -> str | None: @@ -337,8 +552,8 @@ class Version(_BaseVersion): >>> Version("1.2.3+abc").local 'abc' """ - if self._version.local: - return ".".join(str(x) for x in self._version.local) + if self._local: + return ".".join(str(x) for x in self._local) else: return None @@ -369,16 +584,8 @@ class Version(_BaseVersion): The "base version" is the public version of the project without any pre or post release markers. """ - parts = [] - - # Epoch - if self.epoch != 0: - parts.append(f"{self.epoch}!") - - # Release segment - parts.append(".".join(str(x) for x in self.release)) - - return "".join(parts) + release_segment = ".".join(map(str, self.release)) + return f"{self.epoch}!{release_segment}" if self.epoch else release_segment @property def is_prerelease(self) -> bool: @@ -452,6 +659,20 @@ class Version(_BaseVersion): class _TrimmedRelease(Version): + __slots__ = () + + def __init__(self, version: str | Version) -> None: + if isinstance(version, Version): + self._epoch = version._epoch + self._release = version._release + self._dev = version._dev + self._pre = version._pre + self._post = version._post + self._local = version._local + self._key_cache = version._key_cache + return + super().__init__(version) # pragma: no cover + @property def release(self) -> tuple[int, ...]: """ @@ -462,45 +683,35 @@ class _TrimmedRelease(Version): >>> _TrimmedRelease('0.0').release (0,) """ + # This leaves one 0. rel = super().release - nonzeros = (index for index, val in enumerate(rel) if val) - last_nonzero = max(nonzeros, default=0) - return rel[: last_nonzero + 1] + len_release = len(rel) + i = len_release + while i > 1 and rel[i - 1] == 0: + i -= 1 + return rel if i == len_release else rel[:i] def _parse_letter_version( letter: str | None, number: str | bytes | SupportsInt | None ) -> tuple[str, int] | None: if letter: - # We consider there to be an implicit 0 in a pre-release if there is - # not a numeral associated with it. - if number is None: - number = 0 - # We normalize any letters to their lower case form letter = letter.lower() # We consider some words to be alternate spellings of other words and # in those cases we want to normalize the spellings to our preferred # spelling. - if letter == "alpha": - letter = "a" - elif letter == "beta": - letter = "b" - elif letter in ["c", "pre", "preview"]: - letter = "rc" - elif letter in ["rev", "r"]: - letter = "post" + letter = _LETTER_NORMALIZATION.get(letter, letter) - return letter, int(number) + # We consider there to be an implicit 0 in a pre-release if there is + # not a numeral associated with it. + return letter, int(number or 0) - assert not letter if number: # We assume if we are given a number, but we are not given a letter # then this is using the implicit post release syntax (e.g. 1.0-1) - letter = "post" - - return letter, int(number) + return "post", int(number) return None @@ -529,13 +740,12 @@ def _cmpkey( local: LocalType | None, ) -> CmpKey: # When we compare a release version, we want to compare it with all of the - # trailing zeros removed. So we'll use a reverse the list, drop all the now - # leading zeros until we come to something non zero, then take the rest - # re-reverse it back into the correct order and make it a tuple and use - # that for our sorting key. - _release = tuple( - reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) - ) + # trailing zeros removed. We will use this for our sorting key. + len_release = len(release) + i = len_release + while i and release[i - 1] == 0: + i -= 1 + _release = release if i == len_release else release[:i] # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. # We'll do this by abusing the pre segment, but we _only_ want to do this diff --git a/contrib/python/packaging/py3/ya.make b/contrib/python/packaging/py3/ya.make index 0e79cffb75a..6cc32b0d90f 100644 --- a/contrib/python/packaging/py3/ya.make +++ b/contrib/python/packaging/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(25.0) +VERSION(26.0) LICENSE(BSD-2-Clause AND Apache-2.0) @@ -21,6 +21,7 @@ PY_SRCS( packaging/licenses/_spdx.py packaging/markers.py packaging/metadata.py + packaging/pylock.py packaging/requirements.py packaging/specifiers.py packaging/tags.py |
