summaryrefslogtreecommitdiff
diff options
authorMatthias Klose <doko@debian.org>2024-07-10 09:41:25 +0200
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2024-07-10 16:39:24 +0000
commitbe34851b93d29ddedeb323e154275818eea1a471 (patch)
treecdbc568241dde93321884f2f5c952a0276ab03c6
parent811e0e635c98d8d4bb145b5455b930df4c8d3bbf (diff)
Imported using git-ubuntu import.
Notes
Notes: * New upstream version.
-rw-r--r--CHANGELOG.rst5
-rw-r--r--PKG-INFO6
-rw-r--r--debian/changelog6
-rw-r--r--docs/conf.py1
-rw-r--r--docs/development/getting-started.rst8
-rw-r--r--docs/markers.rst14
-rw-r--r--docs/requirements.txt2
-rw-r--r--pyproject.toml48
-rw-r--r--src/packaging/__init__.py2
-rw-r--r--src/packaging/_elffile.py8
-rw-r--r--src/packaging/_manylinux.py22
-rw-r--r--src/packaging/_musllinux.py10
-rw-r--r--src/packaging/_parser.py26
-rw-r--r--src/packaging/_tokenizer.py18
-rw-r--r--src/packaging/markers.py115
-rw-r--r--src/packaging/metadata.py153
-rw-r--r--src/packaging/requirements.py9
-rw-r--r--src/packaging/specifiers.py56
-rw-r--r--src/packaging/tags.py43
-rw-r--r--src/packaging/utils.py10
-rw-r--r--src/packaging/version.py54
-rw-r--r--tests/test_markers.py13
-rw-r--r--tests/test_requirements.py6
23 files changed, 384 insertions, 251 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 68dfb80..afc671b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,11 @@
Changelog
---------
+24.1 - 2024-06-10
+~~~~~~~~~~~~~~~~~
+
+No unreleased changes.
+
24.0 - 2024-03-10
~~~~~~~~~~~~~~~~~
diff --git a/PKG-INFO b/PKG-INFO
index 10ab439..255dc46 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,9 +1,9 @@
Metadata-Version: 2.1
Name: packaging
-Version: 24.0
+Version: 24.1
Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io>
-Requires-Python: >=3.7
+Requires-Python: >=3.8
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
@@ -12,12 +12,12 @@ Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
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 :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
diff --git a/debian/changelog b/debian/changelog
index 77d9d7a..a428f3a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-packaging (24.1-1) unstable; urgency=medium
+
+ * New upstream version.
+
+ -- Matthias Klose <doko@debian.org> Wed, 10 Jul 2024 09:41:25 +0200
+
python-packaging (24.0-1) unstable; urgency=medium
* New upstream version.
diff --git a/docs/conf.py b/docs/conf.py
index d89b26a..b4da79e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,6 +21,7 @@ extensions = [
"sphinx.ext.doctest",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
+ "sphinx_toolbox.more_autodoc.autotypeddict",
]
# General information about the project.
diff --git a/docs/development/getting-started.rst b/docs/development/getting-started.rst
index a713eac..0cad38a 100644
--- a/docs/development/getting-started.rst
+++ b/docs/development/getting-started.rst
@@ -30,11 +30,15 @@ each supported Python version and run the tests. For example:
$ nox -s tests
...
nox > Ran multiple sessions:
- nox > * tests-3.7: success
nox > * tests-3.8: success
nox > * tests-3.9: success
nox > * tests-3.10: success
- nox > * tests-pypy3: skipped
+ nox > * tests-3.11: success
+ nox > * tests-3.12: success
+ nox > * tests-3.13: success
+ nox > * tests-pypy3.8: skipped
+ nox > * tests-pypy3.9: skipped
+ nox > * tests-pypy3.10: skipped
You may not have all the required Python versions installed, in which case you
will see one or more ``InterpreterNotFound`` errors.
diff --git a/docs/markers.rst b/docs/markers.rst
index d3b7676..e802dac 100644
--- a/docs/markers.rst
+++ b/docs/markers.rst
@@ -71,6 +71,20 @@ Reference
:raises: UndefinedEnvironmentName: If the marker accesses a value that
isn't present inside of the environment
dictionary.
+ :rtype: bool
+
+.. autotypeddict:: packaging.markers.Environment
+
+ A dictionary that represents a Python environment as captured by
+ :func:`default_environment`.
+
+.. function:: default_environment()
+
+ Returns a dictionary representing the current Python process. This is the
+ base environment that is used when evaluating markers in
+ :meth:`Marker.evaluate`.
+
+ :rtype: Environment
.. exception:: InvalidMarker
diff --git a/docs/requirements.txt b/docs/requirements.txt
index a95ae18..299a951 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1 +1,3 @@
furo
+sphinx-toolbox
+typing-extensions>=4.1.0; python_version < "3.9"
diff --git a/pyproject.toml b/pyproject.toml
index 9fa0fb1..4f6aca0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ name = "packaging"
description = "Core utilities for Python packages"
dynamic = ["version"]
readme = "README.rst"
-requires-python = ">=3.7"
+requires-python = ">=3.8"
authors = [{name = "Donald Stufft", email = "donald@stufft.io"}]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -18,12 +18,12 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Typing :: Typed",
@@ -51,12 +51,50 @@ exclude_lines = ["pragma: no cover", "@abc.abstractmethod", "@abc.abstractproper
strict = true
show_error_codes = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
+warn_unused_ignores = true
[[tool.mypy.overrides]]
module = ["_manylinux"]
ignore_missing_imports = true
-[tool.isort]
-profile = "black"
-combine_as_imports = true
+[tool.ruff]
+src = ["src"]
+
+[tool.ruff.lint]
+extend-select = [
+ "B",
+ "E",
+ "F",
+ "FA",
+ "I",
+ "N",
+ "UP",
+ "W"
+]
+ignore = [
+ "B009",
+ "B015",
+ "B018",
+ "B027",
+ "B028",
+ "B904",
+ "N818",
+ "UP032",
+ "UP030",
+ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
+ "W191",
+ "E111",
+ "E114",
+ "E117",
+ "D206",
+ "D300",
+ "Q000",
+ "Q001",
+ "Q002",
+ "Q003",
+ "COM812",
+ "COM819",
+ "ISC001",
+ "ISC002",
+]
diff --git a/src/packaging/__init__.py b/src/packaging/__init__.py
index e7c0aa1..9ba41d8 100644
--- a/src/packaging/__init__.py
+++ b/src/packaging/__init__.py
@@ -6,7 +6,7 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
-__version__ = "24.0"
+__version__ = "24.1"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
diff --git a/src/packaging/_elffile.py b/src/packaging/_elffile.py
index 6fb19b3..f7a0218 100644
--- a/src/packaging/_elffile.py
+++ b/src/packaging/_elffile.py
@@ -8,10 +8,12 @@ Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
+from __future__ import annotations
+
import enum
import os
import struct
-from typing import IO, Optional, Tuple
+from typing import IO
class ELFInvalid(ValueError):
@@ -87,11 +89,11 @@ class ELFFile:
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e
- def _read(self, fmt: str) -> Tuple[int, ...]:
+ def _read(self, fmt: str) -> tuple[int, ...]:
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
@property
- def interpreter(self) -> Optional[str]:
+ def interpreter(self) -> str | None:
"""
The path recorded in the ``PT_INTERP`` section header.
"""
diff --git a/src/packaging/_manylinux.py b/src/packaging/_manylinux.py
index ad62505..08f651f 100644
--- a/src/packaging/_manylinux.py
+++ b/src/packaging/_manylinux.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import collections
import contextlib
import functools
@@ -5,7 +7,7 @@ import os
import re
import sys
import warnings
-from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple
+from typing import Generator, Iterator, NamedTuple, Sequence
from ._elffile import EIClass, EIData, ELFFile, EMachine
@@ -17,7 +19,7 @@ EF_ARM_ABI_FLOAT_HARD = 0x00000400
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then.
@contextlib.contextmanager
-def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]:
+def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
try:
with open(path, "rb") as f:
yield ELFFile(f)
@@ -72,7 +74,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
# For now, guess what the highest minor version might be, assume it will
# be 50 for testing. Once this actually happens, update the dictionary
# with the actual value.
-_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50)
+_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
class _GLibCVersion(NamedTuple):
@@ -80,7 +82,7 @@ class _GLibCVersion(NamedTuple):
minor: int
-def _glibc_version_string_confstr() -> Optional[str]:
+def _glibc_version_string_confstr() -> str | None:
"""
Primary implementation of glibc_version_string using os.confstr.
"""
@@ -90,7 +92,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
try:
# Should be a string like "glibc 2.17".
- version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION")
+ version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
assert version_string is not None
_, version = version_string.rsplit()
except (AssertionError, AttributeError, OSError, ValueError):
@@ -99,7 +101,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
return version
-def _glibc_version_string_ctypes() -> Optional[str]:
+def _glibc_version_string_ctypes() -> str | None:
"""
Fallback implementation of glibc_version_string using ctypes.
"""
@@ -143,12 +145,12 @@ def _glibc_version_string_ctypes() -> Optional[str]:
return version_str
-def _glibc_version_string() -> Optional[str]:
+def _glibc_version_string() -> str | None:
"""Returns glibc version string, or None if not using glibc."""
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) -> tuple[int, int]:
"""Parse glibc version.
We use a regexp instead of str.split because we want to discard any
@@ -167,8 +169,8 @@ def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
return int(m.group("major")), int(m.group("minor"))
-@functools.lru_cache()
-def _get_glibc_version() -> Tuple[int, int]:
+@functools.lru_cache
+def _get_glibc_version() -> tuple[int, int]:
version_str = _glibc_version_string()
if version_str is None:
return (-1, -1)
diff --git a/src/packaging/_musllinux.py b/src/packaging/_musllinux.py
index 86419df..d2bf30b 100644
--- a/src/packaging/_musllinux.py
+++ b/src/packaging/_musllinux.py
@@ -4,11 +4,13 @@ This module implements logic to detect if the currently running Python is
linked against musl, and what musl version is used.
"""
+from __future__ import annotations
+
import functools
import re
import subprocess
import sys
-from typing import Iterator, NamedTuple, Optional, Sequence
+from typing import Iterator, NamedTuple, Sequence
from ._elffile import ELFFile
@@ -18,7 +20,7 @@ class _MuslVersion(NamedTuple):
minor: int
-def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
+def _parse_musl_version(output: str) -> _MuslVersion | None:
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
if len(lines) < 2 or lines[0][:4] != "musl":
return None
@@ -28,8 +30,8 @@ def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
-@functools.lru_cache()
-def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
+@functools.lru_cache
+def _get_musl_version(executable: str) -> _MuslVersion | None:
"""Detect currently-running musl runtime version.
This is done by checking the specified executable's dynamic linking
diff --git a/src/packaging/_parser.py b/src/packaging/_parser.py
index 684df75..c1238c0 100644
--- a/src/packaging/_parser.py
+++ b/src/packaging/_parser.py
@@ -1,11 +1,13 @@
"""Handwritten parser of dependency specifiers.
-The docstring for each __parse_* function contains ENBF-inspired grammar representing
+The docstring for each __parse_* function contains EBNF-inspired grammar representing
the implementation.
"""
+from __future__ import annotations
+
import ast
-from typing import Any, List, NamedTuple, Optional, Tuple, Union
+from typing import NamedTuple, Sequence, Tuple, Union
from ._tokenizer import DEFAULT_RULES, Tokenizer
@@ -41,20 +43,16 @@ class Op(Node):
MarkerVar = Union[Variable, Value]
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
-# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]]
-# MarkerList = List[Union["MarkerList", MarkerAtom, str]]
-# mypy does not support recursive type definition
-# https://github.com/python/mypy/issues/731
-MarkerAtom = Any
-MarkerList = List[Any]
+MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
+MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
class ParsedRequirement(NamedTuple):
name: str
url: str
- extras: List[str]
+ extras: list[str]
specifier: str
- marker: Optional[MarkerList]
+ marker: MarkerList | None
# --------------------------------------------------------------------------------------
@@ -87,7 +85,7 @@ def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
def _parse_requirement_details(
tokenizer: Tokenizer,
-) -> Tuple[str, str, Optional[MarkerList]]:
+) -> tuple[str, str, MarkerList | None]:
"""
requirement_details = AT URL (WS requirement_marker?)?
| specifier WS? (requirement_marker)?
@@ -156,7 +154,7 @@ def _parse_requirement_marker(
return marker
-def _parse_extras(tokenizer: Tokenizer) -> List[str]:
+def _parse_extras(tokenizer: Tokenizer) -> list[str]:
"""
extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
"""
@@ -175,11 +173,11 @@ def _parse_extras(tokenizer: Tokenizer) -> List[str]:
return extras
-def _parse_extras_list(tokenizer: Tokenizer) -> List[str]:
+def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
"""
extras_list = identifier (wsp* ',' wsp* identifier)*
"""
- extras: List[str] = []
+ extras: list[str] = []
if not tokenizer.check("IDENTIFIER"):
return extras
diff --git a/src/packaging/_tokenizer.py b/src/packaging/_tokenizer.py
index dd0d648..89d0416 100644
--- a/src/packaging/_tokenizer.py
+++ b/src/packaging/_tokenizer.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
import contextlib
import re
from dataclasses import dataclass
-from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union
+from typing import Iterator, NoReturn
from .specifiers import Specifier
@@ -21,7 +23,7 @@ class ParserSyntaxError(Exception):
message: str,
*,
source: str,
- span: Tuple[int, int],
+ span: tuple[int, int],
) -> None:
self.span = span
self.message = message
@@ -34,7 +36,7 @@ class ParserSyntaxError(Exception):
return "\n ".join([self.message, self.source, marker])
-DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = {
+DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
"LEFT_PARENTHESIS": r"\(",
"RIGHT_PARENTHESIS": r"\)",
"LEFT_BRACKET": r"\[",
@@ -96,13 +98,13 @@ class Tokenizer:
self,
source: str,
*,
- rules: "Dict[str, Union[str, re.Pattern[str]]]",
+ rules: dict[str, str | re.Pattern[str]],
) -> None:
self.source = source
- self.rules: Dict[str, re.Pattern[str]] = {
+ self.rules: dict[str, re.Pattern[str]] = {
name: re.compile(pattern) for name, pattern in rules.items()
}
- self.next_token: Optional[Token] = None
+ self.next_token: Token | None = None
self.position = 0
def consume(self, name: str) -> None:
@@ -154,8 +156,8 @@ class Tokenizer:
self,
message: str,
*,
- span_start: Optional[int] = None,
- span_end: Optional[int] = None,
+ span_start: int | None = None,
+ span_end: int | None = None,
) -> NoReturn:
"""Raise ParserSyntaxError at the given position."""
span = (
diff --git a/src/packaging/markers.py b/src/packaging/markers.py
index 8b98fca..7ac7bb6 100644
--- a/src/packaging/markers.py
+++ b/src/packaging/markers.py
@@ -2,20 +2,16 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
+from __future__ import annotations
+
import operator
import os
import platform
import sys
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union
-
-from ._parser import (
- MarkerAtom,
- MarkerList,
- Op,
- Value,
- Variable,
- parse_marker as _parse_marker,
-)
+from typing import Any, Callable, TypedDict, cast
+
+from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
+from ._parser import parse_marker as _parse_marker
from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
@@ -50,6 +46,78 @@ class UndefinedEnvironmentName(ValueError):
"""
+class Environment(TypedDict):
+ implementation_name: str
+ """The implementation's identifier, e.g. ``'cpython'``."""
+
+ implementation_version: str
+ """
+ The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or
+ ``'7.3.13'`` for PyPy3.10 v7.3.13.
+ """
+
+ os_name: str
+ """
+ The value of :py:data:`os.name`. The name of the operating system dependent module
+ imported, e.g. ``'posix'``.
+ """
+
+ platform_machine: str
+ """
+ Returns the machine type, e.g. ``'i386'``.
+
+ An empty string if the value cannot be determined.
+ """
+
+ platform_release: str
+ """
+ The system's release, e.g. ``'2.2.0'`` or ``'NT'``.
+
+ An empty string if the value cannot be determined.
+ """
+
+ platform_system: str
+ """
+ The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.
+
+ An empty string if the value cannot be determined.
+ """
+
+ platform_version: str
+ """
+ The system's release version, e.g. ``'#3 on degas'``.
+
+ An empty string if the value cannot be determined.
+ """
+
+ python_full_version: str
+ """
+ The Python version as string ``'major.minor.patchlevel'``.
+
+ Note that unlike the Python :py:data:`sys.version`, this value will always include
+ the patchlevel (it defaults to 0).
+ """
+
+ platform_python_implementation: str
+ """
+ A string identifying the Python implementation, e.g. ``'CPython'``.
+ """
+
+ python_version: str
+ """The Python version as string ``'major.minor'``."""
+
+ sys_platform: str
+ """
+ This string contains a platform identifier that can be used to append
+ platform-specific components to :py:data:`sys.path`, for instance.
+
+ For Unix systems, except on Linux and AIX, this is the lowercased OS name as
+ returned by ``uname -s`` with the first part of the version as returned by
+ ``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python
+ was built.
+ """
+
+
def _normalize_extra_values(results: Any) -> Any:
"""
Normalize extra values.
@@ -67,9 +135,8 @@ def _normalize_extra_values(results: Any) -> Any:
def _format_marker(
- marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True
+ marker: list[str] | MarkerAtom | str, first: bool | None = True
) -> str:
-
assert isinstance(marker, (list, tuple, str))
# Sometimes we have a structure like [[...]] which is a single item list
@@ -95,7 +162,7 @@ def _format_marker(
return marker
-_operators: Dict[str, Operator] = {
+_operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": operator.lt,
@@ -115,14 +182,14 @@ def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
else:
return spec.contains(lhs, prereleases=True)
- oper: Optional[Operator] = _operators.get(op.serialize())
+ oper: Operator | None = _operators.get(op.serialize())
if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
return oper(lhs, rhs)
-def _normalize(*values: str, key: str) -> Tuple[str, ...]:
+def _normalize(*values: str, key: str) -> tuple[str, ...]:
# 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
@@ -134,8 +201,8 @@ def _normalize(*values: str, key: str) -> Tuple[str, ...]:
return values
-def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
- groups: List[List[bool]] = [[]]
+def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:
+ groups: list[list[bool]] = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, str))
@@ -164,7 +231,7 @@ def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
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 = "{0.major}.{0.minor}.{0.micro}".format(info)
kind = info.releaselevel
if kind != "final":
@@ -172,7 +239,7 @@ def format_full_version(info: "sys._version_info") -> str:
return version
-def default_environment() -> Dict[str, str]:
+def default_environment() -> Environment:
iver = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
return {
@@ -231,7 +298,7 @@ class Marker:
return str(self) == str(other)
- def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
+ def evaluate(self, environment: dict[str, str] | None = None) -> bool:
"""Evaluate a marker.
Return the boolean from evaluating the given marker against the
@@ -240,8 +307,14 @@ class Marker:
The environment is determined from the current Python process.
"""
- current_environment = default_environment()
+ current_environment = cast("dict[str, str]", default_environment())
current_environment["extra"] = ""
+ # Work around platform.python_version() returning something that is not PEP 440
+ # compliant for non-tagged Python builds. We preserve default_environment()'s
+ # behavior of returning platform.python_version() verbatim, and leave it to the
+ # caller to provide a syntactically valid version if they want to override it.
+ if current_environment["python_full_version"].endswith("+"):
+ current_environment["python_full_version"] += "local"
if environment is not None:
current_environment.update(environment)
# The API used to allow setting extra to None. We need to handle this
diff --git a/src/packaging/metadata.py b/src/packaging/metadata.py
index fb27493..eb8dc84 100644
--- a/src/packaging/metadata.py
+++ b/src/packaging/metadata.py
@@ -1,50 +1,31 @@
+from __future__ import annotations
+
import email.feedparser
import email.header
import email.message
import email.parser
import email.policy
-import sys
import typing
from typing import (
Any,
Callable,
- Dict,
Generic,
- List,
- Optional,
- Tuple,
- Type,
- Union,
+ Literal,
+ TypedDict,
cast,
)
-from . import requirements, specifiers, utils, version as version_module
+from . import requirements, specifiers, utils
+from . import version as version_module
T = typing.TypeVar("T")
-if sys.version_info[:2] >= (3, 8): # pragma: no cover
- from typing import Literal, TypedDict
-else: # pragma: no cover
- if typing.TYPE_CHECKING:
- from typing_extensions import Literal, TypedDict
- else:
- try:
- from typing_extensions import Literal, TypedDict
- except ImportError:
-
- class Literal:
- def __init_subclass__(*_args, **_kwargs):
- pass
-
- class TypedDict:
- def __init_subclass__(*_args, **_kwargs):
- pass
try:
ExceptionGroup
except NameError: # pragma: no cover
- class ExceptionGroup(Exception): # noqa: N818
+ class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
If :external:exc:`ExceptionGroup` is already defined by Python itself,
@@ -52,9 +33,9 @@ except NameError: # pragma: no cover
"""
message: str
- exceptions: List[Exception]
+ exceptions: list[Exception]
- def __init__(self, message: str, exceptions: List[Exception]) -> None:
+ def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message
self.exceptions = exceptions
@@ -100,32 +81,32 @@ class RawMetadata(TypedDict, total=False):
metadata_version: str
name: str
version: str
- platforms: List[str]
+ platforms: list[str]
summary: str
description: str
- keywords: List[str]
+ keywords: list[str]
home_page: str
author: str
author_email: str
license: str
# Metadata 1.1 - PEP 314
- supported_platforms: List[str]
+ supported_platforms: list[str]
download_url: str
- classifiers: List[str]
- requires: List[str]
- provides: List[str]
- obsoletes: List[str]
+ classifiers: list[str]
+ requires: list[str]
+ provides: list[str]
+ obsoletes: list[str]
# Metadata 1.2 - PEP 345
maintainer: str
maintainer_email: str
- requires_dist: List[str]
- provides_dist: List[str]
- obsoletes_dist: List[str]
+ requires_dist: list[str]
+ provides_dist: list[str]
+ obsoletes_dist: list[str]
requires_python: str
- requires_external: List[str]
- project_urls: Dict[str, str]
+ requires_external: list[str]
+ project_urls: dict[str, str]
# Metadata 2.0
# PEP 426 attempted to completely revamp the metadata format
@@ -138,10 +119,10 @@ class RawMetadata(TypedDict, total=False):
# Metadata 2.1 - PEP 566
description_content_type: str
- provides_extra: List[str]
+ provides_extra: list[str]
# Metadata 2.2 - PEP 643
- dynamic: List[str]
+ dynamic: list[str]
# Metadata 2.3 - PEP 685
# No new fields were added in PEP 685, just some edge case were
@@ -185,12 +166,12 @@ _DICT_FIELDS = {
}
-def _parse_keywords(data: str) -> List[str]:
+def _parse_keywords(data: str) -> list[str]:
"""Split a string of comma-separate keyboards into a list of keywords."""
return [k.strip() for k in data.split(",")]
-def _parse_project_urls(data: List[str]) -> Dict[str, str]:
+def _parse_project_urls(data: list[str]) -> dict[str, str]:
"""Parse a list of label/URL string pairings separated by a comma."""
urls = {}
for pair in data:
@@ -230,7 +211,7 @@ def _parse_project_urls(data: List[str]) -> Dict[str, str]:
return urls
-def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str:
+def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
"""Get the body of the message."""
# If our source is a str, then our caller has managed encodings for us,
# and we don't need to deal with it.
@@ -292,7 +273,7 @@ _EMAIL_TO_RAW_MAPPING = {
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
-def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]:
+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``).
This function returns a two-item tuple of dicts. The first dict is of
@@ -308,8 +289,8 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
included in this dict.
"""
- raw: Dict[str, Union[str, List[str], Dict[str, str]]] = {}
- unparsed: Dict[str, List[str]] = {}
+ raw: dict[str, str | list[str] | dict[str, str]] = {}
+ unparsed: dict[str, list[str]] = {}
if isinstance(data, str):
parsed = email.parser.Parser(policy=email.policy.compat32).parsestr(data)
@@ -357,7 +338,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
# The Header object stores it's data as chunks, and each chunk
# can be independently encoded, so we'll need to check each
# of them.
- chunks: List[Tuple[bytes, Optional[str]]] = []
+ chunks: list[tuple[bytes, str | None]] = []
for bin, encoding in email.header.decode_header(h):
try:
bin.decode("utf8", "strict")
@@ -499,11 +480,11 @@ class _Validator(Generic[T]):
) -> None:
self.added = added
- def __set_name__(self, _owner: "Metadata", name: str) -> None:
+ def __set_name__(self, _owner: Metadata, name: str) -> None:
self.name = name
self.raw_name = _RAW_TO_EMAIL_MAPPING[name]
- def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T:
+ def __get__(self, instance: Metadata, _owner: type[Metadata]) -> T:
# With Python 3.8, the caching can be replaced with functools.cached_property().
# No need to check the cache as attribute lookup will resolve into the
# instance's __dict__ before __get__ is called.
@@ -531,7 +512,7 @@ class _Validator(Generic[T]):
return cast(T, value)
def _invalid_metadata(
- self, msg: str, cause: Optional[Exception] = None
+ self, msg: str, cause: Exception | None = None
) -> InvalidMetadata:
exc = InvalidMetadata(
self.raw_name, msg.format_map({"field": repr(self.raw_name)})
@@ -606,7 +587,7 @@ class _Validator(Generic[T]):
)
return value
- def _process_dynamic(self, value: List[str]) -> List[str]:
+ def _process_dynamic(self, value: list[str]) -> list[str]:
for dynamic_field in map(str.lower, value):
if dynamic_field in {"name", "version", "metadata-version"}:
raise self._invalid_metadata(
@@ -618,8 +599,8 @@ class _Validator(Generic[T]):
def _process_provides_extra(
self,
- value: List[str],
- ) -> List[utils.NormalizedName]:
+ value: list[str],
+ ) -> list[utils.NormalizedName]:
normalized_names = []
try:
for name in value:
@@ -641,8 +622,8 @@ class _Validator(Generic[T]):
def _process_requires_dist(
self,
- value: List[str],
- ) -> List[requirements.Requirement]:
+ value: list[str],
+ ) -> list[requirements.Requirement]:
reqs = []
try:
for req in value:
@@ -665,7 +646,7 @@ class Metadata:
_raw: RawMetadata
@classmethod
- def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata":
+ def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> Metadata:
"""Create an instance from :class:`RawMetadata`.
If *validate* is true, all metadata will be validated. All exceptions
@@ -675,7 +656,7 @@ class Metadata:
ins._raw = data.copy() # Mutations occur due to caching enriched values.
if validate:
- exceptions: List[Exception] = []
+ exceptions: list[Exception] = []
try:
metadata_version = ins.metadata_version
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
@@ -722,9 +703,7 @@ class Metadata:
return ins
@classmethod
- def from_email(
- cls, data: Union[bytes, str], *, validate: bool = True
- ) -> "Metadata":
+ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:
"""Parse metadata from email headers.
If *validate* is true, the metadata will be validated. All exceptions
@@ -760,66 +739,66 @@ class Metadata:
*validate* parameter)"""
version: _Validator[version_module.Version] = _Validator()
""":external:ref:`core-metadata-version` (required)"""
- dynamic: _Validator[Optional[List[str]]] = _Validator(
+ dynamic: _Validator[list[str] | None] = _Validator(
added="2.2",
)
""":external:ref:`core-metadata-dynamic`
(validated against core metadata field names and lowercased)"""
- platforms: _Validator[Optional[List[str]]] = _Validator()
+ platforms: _Validator[list[str] | None] = _Validator()
""":external:ref:`core-metadata-platform`"""
- supported_platforms: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+ supported_platforms: _Validator[list[str] | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-supported-platform`"""
- summary: _Validator[Optional[str]] = _Validator()
+ summary: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-summary` (validated to contain no newlines)"""
- description: _Validator[Optional[str]] = _Validator() # TODO 2.1: can be in body
+ description: _Validator[str | None] = _Validator() # TODO 2.1: can be in body
""":external:ref:`core-metadata-description`"""
- description_content_type: _Validator[Optional[str]] = _Validator(added="2.1")
+ description_content_type: _Validator[str | None] = _Validator(added="2.1")
""":external:ref:`core-metadata-description-content-type` (validated)"""
- keywords: _Validator[Optional[List[str]]] = _Validator()
+ keywords: _Validator[list[str] | None] = _Validator()
""":external:ref:`core-metadata-keywords`"""
- home_page: _Validator[Optional[str]] = _Validator()
+ home_page: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-home-page`"""
- download_url: _Validator[Optional[str]] = _Validator(added="1.1")
+ download_url: _Validator[str | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-download-url`"""
- author: _Validator[Optional[str]] = _Validator()
+ author: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-author`"""
- author_email: _Validator[Optional[str]] = _Validator()
+ author_email: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-author-email`"""
- maintainer: _Validator[Optional[str]] = _Validator(added="1.2")
+ maintainer: _Validator[str | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-maintainer`"""
- maintainer_email: _Validator[Optional[str]] = _Validator(added="1.2")
+ maintainer_email: _Validator[str | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-maintainer-email`"""
- license: _Validator[Optional[str]] = _Validator()
+ license: _Validator[str | None] = _Validator()
""":external:ref:`core-metadata-license`"""
- classifiers: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+ classifiers: _Validator[list[str] | None] = _Validator(added="1.1")
""":external:ref:`core-metadata-classifier`"""
- requires_dist: _Validator[Optional[List[requirements.Requirement]]] = _Validator(
+ requires_dist: _Validator[list[requirements.Requirement] | None] = _Validator(
added="1.2"
)
""":external:ref:`core-metadata-requires-dist`"""
- requires_python: _Validator[Optional[specifiers.SpecifierSet]] = _Validator(
+ requires_python: _Validator[specifiers.SpecifierSet | None] = _Validator(
added="1.2"
)
""":external:ref:`core-metadata-requires-python`"""
# Because `Requires-External` allows for non-PEP 440 version specifiers, we
# don't do any processing on the values.
- requires_external: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+ requires_external: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-requires-external`"""
- project_urls: _Validator[Optional[Dict[str, str]]] = _Validator(added="1.2")
+ project_urls: _Validator[dict[str, str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-project-url`"""
# PEP 685 lets us raise an error if an extra doesn't pass `Name` validation
# regardless of metadata version.
- provides_extra: _Validator[Optional[List[utils.NormalizedName]]] = _Validator(
+ provides_extra: _Validator[list[utils.NormalizedName] | None] = _Validator(
added="2.1",
)
""":external:ref:`core-metadata-provides-extra`"""
- provides_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+ provides_dist: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-provides-dist`"""
- obsoletes_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+ obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-obsoletes-dist`"""
- requires: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+ requires: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Requires`` (deprecated)"""
- provides: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+ provides: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Provides`` (deprecated)"""
- obsoletes: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+ obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Obsoletes`` (deprecated)"""
diff --git a/src/packaging/requirements.py b/src/packaging/requirements.py
index bdc43a7..4e068c9 100644
--- a/src/packaging/requirements.py
+++ b/src/packaging/requirements.py
@@ -1,8 +1,9 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
+from __future__ import annotations
-from typing import Any, Iterator, Optional, Set
+from typing import Any, Iterator
from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError
@@ -37,10 +38,10 @@ class Requirement:
raise InvalidRequirement(str(e)) from e
self.name: str = parsed.name
- self.url: Optional[str] = parsed.url or None
- self.extras: Set[str] = set(parsed.extras or [])
+ self.url: str | None = parsed.url or None
+ self.extras: set[str] = set(parsed.extras or [])
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
- self.marker: Optional[Marker] = None
+ self.marker: Marker | None = None
if parsed.marker is not None:
self.marker = Marker.__new__(Marker)
self.marker._markers = _normalize_extra_values(parsed.marker)
diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py
index 2d015ba..2fa75f7 100644
--- a/src/packaging/specifiers.py
+++ b/src/packaging/specifiers.py
@@ -8,10 +8,12 @@
from packaging.version import Version
"""
+from __future__ import annotations
+
import abc
import itertools
import re
-from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union
+from typing import Callable, Iterable, Iterator, TypeVar, Union
from .utils import canonicalize_version
from .version import Version
@@ -64,7 +66,7 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
- def prereleases(self) -> Optional[bool]:
+ def prereleases(self) -> bool | None:
"""Whether or not pre-releases as a whole are allowed.
This can be set to either ``True`` or ``False`` to explicitly enable or disable
@@ -79,14 +81,14 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
"""
@abc.abstractmethod
- def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
+ def contains(self, item: str, prereleases: bool | None = None) -> bool:
"""
Determines if the given item is contained within this specifier.
"""
@abc.abstractmethod
def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+ self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
) -> Iterator[UnparsedVersionVar]:
"""
Takes an iterable of items and filters them so that only items which
@@ -217,7 +219,7 @@ class Specifier(BaseSpecifier):
"===": "arbitrary",
}
- def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+ def __init__(self, spec: str = "", prereleases: bool | None = None) -> None:
"""Initialize a Specifier instance.
:param spec:
@@ -234,7 +236,7 @@ class Specifier(BaseSpecifier):
if not match:
raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
- self._spec: Tuple[str, str] = (
+ self._spec: tuple[str, str] = (
match.group("operator").strip(),
match.group("version").strip(),
)
@@ -318,7 +320,7 @@ class Specifier(BaseSpecifier):
return "{}{}".format(*self._spec)
@property
- def _canonical_spec(self) -> Tuple[str, str]:
+ def _canonical_spec(self) -> tuple[str, str]:
canonical_version = canonicalize_version(
self._spec[1],
strip_trailing_zero=(self._spec[0] != "~="),
@@ -364,7 +366,6 @@ class Specifier(BaseSpecifier):
return operator_callable
def _compare_compatible(self, prospective: Version, spec: str) -> bool:
-
# Compatible releases have an equivalent combination of >= and ==. That
# is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
# implement this in terms of the other specifiers instead of
@@ -385,7 +386,6 @@ class Specifier(BaseSpecifier):
)
def _compare_equal(self, prospective: Version, spec: str) -> bool:
-
# We need special logic to handle prefix matching
if spec.endswith(".*"):
# In the case of prefix matching we want to ignore local segment.
@@ -429,21 +429,18 @@ class Specifier(BaseSpecifier):
return not self._compare_equal(prospective, spec)
def _compare_less_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)
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)
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)
@@ -468,7 +465,6 @@ class Specifier(BaseSpecifier):
return True
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)
@@ -501,7 +497,7 @@ class Specifier(BaseSpecifier):
def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
return str(prospective).lower() == str(spec).lower()
- def __contains__(self, item: Union[str, Version]) -> bool:
+ def __contains__(self, item: str | Version) -> bool:
"""Return whether or not the item is contained in this specifier.
:param item: The item to check for.
@@ -522,9 +518,7 @@ class Specifier(BaseSpecifier):
"""
return self.contains(item)
- def contains(
- self, item: UnparsedVersion, prereleases: Optional[bool] = None
- ) -> bool:
+ def contains(self, item: UnparsedVersion, prereleases: bool | None = None) -> bool:
"""Return whether or not the item is contained in this specifier.
:param item:
@@ -569,7 +563,7 @@ class Specifier(BaseSpecifier):
return operator_callable(normalized_item, self.version)
def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+ self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
) -> Iterator[UnparsedVersionVar]:
"""Filter items in the given iterable, that match the specifier.
@@ -633,7 +627,7 @@ class Specifier(BaseSpecifier):
_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
-def _version_split(version: str) -> List[str]:
+def _version_split(version: str) -> list[str]:
"""Split version into components.
The split components are intended for version comparison. The logic does
@@ -641,7 +635,7 @@ def _version_split(version: str) -> List[str]:
components back with :func:`_version_join` may not produce the original
version string.
"""
- result: List[str] = []
+ result: list[str] = []
epoch, _, rest = version.rpartition("!")
result.append(epoch or "0")
@@ -655,7 +649,7 @@ def _version_split(version: str) -> List[str]:
return result
-def _version_join(components: List[str]) -> str:
+def _version_join(components: list[str]) -> str:
"""Join split version components into a version string.
This function assumes the input came from :func:`_version_split`, where the
@@ -672,7 +666,7 @@ def _is_not_suffix(segment: str) -> bool:
)
-def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
+def _pad_version(left: list[str], right: list[str]) -> tuple[list[str], list[str]]:
left_split, right_split = [], []
# Get the release segment of our versions
@@ -700,9 +694,7 @@ class SpecifierSet(BaseSpecifier):
specifiers (``>=3.0,!=3.1``), or no specifier at all.
"""
- def __init__(
- self, specifiers: str = "", prereleases: Optional[bool] = None
- ) -> None:
+ def __init__(self, specifiers: str = "", prereleases: bool | None = None) -> None:
"""Initialize a SpecifierSet instance.
:param specifiers:
@@ -730,7 +722,7 @@ class SpecifierSet(BaseSpecifier):
self._prereleases = prereleases
@property
- def prereleases(self) -> Optional[bool]:
+ def prereleases(self) -> bool | None:
# If we have been given an explicit prerelease modifier, then we'll
# pass that through here.
if self._prereleases is not None:
@@ -787,7 +779,7 @@ class SpecifierSet(BaseSpecifier):
def __hash__(self) -> int:
return hash(self._specs)
- def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
+ def __and__(self, other: SpecifierSet | str) -> SpecifierSet:
"""Return a SpecifierSet which is a combination of the two sets.
:param other: The other object to combine with.
@@ -883,8 +875,8 @@ class SpecifierSet(BaseSpecifier):
def contains(
self,
item: UnparsedVersion,
- prereleases: Optional[bool] = None,
- installed: Optional[bool] = None,
+ prereleases: bool | None = None,
+ installed: bool | None = None,
) -> bool:
"""Return whether or not the item is contained in this SpecifierSet.
@@ -938,7 +930,7 @@ class SpecifierSet(BaseSpecifier):
return all(s.contains(item, prereleases=prereleases) for s in self._specs)
def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+ self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
) -> Iterator[UnparsedVersionVar]:
"""Filter items in the given iterable, that match the specifiers in this set.
@@ -995,8 +987,8 @@ class SpecifierSet(BaseSpecifier):
# which will filter out any pre-releases, unless there are no final
# releases.
else:
- filtered: List[UnparsedVersionVar] = []
- found_prereleases: List[UnparsedVersionVar] = []
+ filtered: list[UnparsedVersionVar] = []
+ found_prereleases: list[UnparsedVersionVar] = []
for item in iterable:
parsed_version = _coerce_version(item)
diff --git a/src/packaging/tags.py b/src/packaging/tags.py
index 89f1926..6667d29 100644
--- a/src/packaging/tags.py
+++ b/src/packaging/tags.py
@@ -2,6 +2,8 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
+from __future__ import annotations
+
import logging
import platform
import re
@@ -11,15 +13,10 @@ import sys
import sysconfig
from importlib.machinery import EXTENSION_SUFFIXES
from typing import (
- Dict,
- FrozenSet,
Iterable,
Iterator,
- List,
- Optional,
Sequence,
Tuple,
- Union,
cast,
)
@@ -30,7 +27,7 @@ logger = logging.getLogger(__name__)
PythonVersion = Sequence[int]
MacVersion = Tuple[int, int]
-INTERPRETER_SHORT_NAMES: Dict[str, str] = {
+INTERPRETER_SHORT_NAMES: dict[str, str] = {
"python": "py", # Generic.
"cpython": "cp",
"pypy": "pp",
@@ -96,7 +93,7 @@ class Tag:
return f"<{self} @ {id(self)}>"
-def parse_tag(tag: str) -> FrozenSet[Tag]:
+def parse_tag(tag: str) -> frozenset[Tag]:
"""
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
@@ -112,8 +109,8 @@ def parse_tag(tag: str) -> FrozenSet[Tag]:
return frozenset(tags)
-def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
- value: Union[int, str, None] = sysconfig.get_config_var(name)
+def _get_config_var(name: str, warn: bool = False) -> int | str | None:
+ value: int | str | None = sysconfig.get_config_var(name)
if value is None and warn:
logger.debug(
"Config variable '%s' is unset, Python ABI tag may be incorrect", name
@@ -125,7 +122,7 @@ def _normalize_string(string: str) -> str:
return string.replace(".", "_").replace("-", "_").replace(" ", "_")
-def _is_threaded_cpython(abis: List[str]) -> bool:
+def _is_threaded_cpython(abis: list[str]) -> bool:
"""
Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
@@ -151,7 +148,7 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
-def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
+def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
py_version = tuple(py_version) # To allow for version comparison.
abis = []
version = _version_nodot(py_version[:2])
@@ -185,9 +182,9 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
def cpython_tags(
- python_version: Optional[PythonVersion] = None,
- abis: Optional[Iterable[str]] = None,
- platforms: Optional[Iterable[str]] = None,
+ python_version: PythonVersion | None = None,
+ abis: Iterable[str] | None = None,
+ platforms: Iterable[str] | None = None,
*,
warn: bool = False,
) -> Iterator[Tag]:
@@ -244,7 +241,7 @@ def cpython_tags(
yield Tag(interpreter, "abi3", platform_)
-def _generic_abi() -> List[str]:
+def _generic_abi() -> list[str]:
"""
Return the ABI tag based on EXT_SUFFIX.
"""
@@ -286,9 +283,9 @@ def _generic_abi() -> List[str]:
def generic_tags(
- interpreter: Optional[str] = None,
- abis: Optional[Iterable[str]] = None,
- platforms: Optional[Iterable[str]] = None,
+ interpreter: str | None = None,
+ abis: Iterable[str] | None = None,
+ platforms: Iterable[str] | None = None,
*,
warn: bool = False,
) -> Iterator[Tag]:
@@ -332,9 +329,9 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
def compatible_tags(
- python_version: Optional[PythonVersion] = None,
- interpreter: Optional[str] = None,
- platforms: Optional[Iterable[str]] = None,
+ python_version: PythonVersion | None = None,
+ interpreter: str | None = None,
+ platforms: Iterable[str] | None = None,
) -> Iterator[Tag]:
"""
Yields the sequence of tags that are compatible with a specific version of Python.
@@ -366,7 +363,7 @@ def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
return "i386"
-def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
+def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
formats = [cpu_arch]
if cpu_arch == "x86_64":
if version < (10, 4):
@@ -399,7 +396,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
def mac_platforms(
- version: Optional[MacVersion] = None, arch: Optional[str] = None
+ version: MacVersion | None = None, arch: str | None = None
) -> Iterator[str]:
"""
Yields the platform tags for a macOS system.
diff --git a/src/packaging/utils.py b/src/packaging/utils.py
index c2c2f75..d33da5b 100644
--- a/src/packaging/utils.py
+++ b/src/packaging/utils.py
@@ -2,8 +2,10 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
+from __future__ import annotations
+
import re
-from typing import FrozenSet, NewType, Tuple, Union, cast
+from typing import NewType, Tuple, Union, cast
from .tags import Tag, parse_tag
from .version import InvalidVersion, Version
@@ -53,7 +55,7 @@ def is_normalized_name(name: str) -> bool:
def canonicalize_version(
- version: Union[Version, str], *, strip_trailing_zero: bool = True
+ version: Version | str, *, strip_trailing_zero: bool = True
) -> str:
"""
This is very similar to Version.__str__, but has one subtle difference
@@ -102,7 +104,7 @@ def canonicalize_version(
def parse_wheel_filename(
filename: str,
-) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
+) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
if not filename.endswith(".whl"):
raise InvalidWheelFilename(
f"Invalid wheel filename (extension must be '.whl'): {filename}"
@@ -143,7 +145,7 @@ def parse_wheel_filename(
return (name, version, build, tags)
-def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
+def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
if filename.endswith(".tar.gz"):
file_stem = filename[: -len(".tar.gz")]
elif filename.endswith(".zip"):
diff --git a/src/packaging/version.py b/src/packaging/version.py
index 5faab9b..46bc261 100644
--- a/src/packaging/version.py
+++ b/src/packaging/version.py
@@ -7,9 +7,11 @@
from packaging.version import parse, Version
"""
+from __future__ import annotations
+
import itertools
import re
-from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union
+from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
@@ -35,14 +37,14 @@ VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
class _Version(NamedTuple):
epoch: int
- release: Tuple[int, ...]
- dev: Optional[Tuple[str, int]]
- pre: Optional[Tuple[str, int]]
- post: Optional[Tuple[str, int]]
- local: Optional[LocalType]
+ release: tuple[int, ...]
+ dev: tuple[str, int] | None
+ pre: tuple[str, int] | None
+ post: tuple[str, int] | None
+ local: LocalType | None
-def parse(version: str) -> "Version":
+def parse(version: str) -> Version:
"""Parse the given version string.
>>> parse('1.0.dev1')
@@ -65,7 +67,7 @@ class InvalidVersion(ValueError):
class _BaseVersion:
- _key: Tuple[Any, ...]
+ _key: tuple[Any, ...]
def __hash__(self) -> int:
return hash(self._key)
@@ -73,13 +75,13 @@ class _BaseVersion:
# Please keep the duplicated `isinstance` check
# in the six comparisons hereunder
# unless you find a way to avoid adding overhead function calls.
- def __lt__(self, other: "_BaseVersion") -> bool:
+ def __lt__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented
return self._key < other._key
- def __le__(self, other: "_BaseVersion") -> bool:
+ def __le__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented
@@ -91,13 +93,13 @@ class _BaseVersion:
return self._key == other._key
- def __ge__(self, other: "_BaseVersion") -> bool:
+ def __ge__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented
return self._key >= other._key
- def __gt__(self, other: "_BaseVersion") -> bool:
+ def __gt__(self, other: _BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented
@@ -274,7 +276,7 @@ class Version(_BaseVersion):
return self._version.epoch
@property
- def release(self) -> Tuple[int, ...]:
+ def release(self) -> tuple[int, ...]:
"""The components of the "release" segment of the version.
>>> Version("1.2.3").release
@@ -290,7 +292,7 @@ class Version(_BaseVersion):
return self._version.release
@property
- def pre(self) -> Optional[Tuple[str, int]]:
+ def pre(self) -> tuple[str, int] | None:
"""The pre-release segment of the version.
>>> print(Version("1.2.3").pre)
@@ -305,7 +307,7 @@ class Version(_BaseVersion):
return self._version.pre
@property
- def post(self) -> Optional[int]:
+ def post(self) -> int | None:
"""The post-release number of the version.
>>> print(Version("1.2.3").post)
@@ -316,7 +318,7 @@ class Version(_BaseVersion):
return self._version.post[1] if self._version.post else None
@property
- def dev(self) -> Optional[int]:
+ def dev(self) -> int | None:
"""The development number of the version.
>>> print(Version("1.2.3").dev)
@@ -327,7 +329,7 @@ class Version(_BaseVersion):
return self._version.dev[1] if self._version.dev else None
@property
- def local(self) -> Optional[str]:
+ def local(self) -> str | None:
"""The local version segment of the version.
>>> print(Version("1.2.3").local)
@@ -450,9 +452,8 @@ class Version(_BaseVersion):
def _parse_letter_version(
- letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
-) -> Optional[Tuple[str, int]]:
-
+ 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.
@@ -488,7 +489,7 @@ def _parse_letter_version(
_local_version_separators = re.compile(r"[\._-]")
-def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
+def _parse_local_version(local: str | None) -> LocalType | None:
"""
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
"""
@@ -502,13 +503,12 @@ def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
def _cmpkey(
epoch: int,
- release: Tuple[int, ...],
- pre: Optional[Tuple[str, int]],
- post: Optional[Tuple[str, int]],
- dev: Optional[Tuple[str, int]],
- local: Optional[LocalType],
+ release: tuple[int, ...],
+ pre: tuple[str, int] | None,
+ post: tuple[str, int] | None,
+ dev: tuple[str, int] | None,
+ 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
diff --git a/tests/test_markers.py b/tests/test_markers.py
index a2f92b3..775b51f 100644
--- a/tests/test_markers.py
+++ b/tests/test_markers.py
@@ -8,6 +8,7 @@ import os
import platform
import sys
from typing import cast
+from unittest import mock
import pytest
@@ -19,6 +20,7 @@ from packaging.markers import (
default_environment,
format_full_version,
)
+from packaging.version import InvalidVersion
VARIABLES = [
"extra",
@@ -384,3 +386,14 @@ class TestMarker:
assert str(Marker(lhs)) == f'"{normalized_name}" == extra'
assert str(Marker(rhs)) == f'extra == "{normalized_name}"'
+
+ def test_python_full_version_untagged_user_provided(self):
+ """A user-provided python_full_version ending with a + fails to parse."""
+ with pytest.raises(InvalidVersion):
+ Marker("python_full_version < '3.12'").evaluate(
+ {"python_full_version": "3.11.1+"}
+ )
+
+ def test_python_full_version_untagged(self):
+ with mock.patch("platform.python_version", return_value="3.11.1+"):
+ assert Marker("python_full_version < '3.12'").evaluate()
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index 491b3e0..9907b9c 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -2,7 +2,7 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
-from typing import Optional, Set
+from __future__ import annotations
import pytest
@@ -108,9 +108,9 @@ DIFFERENT_DEPENDENCIES = [
@pytest.mark.parametrize("whitespace", ["", " ", "\t"])
def test_basic_valid_requirement_parsing(
name: str,
- extras: Set[str],
+ extras: set[str],
specifier: str,
- url: Optional[str],
+ url: str | None,
marker: str,
whitespace: str,
) -> None: