From cc938f984bbbe43c5734b9656c9837ab3a28191f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 10 Mar 2024 23:33:48 +0000 Subject: [PATCH] Modernise type annotations using FA rules from ruff (#785) * Modernise type annotations using `FA` rules from ruff This enforces the use of `from __future__ import annotations` on all files that use objects from `typing` that have clearer syntax available in newer typing annotation syntax. * Use recursive types in `_parser` This makes use of more accurate type checking enabled by newer type checker versions. --------- Co-authored-by: Pradyun Gedam --- pyproject.toml | 1 + src/packaging/_elffile.py | 8 ++- src/packaging/_manylinux.py | 20 +++--- src/packaging/_musllinux.py | 8 ++- src/packaging/_parser.py | 24 +++---- src/packaging/_tokenizer.py | 18 ++--- src/packaging/markers.py | 36 ++++------ src/packaging/metadata.py | 128 ++++++++++++++++------------------ src/packaging/requirements.py | 9 +-- src/packaging/specifiers.py | 50 +++++++------ src/packaging/tags.py | 43 ++++++------ src/packaging/utils.py | 10 +-- src/packaging/version.py | 52 +++++++------- tests/test_requirements.py | 6 +- 14 files changed, 204 insertions(+), 209 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5deff4d7..d2bc3f9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ extend-select = [ "B", "E", "F", + "FA", "I", "N", "UP", diff --git a/src/packaging/_elffile.py b/src/packaging/_elffile.py index 6fb19b30..f7a02180 100644 --- a/src/packaging/_elffile.py +++ b/src/packaging/_elffile.py @@ -8,10 +8,12 @@ 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 @@ def __init__(self, f: IO[bytes]) -> None: 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 1f5f4ab3..08f651fb 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 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 @@ # `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 @@ -168,7 +170,7 @@ def _parse_glibc_version(version_str: str) -> Tuple[int, int]: @functools.lru_cache -def _get_glibc_version() -> Tuple[int, int]: +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 eb4251b5..d2bf30b5 100644 --- a/src/packaging/_musllinux.py +++ b/src/packaging/_musllinux.py @@ -4,11 +4,13 @@ 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 @@ -29,7 +31,7 @@ def _parse_musl_version(output: str) -> Optional[_MuslVersion]: @functools.lru_cache -def _get_musl_version(executable: str) -> Optional[_MuslVersion]: +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 513686a2..c1238c06 100644 --- a/src/packaging/_parser.py +++ b/src/packaging/_parser.py @@ -4,8 +4,10 @@ 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 @@ def serialize(self) -> str: 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 dd0d648d..89d04160 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 @@ def __init__( message: str, *, source: str, - span: Tuple[int, int], + span: tuple[int, int], ) -> None: self.span = span self.message = message @@ -34,7 +36,7 @@ def __str__(self) -> str: 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 @@ def __init__( 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 @@ def raise_syntax_error( 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 c96d22a5..6eb08dcf 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -2,22 +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, -) -from ._parser import ( - parse_marker as _parse_marker, -) +from typing import Any, Callable + +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 @@ -69,7 +63,7 @@ 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)) @@ -96,7 +90,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, @@ -116,14 +110,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 @@ -135,8 +129,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)) @@ -165,7 +159,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": @@ -173,7 +167,7 @@ def format_full_version(info: "sys._version_info") -> str: return version -def default_environment() -> Dict[str, str]: +def default_environment() -> dict[str, str]: iver = format_full_version(sys.implementation.version) implementation_name = sys.implementation.name return { @@ -232,7 +226,7 @@ def __eq__(self, other: Any) -> bool: 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 diff --git a/src/packaging/metadata.py b/src/packaging/metadata.py index fe835071..eb8dc844 100644 --- a/src/packaging/metadata.py +++ b/src/packaging/metadata.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import email.feedparser import email.header import email.message @@ -7,15 +9,9 @@ from typing import ( Any, Callable, - Dict, Generic, - List, Literal, - Optional, - Tuple, - Type, TypedDict, - Union, cast, ) @@ -37,9 +33,9 @@ class ExceptionGroup(Exception): """ 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 @@ -85,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 @@ -123,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 @@ -170,12 +166,12 @@ class RawMetadata(TypedDict, total=False): } -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: @@ -215,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. @@ -277,7 +273,7 @@ def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str: _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 @@ -293,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) @@ -342,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") @@ -484,11 +480,11 @@ def __init__( ) -> 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. @@ -516,7 +512,7 @@ def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> 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)}) @@ -591,7 +587,7 @@ def _process_description_content_type(self, value: str) -> str: ) 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( @@ -603,8 +599,8 @@ def _process_dynamic(self, value: List[str]) -> List[str]: def _process_provides_extra( self, - value: List[str], - ) -> List[utils.NormalizedName]: + value: list[str], + ) -> list[utils.NormalizedName]: normalized_names = [] try: for name in value: @@ -626,8 +622,8 @@ def _process_requires_python(self, value: str) -> specifiers.SpecifierSet: def _process_requires_dist( self, - value: List[str], - ) -> List[requirements.Requirement]: + value: list[str], + ) -> list[requirements.Requirement]: reqs = [] try: for req in value: @@ -650,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 @@ -660,7 +656,7 @@ def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "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) @@ -707,9 +703,7 @@ def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "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 @@ -745,66 +739,66 @@ def from_email( *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 bdc43a7e..4e068c95 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 @@ def __init__(self, requirement_string: str) -> None: 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 6d4066ae..2fa75f7a 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 @@ def __eq__(self, other: object) -> bool: @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 @@ def prereleases(self, value: bool) -> None: """ @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 @@ def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: 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 @@ def __str__(self) -> str: 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] != "~="), @@ -495,7 +497,7 @@ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: 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. @@ -516,9 +518,7 @@ def __contains__(self, item: Union[str, Version]) -> bool: """ 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: @@ -563,7 +563,7 @@ def contains( 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. @@ -627,7 +627,7 @@ def filter( _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 @@ -635,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") @@ -649,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 @@ -666,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 @@ -694,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: @@ -724,7 +722,7 @@ def __init__( 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: @@ -781,7 +779,7 @@ def __str__(self) -> str: 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. @@ -877,8 +875,8 @@ def __contains__(self, item: UnparsedVersion) -> bool: 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. @@ -932,7 +930,7 @@ def contains( 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. @@ -989,8 +987,8 @@ def filter( # 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 89f19261..6667d299 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 sysconfig from importlib.machinery import EXTENSION_SUFFIXES from typing import ( - Dict, - FrozenSet, Iterable, Iterator, - List, - Optional, Sequence, Tuple, - Union, cast, ) @@ -30,7 +27,7 @@ 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 @@ def __repr__(self) -> str: 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 c2c2f75a..d33da5bb 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 cda8e999..46bc2613 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 @@ 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 @@ def __hash__(self) -> int: # 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 @@ def __eq__(self, other: object) -> bool: 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 @@ def epoch(self) -> int: 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 @@ def release(self) -> Tuple[int, ...]: 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 @@ def pre(self) -> Optional[Tuple[str, int]]: 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 @@ def post(self) -> Optional[int]: 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 @@ def dev(self) -> Optional[int]: 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,8 +452,8 @@ def micro(self) -> int: 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. @@ -487,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"). """ @@ -501,11 +503,11 @@ 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 diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 491b3e03..9907b9c9 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 @@ @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: