summaryrefslogtreecommitdiffstats
path: root/contrib/python/packaging
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-02-06 17:53:44 +0300
committerrobot-piglet <[email protected]>2026-02-06 19:01:13 +0300
commit3c539da5e7db7e675a202a8551cb3657ec64c193 (patch)
tree4cddb3c545c5a4bf4f599a2369d7945d136ae63f /contrib/python/packaging
parent3373df6e8cc97499d33a588fe2890571ee447e62 (diff)
Intermediate changes
commit_hash:c4352485eb2978cb2b34e9c09ab3231db1290c81
Diffstat (limited to 'contrib/python/packaging')
-rw-r--r--contrib/python/packaging/py3/.dist-info/METADATA10
-rw-r--r--contrib/python/packaging/py3/README.rst4
-rw-r--r--contrib/python/packaging/py3/packaging/__init__.py2
-rw-r--r--contrib/python/packaging/py3/packaging/_elffile.py1
-rw-r--r--contrib/python/packaging/py3/packaging/_manylinux.py72
-rw-r--r--contrib/python/packaging/py3/packaging/_musllinux.py2
-rw-r--r--contrib/python/packaging/py3/packaging/_parser.py32
-rw-r--r--contrib/python/packaging/py3/packaging/_structures.py8
-rw-r--r--contrib/python/packaging/py3/packaging/_tokenizer.py48
-rw-r--r--contrib/python/packaging/py3/packaging/licenses/__init__.py24
-rw-r--r--contrib/python/packaging/py3/packaging/licenses/_spdx.py42
-rw-r--r--contrib/python/packaging/py3/packaging/markers.py102
-rw-r--r--contrib/python/packaging/py3/packaging/metadata.py170
-rw-r--r--contrib/python/packaging/py3/packaging/pylock.py635
-rw-r--r--contrib/python/packaging/py3/packaging/requirements.py15
-rw-r--r--contrib/python/packaging/py3/packaging/specifiers.py379
-rw-r--r--contrib/python/packaging/py3/packaging/tags.py35
-rw-r--r--contrib/python/packaging/py3/packaging/utils.py43
-rw-r--r--contrib/python/packaging/py3/packaging/version.py414
-rw-r--r--contrib/python/packaging/py3/ya.make3
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