add updates libs
This commit is contained in:
@@ -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",
|
||||
@@ -25,31 +26,58 @@ __all__ = [
|
||||
"default_environment",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
|
||||
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
|
||||
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
|
||||
|
||||
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
|
||||
|
||||
* ``"metadata"`` (for core metadata; default)
|
||||
* ``"lock_file"`` (for lock files)
|
||||
* ``"requirement"`` (i.e. all other situations)
|
||||
"""
|
||||
|
||||
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
||||
MARKERS_REQUIRING_VERSION = {
|
||||
"implementation_version",
|
||||
"platform_release",
|
||||
"python_full_version",
|
||||
"python_version",
|
||||
}
|
||||
|
||||
|
||||
class InvalidMarker(ValueError):
|
||||
"""
|
||||
An invalid marker was found, users should refer to PEP 508.
|
||||
"""Raised when attempting to create a :class:`Marker` from invalid input.
|
||||
|
||||
This error indicates that the given marker string does not conform to the
|
||||
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedComparison(ValueError):
|
||||
"""
|
||||
An invalid operation was attempted on a value that doesn't support it.
|
||||
"""Raised when evaluating an unsupported marker comparison.
|
||||
|
||||
This can happen when marker values are compared as versions but do not
|
||||
conform to the :ref:`specification of version specifiers
|
||||
<pypug:version-specifiers>`.
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedEnvironmentName(ValueError):
|
||||
"""
|
||||
A name was attempted to be used that does not exist inside of the
|
||||
environment.
|
||||
"""
|
||||
"""Raised when evaluating a marker that references a missing environment key."""
|
||||
|
||||
|
||||
class Environment(TypedDict):
|
||||
"""
|
||||
A dictionary that represents a Python environment as captured by
|
||||
:func:`default_environment`. All fields are required.
|
||||
"""
|
||||
|
||||
implementation_name: str
|
||||
"""The implementation's identifier, e.g. ``'cpython'``."""
|
||||
|
||||
@@ -121,20 +149,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 +204,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 +233,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 +257,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,18 +270,21 @@ 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)
|
||||
|
||||
|
||||
def format_full_version(info: sys._version_info) -> str:
|
||||
def _format_full_version(info: sys._version_info) -> str:
|
||||
version = f"{info.major}.{info.minor}.{info.micro}"
|
||||
kind = info.releaselevel
|
||||
if kind != "final":
|
||||
@@ -254,7 +293,11 @@ def format_full_version(info: sys._version_info) -> str:
|
||||
|
||||
|
||||
def default_environment() -> Environment:
|
||||
iver = format_full_version(sys.implementation.version)
|
||||
"""Return the default marker environment for the current Python process.
|
||||
|
||||
This is the base environment used by :meth:`Marker.evaluate`.
|
||||
"""
|
||||
iver = _format_full_version(sys.implementation.version)
|
||||
implementation_name = sys.implementation.name
|
||||
return {
|
||||
"implementation_name": implementation_name,
|
||||
@@ -272,10 +315,36 @@ def default_environment() -> Environment:
|
||||
|
||||
|
||||
class Marker:
|
||||
"""Represents a parsed dependency marker expression.
|
||||
|
||||
Marker expressions are parsed according to the
|
||||
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||
|
||||
:param marker: The string representation of a marker expression.
|
||||
:raises InvalidMarker: If ``marker`` cannot be parsed.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
__slots__ = ("_markers",)
|
||||
|
||||
def __init__(self, marker: str) -> None:
|
||||
# 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:
|
||||
@@ -297,36 +366,94 @@ class Marker:
|
||||
except ParserSyntaxError as e:
|
||||
raise InvalidMarker(str(e)) from e
|
||||
|
||||
@classmethod
|
||||
def _from_markers(cls, markers: MarkerList) -> Marker:
|
||||
"""Create a Marker instance from a pre-parsed marker tree.
|
||||
|
||||
This avoids re-parsing serialised marker strings when combining markers.
|
||||
"""
|
||||
new = cls.__new__(cls)
|
||||
new._markers = markers
|
||||
return new
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _format_marker(self._markers)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Marker('{self}')>"
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
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
|
||||
|
||||
return str(self) == str(other)
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return the marker expression string for compactness and stability.
|
||||
# Internal Node objects are excluded; the string is re-parsed on load.
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the marker expression string.
|
||||
try:
|
||||
self._markers = _normalize_extra_values(_parse_marker(state))
|
||||
except ParserSyntaxError as exc:
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
|
||||
return
|
||||
if isinstance(state, dict) and "_markers" in state:
|
||||
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||
markers = state["_markers"]
|
||||
if isinstance(markers, list):
|
||||
self._markers = markers
|
||||
return
|
||||
if isinstance(state, tuple) and len(state) == 2:
|
||||
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||
_, slot_dict = state
|
||||
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
|
||||
markers = slot_dict["_markers"]
|
||||
if isinstance(markers, list):
|
||||
self._markers = markers
|
||||
return
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}")
|
||||
|
||||
def __and__(self, other: Marker) -> Marker:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "and", other._markers])
|
||||
|
||||
def __or__(self, other: Marker) -> Marker:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "or", other._markers])
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
environment: dict[str, str] | None = None,
|
||||
environment: Mapping[str, str | AbstractSet[str]] | None = None,
|
||||
context: EvaluateContext = "metadata",
|
||||
) -> bool:
|
||||
"""Evaluate a marker.
|
||||
|
||||
Return the boolean from evaluating the given marker against the
|
||||
environment. environment is an optional argument to override all or
|
||||
part of the determined environment. The *context* parameter specifies what
|
||||
context the markers are being evaluated for, which influences what markers
|
||||
are considered valid. Acceptable values are "metadata" (for core metadata;
|
||||
default), "lock_file", and "requirement" (i.e. all other situations).
|
||||
Return the boolean from evaluating this marker against the environment.
|
||||
The environment is determined from the current Python process unless
|
||||
passed in explicitly.
|
||||
|
||||
:param environment: Mapping containing keys and values to override the
|
||||
detected environment.
|
||||
:param EvaluateContext context: The context in which the marker is
|
||||
evaluated, which influences what marker names are considered valid.
|
||||
Accepted values are ``"metadata"`` (for core metadata; default),
|
||||
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
|
||||
:raises UndefinedComparison: If the marker uses a comparison on values
|
||||
that are not valid versions per the :ref:`specification of version
|
||||
specifiers <pypug:version-specifiers>`.
|
||||
:raises UndefinedEnvironmentName: If the marker references a value that
|
||||
is missing from the evaluation environment.
|
||||
:returns: ``True`` if the marker matches, otherwise ``False``.
|
||||
|
||||
The environment is determined from the current Python process.
|
||||
"""
|
||||
current_environment = cast(
|
||||
"dict[str, str | AbstractSet[str]]", default_environment()
|
||||
@@ -337,12 +464,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 +486,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
|
||||
|
||||
Reference in New Issue
Block a user