setuptools
Advanced tools
Sorry, the diff of this file is not supported yet
| This software is made available under the terms of *either* of the licenses | ||
| found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made | ||
| under the terms of *both* these licenses. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| Metadata-Version: 2.4 | ||
| Name: packaging | ||
| Version: 26.0 | ||
| Summary: Core utilities for Python packages | ||
| Author-email: Donald Stufft <donald@stufft.io> | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/x-rst | ||
| License-Expression: Apache-2.0 OR BSD-2-Clause | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| 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 :: 3.14 | ||
| Classifier: Programming Language :: Python :: Implementation :: CPython | ||
| Classifier: Programming Language :: Python :: Implementation :: PyPy | ||
| Classifier: Typing :: Typed | ||
| License-File: LICENSE | ||
| License-File: LICENSE.APACHE | ||
| License-File: LICENSE.BSD | ||
| Project-URL: Documentation, https://packaging.pypa.io/ | ||
| Project-URL: Source, https://github.com/pypa/packaging | ||
| packaging | ||
| ========= | ||
| .. start-intro | ||
| Reusable core utilities for various Python Packaging | ||
| `interoperability specifications <https://packaging.python.org/specifications/>`_. | ||
| This library provides utilities that implement the interoperability | ||
| specifications which have clearly one correct behaviour (eg: :pep:`440`) | ||
| or benefit greatly from having a single shared implementation (eg: :pep:`425`). | ||
| .. end-intro | ||
| The ``packaging`` project includes the following: version handling, specifiers, | ||
| markers, requirements, tags, metadata, lockfiles, utilities. | ||
| Documentation | ||
| ------------- | ||
| The `documentation`_ provides information and the API for the following: | ||
| - Version Handling | ||
| - Specifiers | ||
| - Markers | ||
| - Requirements | ||
| - Tags | ||
| - Metadata | ||
| - Lockfiles | ||
| - Utilities | ||
| Installation | ||
| ------------ | ||
| Use ``pip`` to install these utilities:: | ||
| pip install packaging | ||
| The ``packaging`` library uses calendar-based versioning (``YY.N``). | ||
| Discussion | ||
| ---------- | ||
| If you run into bugs, you can file them in our `issue tracker`_. | ||
| You can also join ``#pypa`` on Freenode to ask questions or get involved. | ||
| .. _`documentation`: https://packaging.pypa.io/ | ||
| .. _`issue tracker`: https://github.com/pypa/packaging/issues | ||
| Code of Conduct | ||
| --------------- | ||
| Everyone interacting in the packaging project's codebases, issue trackers, chat | ||
| rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. | ||
| .. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md | ||
| Contributing | ||
| ------------ | ||
| The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as | ||
| well as how to report a potential security issue. The documentation for this | ||
| project also covers information about `project development`_ and `security`_. | ||
| .. _`project development`: https://packaging.pypa.io/en/latest/development/ | ||
| .. _`security`: https://packaging.pypa.io/en/latest/security/ | ||
| Project History | ||
| --------------- | ||
| Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for | ||
| recent changes and project history. | ||
| .. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ | ||
| packaging-26.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 | ||
| packaging-26.0.dist-info/METADATA,sha256=M2K7fWom2iliuo2qpHhc0LrKwhq6kIoRlcyPWVgKJlo,3309 | ||
| packaging-26.0.dist-info/RECORD,, | ||
| packaging-26.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| packaging-26.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 | ||
| packaging-26.0.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 | ||
| packaging-26.0.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 | ||
| packaging-26.0.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 | ||
| packaging/__init__.py,sha256=y4lVbpeBzCGk-IPDw5BGBZ_b0P3ukEEJZAbGYc6Ey8c,494 | ||
| packaging/_elffile.py,sha256=-sKkptYqzYw2-x3QByJa5mB4rfPWu1pxkZHRx1WAFCY,3211 | ||
| packaging/_manylinux.py,sha256=Hf6nB0cOrayEs96-p3oIXAgGnFquv20DO5l-o2_Xnv0,9559 | ||
| packaging/_musllinux.py,sha256=Z6swjH3MA7XS3qXnmMN7QPhqP3fnoYI0eQ18e9-HgAE,2707 | ||
| packaging/_parser.py,sha256=U_DajsEx2VoC_F46fSVV3hDKNCWoQYkPkasO3dld0ig,10518 | ||
| packaging/_structures.py,sha256=Hn49Ta8zV9Wo8GiCL8Nl2ARZY983Un3pruZGVNldPwE,1514 | ||
| packaging/_tokenizer.py,sha256=M8EwNIdXeL9NMFuFrQtiOKwjka_xFx8KjRQnfE8O_z8,5421 | ||
| packaging/licenses/__init__.py,sha256=TwXLHZCXwSgdFwRLPxW602T6mSieunSFHM6fp8pgW78,5819 | ||
| packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122 | ||
| packaging/markers.py,sha256=ZX-cLvW1S3cZcEc0fHI4z7zSx5U2T19yMpDP_mE-CYw,12771 | ||
| packaging/metadata.py,sha256=CWVZpN_HfoYMSSDuCP7igOvGgqA9AOmpW8f3qTisfnc,39360 | ||
| packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| packaging/pylock.py,sha256=-R1uNfJ4PaLto7Mg62YsGOHgvskuiIEqPwxOywl42Jk,22537 | ||
| packaging/requirements.py,sha256=PMCAWD8aNMnVD-6uZMedhBuAVX2573eZ4yPBLXmz04I,2870 | ||
| packaging/specifiers.py,sha256=EPNPimY_zFivthv1vdjZYz5IqkKGsnKR2yKh-EVyvZw,40797 | ||
| packaging/tags.py,sha256=cXLV1pJD3UtJlDg7Wz3zrfdQhRZqr8jumSAKKAAd2xE,22856 | ||
| packaging/utils.py,sha256=N4c6oZzFJy6klTZ3AnkNz7sSkJesuFWPp68LA3B5dAo,5040 | ||
| packaging/version.py,sha256=7XWlL2IDYLwDYC0ht6cFEhapLwLWbmyo4rb7sEFj0x8,23272 |
Sorry, the diff of this file is not supported yet
| Wheel-Version: 1.0 | ||
| Generator: flit 3.12.0 | ||
| Root-Is-Purelib: true | ||
| Tag: py3-none-any |
| from __future__ import annotations | ||
| import dataclasses | ||
| import logging | ||
| import re | ||
| from collections.abc import Mapping, Sequence | ||
| from dataclasses import dataclass | ||
| from datetime import datetime | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
| Any, | ||
| Callable, | ||
| Protocol, | ||
| TypeVar, | ||
| ) | ||
| from .markers import Marker | ||
| from .specifiers import SpecifierSet | ||
| from .utils import NormalizedName, is_normalized_name | ||
| from .version import Version | ||
| if TYPE_CHECKING: # pragma: no cover | ||
| from pathlib import Path | ||
| from typing_extensions import Self | ||
| _logger = logging.getLogger(__name__) | ||
| __all__ = [ | ||
| "Package", | ||
| "PackageArchive", | ||
| "PackageDirectory", | ||
| "PackageSdist", | ||
| "PackageVcs", | ||
| "PackageWheel", | ||
| "Pylock", | ||
| "PylockUnsupportedVersionError", | ||
| "PylockValidationError", | ||
| "is_valid_pylock_path", | ||
| ] | ||
| _T = TypeVar("_T") | ||
| _T2 = TypeVar("_T2") | ||
| class _FromMappingProtocol(Protocol): # pragma: no cover | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: ... | ||
| _FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol) | ||
| _PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$") | ||
| def is_valid_pylock_path(path: Path) -> bool: | ||
| """Check if the given path is a valid pylock file path.""" | ||
| return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name)) | ||
| def _toml_key(key: str) -> str: | ||
| return key.replace("_", "-") | ||
| def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401 | ||
| if isinstance(value, (Version, Marker, SpecifierSet)): | ||
| return str(value) | ||
| if isinstance(value, Sequence) and key == "environments": | ||
| return [str(v) for v in value] | ||
| return value | ||
| def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]: | ||
| return { | ||
| _toml_key(key): _toml_value(key, value) | ||
| for key, value in data | ||
| if value is not None | ||
| } | ||
| def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None: | ||
| """Get a value from the dictionary and verify it's the expected type.""" | ||
| if (value := d.get(key)) is None: | ||
| return None | ||
| if not isinstance(value, expected_type): | ||
| raise PylockValidationError( | ||
| f"Unexpected type {type(value).__name__} " | ||
| f"(expected {expected_type.__name__})", | ||
| context=key, | ||
| ) | ||
| return value | ||
| def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T: | ||
| """Get a required value from the dictionary and verify it's the expected type.""" | ||
| if (value := _get(d, expected_type, key)) is None: | ||
| raise _PylockRequiredKeyError(key) | ||
| return value | ||
| def _get_sequence( | ||
| d: Mapping[str, Any], expected_item_type: type[_T], key: str | ||
| ) -> Sequence[_T] | None: | ||
| """Get a list value from the dictionary and verify it's the expected items type.""" | ||
| if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract] | ||
| return None | ||
| if isinstance(value, (str, bytes)): | ||
| # special case: str and bytes are Sequences, but we want to reject it | ||
| raise PylockValidationError( | ||
| f"Unexpected type {type(value).__name__} (expected Sequence)", | ||
| context=key, | ||
| ) | ||
| for i, item in enumerate(value): | ||
| if not isinstance(item, expected_item_type): | ||
| raise PylockValidationError( | ||
| f"Unexpected type {type(item).__name__} " | ||
| f"(expected {expected_item_type.__name__})", | ||
| context=f"{key}[{i}]", | ||
| ) | ||
| return value | ||
| def _get_as( | ||
| d: Mapping[str, Any], | ||
| expected_type: type[_T], | ||
| target_type: Callable[[_T], _T2], | ||
| key: str, | ||
| ) -> _T2 | None: | ||
| """Get a value from the dictionary, verify it's the expected type, | ||
| and convert to the target type. | ||
| This assumes the target_type constructor accepts the value. | ||
| """ | ||
| if (value := _get(d, expected_type, key)) is None: | ||
| return None | ||
| try: | ||
| return target_type(value) | ||
| except Exception as e: | ||
| raise PylockValidationError(e, context=key) from e | ||
| def _get_required_as( | ||
| d: Mapping[str, Any], | ||
| expected_type: type[_T], | ||
| target_type: Callable[[_T], _T2], | ||
| key: str, | ||
| ) -> _T2: | ||
| """Get a required value from the dict, verify it's the expected type, | ||
| and convert to the target type.""" | ||
| if (value := _get_as(d, expected_type, target_type, key)) is None: | ||
| raise _PylockRequiredKeyError(key) | ||
| return value | ||
| def _get_sequence_as( | ||
| d: Mapping[str, Any], | ||
| expected_item_type: type[_T], | ||
| target_item_type: Callable[[_T], _T2], | ||
| key: str, | ||
| ) -> list[_T2] | None: | ||
| """Get list value from dictionary and verify expected items type.""" | ||
| if (value := _get_sequence(d, expected_item_type, key)) is None: | ||
| return None | ||
| result = [] | ||
| try: | ||
| for item in value: | ||
| typed_item = target_item_type(item) | ||
| result.append(typed_item) | ||
| except Exception as e: | ||
| raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e | ||
| return result | ||
| def _get_object( | ||
| d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str | ||
| ) -> _FromMappingProtocolT | None: | ||
| """Get a dictionary value from the dictionary and convert it to a dataclass.""" | ||
| if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract] | ||
| return None | ||
| try: | ||
| return target_type._from_dict(value) | ||
| except Exception as e: | ||
| raise PylockValidationError(e, context=key) from e | ||
| def _get_sequence_of_objects( | ||
| d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str | ||
| ) -> list[_FromMappingProtocolT] | None: | ||
| """Get a list value from the dictionary and convert its items to a dataclass.""" | ||
| if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract] | ||
| return None | ||
| result: list[_FromMappingProtocolT] = [] | ||
| try: | ||
| for item in value: | ||
| typed_item = target_item_type._from_dict(item) | ||
| result.append(typed_item) | ||
| except Exception as e: | ||
| raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e | ||
| return result | ||
| def _get_required_sequence_of_objects( | ||
| d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str | ||
| ) -> Sequence[_FromMappingProtocolT]: | ||
| """Get a required list value from the dictionary and convert its items to a | ||
| dataclass.""" | ||
| if (result := _get_sequence_of_objects(d, target_item_type, key)) is None: | ||
| raise _PylockRequiredKeyError(key) | ||
| return result | ||
| def _validate_normalized_name(name: str) -> NormalizedName: | ||
| """Validate that a string is a NormalizedName.""" | ||
| if not is_normalized_name(name): | ||
| raise PylockValidationError(f"Name {name!r} is not normalized") | ||
| return NormalizedName(name) | ||
| def _validate_path_url(path: str | None, url: str | None) -> None: | ||
| if not path and not url: | ||
| raise PylockValidationError("path or url must be provided") | ||
| def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]: | ||
| if not hashes: | ||
| raise PylockValidationError("At least one hash must be provided") | ||
| if not all(isinstance(hash_val, str) for hash_val in hashes.values()): | ||
| raise PylockValidationError("Hash values must be strings") | ||
| return hashes | ||
| class PylockValidationError(Exception): | ||
| """Raised when when input data is not spec-compliant.""" | ||
| context: str | None = None | ||
| message: str | ||
| def __init__( | ||
| self, | ||
| cause: str | Exception, | ||
| *, | ||
| context: str | None = None, | ||
| ) -> None: | ||
| if isinstance(cause, PylockValidationError): | ||
| if cause.context: | ||
| self.context = ( | ||
| f"{context}.{cause.context}" if context else cause.context | ||
| ) | ||
| else: | ||
| self.context = context | ||
| self.message = cause.message | ||
| else: | ||
| self.context = context | ||
| self.message = str(cause) | ||
| def __str__(self) -> str: | ||
| if self.context: | ||
| return f"{self.message} in {self.context!r}" | ||
| return self.message | ||
| class _PylockRequiredKeyError(PylockValidationError): | ||
| def __init__(self, key: str) -> None: | ||
| super().__init__("Missing required value", context=key) | ||
| class PylockUnsupportedVersionError(PylockValidationError): | ||
| """Raised when encountering an unsupported `lock_version`.""" | ||
| @dataclass(frozen=True, init=False) | ||
| class PackageVcs: | ||
| type: str | ||
| url: str | None = None | ||
| path: str | None = None | ||
| requested_revision: str | None = None | ||
| commit_id: str # type: ignore[misc] | ||
| subdirectory: str | None = None | ||
| def __init__( | ||
| self, | ||
| *, | ||
| type: str, | ||
| url: str | None = None, | ||
| path: str | None = None, | ||
| requested_revision: str | None = None, | ||
| commit_id: str, | ||
| subdirectory: str | None = None, | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "type", type) | ||
| object.__setattr__(self, "url", url) | ||
| object.__setattr__(self, "path", path) | ||
| object.__setattr__(self, "requested_revision", requested_revision) | ||
| object.__setattr__(self, "commit_id", commit_id) | ||
| object.__setattr__(self, "subdirectory", subdirectory) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| package_vcs = cls( | ||
| type=_get_required(d, str, "type"), | ||
| url=_get(d, str, "url"), | ||
| path=_get(d, str, "path"), | ||
| requested_revision=_get(d, str, "requested-revision"), | ||
| commit_id=_get_required(d, str, "commit-id"), | ||
| subdirectory=_get(d, str, "subdirectory"), | ||
| ) | ||
| _validate_path_url(package_vcs.path, package_vcs.url) | ||
| return package_vcs | ||
| @dataclass(frozen=True, init=False) | ||
| class PackageDirectory: | ||
| path: str | ||
| editable: bool | None = None | ||
| subdirectory: str | None = None | ||
| def __init__( | ||
| self, | ||
| *, | ||
| path: str, | ||
| editable: bool | None = None, | ||
| subdirectory: str | None = None, | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "path", path) | ||
| object.__setattr__(self, "editable", editable) | ||
| object.__setattr__(self, "subdirectory", subdirectory) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| return cls( | ||
| path=_get_required(d, str, "path"), | ||
| editable=_get(d, bool, "editable"), | ||
| subdirectory=_get(d, str, "subdirectory"), | ||
| ) | ||
| @dataclass(frozen=True, init=False) | ||
| class PackageArchive: | ||
| url: str | None = None | ||
| path: str | None = None | ||
| size: int | None = None | ||
| upload_time: datetime | None = None | ||
| hashes: Mapping[str, str] # type: ignore[misc] | ||
| subdirectory: str | None = None | ||
| def __init__( | ||
| self, | ||
| *, | ||
| url: str | None = None, | ||
| path: str | None = None, | ||
| size: int | None = None, | ||
| upload_time: datetime | None = None, | ||
| hashes: Mapping[str, str], | ||
| subdirectory: str | None = None, | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "url", url) | ||
| object.__setattr__(self, "path", path) | ||
| object.__setattr__(self, "size", size) | ||
| object.__setattr__(self, "upload_time", upload_time) | ||
| object.__setattr__(self, "hashes", hashes) | ||
| object.__setattr__(self, "subdirectory", subdirectory) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| package_archive = cls( | ||
| url=_get(d, str, "url"), | ||
| path=_get(d, str, "path"), | ||
| size=_get(d, int, "size"), | ||
| upload_time=_get(d, datetime, "upload-time"), | ||
| hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] | ||
| subdirectory=_get(d, str, "subdirectory"), | ||
| ) | ||
| _validate_path_url(package_archive.path, package_archive.url) | ||
| return package_archive | ||
| @dataclass(frozen=True, init=False) | ||
| class PackageSdist: | ||
| name: str | None = None | ||
| upload_time: datetime | None = None | ||
| url: str | None = None | ||
| path: str | None = None | ||
| size: int | None = None | ||
| hashes: Mapping[str, str] # type: ignore[misc] | ||
| def __init__( | ||
| self, | ||
| *, | ||
| name: str | None = None, | ||
| upload_time: datetime | None = None, | ||
| url: str | None = None, | ||
| path: str | None = None, | ||
| size: int | None = None, | ||
| hashes: Mapping[str, str], | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "name", name) | ||
| object.__setattr__(self, "upload_time", upload_time) | ||
| object.__setattr__(self, "url", url) | ||
| object.__setattr__(self, "path", path) | ||
| object.__setattr__(self, "size", size) | ||
| object.__setattr__(self, "hashes", hashes) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| package_sdist = cls( | ||
| name=_get(d, str, "name"), | ||
| upload_time=_get(d, datetime, "upload-time"), | ||
| url=_get(d, str, "url"), | ||
| path=_get(d, str, "path"), | ||
| size=_get(d, int, "size"), | ||
| hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] | ||
| ) | ||
| _validate_path_url(package_sdist.path, package_sdist.url) | ||
| return package_sdist | ||
| @dataclass(frozen=True, init=False) | ||
| class PackageWheel: | ||
| name: str | None = None | ||
| upload_time: datetime | None = None | ||
| url: str | None = None | ||
| path: str | None = None | ||
| size: int | None = None | ||
| hashes: Mapping[str, str] # type: ignore[misc] | ||
| def __init__( | ||
| self, | ||
| *, | ||
| name: str | None = None, | ||
| upload_time: datetime | None = None, | ||
| url: str | None = None, | ||
| path: str | None = None, | ||
| size: int | None = None, | ||
| hashes: Mapping[str, str], | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "name", name) | ||
| object.__setattr__(self, "upload_time", upload_time) | ||
| object.__setattr__(self, "url", url) | ||
| object.__setattr__(self, "path", path) | ||
| object.__setattr__(self, "size", size) | ||
| object.__setattr__(self, "hashes", hashes) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| package_wheel = cls( | ||
| name=_get(d, str, "name"), | ||
| upload_time=_get(d, datetime, "upload-time"), | ||
| url=_get(d, str, "url"), | ||
| path=_get(d, str, "path"), | ||
| size=_get(d, int, "size"), | ||
| hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] | ||
| ) | ||
| _validate_path_url(package_wheel.path, package_wheel.url) | ||
| return package_wheel | ||
| @dataclass(frozen=True, init=False) | ||
| class Package: | ||
| name: NormalizedName | ||
| version: Version | None = None | ||
| marker: Marker | None = None | ||
| requires_python: SpecifierSet | None = None | ||
| dependencies: Sequence[Mapping[str, Any]] | None = None | ||
| vcs: PackageVcs | None = None | ||
| directory: PackageDirectory | None = None | ||
| archive: PackageArchive | None = None | ||
| index: str | None = None | ||
| sdist: PackageSdist | None = None | ||
| wheels: Sequence[PackageWheel] | None = None | ||
| attestation_identities: Sequence[Mapping[str, Any]] | None = None | ||
| tool: Mapping[str, Any] | None = None | ||
| def __init__( | ||
| self, | ||
| *, | ||
| name: NormalizedName, | ||
| version: Version | None = None, | ||
| marker: Marker | None = None, | ||
| requires_python: SpecifierSet | None = None, | ||
| dependencies: Sequence[Mapping[str, Any]] | None = None, | ||
| vcs: PackageVcs | None = None, | ||
| directory: PackageDirectory | None = None, | ||
| archive: PackageArchive | None = None, | ||
| index: str | None = None, | ||
| sdist: PackageSdist | None = None, | ||
| wheels: Sequence[PackageWheel] | None = None, | ||
| attestation_identities: Sequence[Mapping[str, Any]] | None = None, | ||
| tool: Mapping[str, Any] | None = None, | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "name", name) | ||
| object.__setattr__(self, "version", version) | ||
| object.__setattr__(self, "marker", marker) | ||
| object.__setattr__(self, "requires_python", requires_python) | ||
| object.__setattr__(self, "dependencies", dependencies) | ||
| object.__setattr__(self, "vcs", vcs) | ||
| object.__setattr__(self, "directory", directory) | ||
| object.__setattr__(self, "archive", archive) | ||
| object.__setattr__(self, "index", index) | ||
| object.__setattr__(self, "sdist", sdist) | ||
| object.__setattr__(self, "wheels", wheels) | ||
| object.__setattr__(self, "attestation_identities", attestation_identities) | ||
| object.__setattr__(self, "tool", tool) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| package = cls( | ||
| name=_get_required_as(d, str, _validate_normalized_name, "name"), | ||
| version=_get_as(d, str, Version, "version"), | ||
| requires_python=_get_as(d, str, SpecifierSet, "requires-python"), | ||
| dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract] | ||
| marker=_get_as(d, str, Marker, "marker"), | ||
| vcs=_get_object(d, PackageVcs, "vcs"), | ||
| directory=_get_object(d, PackageDirectory, "directory"), | ||
| archive=_get_object(d, PackageArchive, "archive"), | ||
| index=_get(d, str, "index"), | ||
| sdist=_get_object(d, PackageSdist, "sdist"), | ||
| wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"), | ||
| attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract] | ||
| tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract] | ||
| ) | ||
| distributions = bool(package.sdist) + len(package.wheels or []) | ||
| direct_urls = ( | ||
| bool(package.vcs) + bool(package.directory) + bool(package.archive) | ||
| ) | ||
| if distributions > 0 and direct_urls > 0: | ||
| raise PylockValidationError( | ||
| "None of vcs, directory, archive must be set if sdist or wheels are set" | ||
| ) | ||
| if distributions == 0 and direct_urls != 1: | ||
| raise PylockValidationError( | ||
| "Exactly one of vcs, directory, archive must be set " | ||
| "if sdist and wheels are not set" | ||
| ) | ||
| try: | ||
| for i, attestation_identity in enumerate( # noqa: B007 | ||
| package.attestation_identities or [] | ||
| ): | ||
| _get_required(attestation_identity, str, "kind") | ||
| except Exception as e: | ||
| raise PylockValidationError( | ||
| e, context=f"attestation-identities[{i}]" | ||
| ) from e | ||
| return package | ||
| @property | ||
| def is_direct(self) -> bool: | ||
| return not (self.sdist or self.wheels) | ||
| @dataclass(frozen=True, init=False) | ||
| class Pylock: | ||
| """A class representing a pylock file.""" | ||
| lock_version: Version | ||
| environments: Sequence[Marker] | None = None | ||
| requires_python: SpecifierSet | None = None | ||
| extras: Sequence[NormalizedName] | None = None | ||
| dependency_groups: Sequence[str] | None = None | ||
| default_groups: Sequence[str] | None = None | ||
| created_by: str # type: ignore[misc] | ||
| packages: Sequence[Package] # type: ignore[misc] | ||
| tool: Mapping[str, Any] | None = None | ||
| def __init__( | ||
| self, | ||
| *, | ||
| lock_version: Version, | ||
| environments: Sequence[Marker] | None = None, | ||
| requires_python: SpecifierSet | None = None, | ||
| extras: Sequence[NormalizedName] | None = None, | ||
| dependency_groups: Sequence[str] | None = None, | ||
| default_groups: Sequence[str] | None = None, | ||
| created_by: str, | ||
| packages: Sequence[Package], | ||
| tool: Mapping[str, Any] | None = None, | ||
| ) -> None: | ||
| # In Python 3.10+ make dataclass kw_only=True and remove __init__ | ||
| object.__setattr__(self, "lock_version", lock_version) | ||
| object.__setattr__(self, "environments", environments) | ||
| object.__setattr__(self, "requires_python", requires_python) | ||
| object.__setattr__(self, "extras", extras) | ||
| object.__setattr__(self, "dependency_groups", dependency_groups) | ||
| object.__setattr__(self, "default_groups", default_groups) | ||
| object.__setattr__(self, "created_by", created_by) | ||
| object.__setattr__(self, "packages", packages) | ||
| object.__setattr__(self, "tool", tool) | ||
| @classmethod | ||
| def _from_dict(cls, d: Mapping[str, Any]) -> Self: | ||
| pylock = cls( | ||
| lock_version=_get_required_as(d, str, Version, "lock-version"), | ||
| environments=_get_sequence_as(d, str, Marker, "environments"), | ||
| extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"), | ||
| dependency_groups=_get_sequence(d, str, "dependency-groups"), | ||
| default_groups=_get_sequence(d, str, "default-groups"), | ||
| created_by=_get_required(d, str, "created-by"), | ||
| requires_python=_get_as(d, str, SpecifierSet, "requires-python"), | ||
| packages=_get_required_sequence_of_objects(d, Package, "packages"), | ||
| tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract] | ||
| ) | ||
| if not Version("1") <= pylock.lock_version < Version("2"): | ||
| raise PylockUnsupportedVersionError( | ||
| f"pylock version {pylock.lock_version} is not supported" | ||
| ) | ||
| if pylock.lock_version > Version("1.0"): | ||
| _logger.warning( | ||
| "pylock minor version %s is not supported", pylock.lock_version | ||
| ) | ||
| return pylock | ||
| @classmethod | ||
| def from_dict(cls, d: Mapping[str, Any], /) -> Self: | ||
| """Create and validate a Pylock instance from a TOML dictionary. | ||
| Raises :class:`PylockValidationError` if the input data is not | ||
| spec-compliant. | ||
| """ | ||
| return cls._from_dict(d) | ||
| def to_dict(self) -> Mapping[str, Any]: | ||
| """Convert the Pylock instance to a TOML dictionary.""" | ||
| return dataclasses.asdict(self, dict_factory=_toml_dict_factory) | ||
| def validate(self) -> None: | ||
| """Validate the Pylock instance against the specification. | ||
| Raises :class:`PylockValidationError` otherwise.""" | ||
| self.from_dict(self.to_dict()) |
| [console_scripts] | ||
| wheel=wheel._commands:main | ||
| [distutils.commands] | ||
| bdist_wheel=wheel.bdist_wheel:bdist_wheel | ||
Sorry, the diff of this file is not supported yet
| MIT License | ||
| Copyright (c) 2012 Daniel Holth <dholth@fastmail.fm> and contributors | ||
| Permission is hereby granted, free of charge, to any person obtaining a | ||
| copy of this software and associated documentation files (the "Software"), | ||
| to deal in the Software without restriction, including without limitation | ||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||
| and/or sell copies of the Software, and to permit persons to whom the | ||
| Software is furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included | ||
| in all copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | ||
| THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR | ||
| OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | ||
| ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||
| OTHER DEALINGS IN THE SOFTWARE. |
| Metadata-Version: 2.4 | ||
| Name: wheel | ||
| Version: 0.46.3 | ||
| Summary: Command line tool for manipulating wheel files | ||
| Keywords: wheel,packaging | ||
| Author-email: Daniel Holth <dholth@fastmail.fm> | ||
| Maintainer-email: Alex Grönholm <alex.gronholm@nextday.fi> | ||
| Requires-Python: >=3.9 | ||
| Description-Content-Type: text/x-rst | ||
| License-Expression: MIT | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: Topic :: System :: Archiving :: Packaging | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| 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 :: 3.14 | ||
| License-File: LICENSE.txt | ||
| Requires-Dist: packaging >= 24.0 | ||
| Requires-Dist: pytest >= 6.0.0 ; extra == "test" | ||
| Requires-Dist: setuptools >= 77 ; extra == "test" | ||
| Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html | ||
| Project-URL: Documentation, https://wheel.readthedocs.io/ | ||
| Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues | ||
| Project-URL: Source, https://github.com/pypa/wheel | ||
| Provides-Extra: test | ||
| wheel | ||
| ===== | ||
| This is a command line tool for manipulating Python wheel files, as defined in | ||
| `PEP 427`_. It contains the following functionality: | ||
| * Convert ``.egg`` archives into ``.whl`` | ||
| * Unpack wheel archives | ||
| * Repack wheel archives | ||
| * Add or remove tags in existing wheel archives | ||
| .. _PEP 427: https://www.python.org/dev/peps/pep-0427/ | ||
| Historical note | ||
| --------------- | ||
| This project used to contain the implementation of the setuptools_ ``bdist_wheel`` | ||
| command, but as of setuptools v70.1, it no longer needs ``wheel`` installed for that to | ||
| work. Thus, you should install this **only** if you intend to use the ``wheel`` command | ||
| line tool! | ||
| .. _setuptools: https://pypi.org/project/setuptools/ | ||
| Documentation | ||
| ------------- | ||
| The documentation_ can be found on Read The Docs. | ||
| .. _documentation: https://wheel.readthedocs.io/ | ||
| Code of Conduct | ||
| --------------- | ||
| Everyone interacting in the wheel project's codebases, issue trackers, chat | ||
| rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. | ||
| .. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md | ||
| bin/wheel,sha256=OLjFs9G7MQ-f_mhuW2aLGpngsyphW_v5FIoyN88Hfk0,339 | ||
| wheel-0.46.3.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 | ||
| wheel-0.46.3.dist-info/METADATA,sha256=IpEKqXyonLzCCgGeJ_4xNgt5KaS9ZsoNMQ-ZpE9szTU,2410 | ||
| wheel-0.46.3.dist-info/RECORD,, | ||
| wheel-0.46.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| wheel-0.46.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 | ||
| wheel-0.46.3.dist-info/entry_points.txt,sha256=JJdtSAGTvMLbIkTVZUAMvGKO39FtWfCVF8mp_NH6e4g,110 | ||
| wheel-0.46.3.dist-info/licenses/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107 | ||
| wheel/__init__.py,sha256=UweKvhe4SyP7zFyDoYo8BOuwTA6q3-_WpMmY2NNO54c,59 | ||
| wheel/__main__.py,sha256=_83wl9tyGU2cHiqfudpubGHdRL5uonPXnzeznznkxzs,512 | ||
| wheel/_bdist_wheel.py,sha256=bpmNa7_s-CYFkVgXf9ENAYTiJ01XBhRW4pxH1T8XYsI,21729 | ||
| wheel/_commands/__init__.py,sha256=fCRAQZNDyj2JLrufdgPsBlaRS_t_j_aBUMpXj09KZ4E,4432 | ||
| wheel/_commands/convert.py,sha256=0wSJMU0m-6LY16Om8Wmmloy-hJWFZeOmI8hT-2Z7Qms,12743 | ||
| wheel/_commands/pack.py,sha256=o3iwjfRHl7N9ul-M2kHbewLJZnqBLAWf0tzUCwoiTMw,3078 | ||
| wheel/_commands/tags.py,sha256=Rv2ySVb8-qX3osKp3uJgxcIMXkjt43XUD0-zvC6KvnY,4775 | ||
| wheel/_commands/unpack.py,sha256=AjDSS23XYyCSFfifnMutinrpPv-DK_2wbNHkKAUFwgM,1016 | ||
| wheel/_metadata.py,sha256=BP5jC9uC1hyicp7nL4FJ2LYixNFpEJIV_uMDY1KBZBg,6188 | ||
| wheel/_setuptools_logging.py,sha256=-5KC-lne0ilOUWIDfOkqapUWGMFZhuKYDIavIZiB5kM,781 | ||
| wheel/bdist_wheel.py,sha256=HrzYiSzMkh5ohAAhlQnYBS1p8qbr85X6F59xqxd9kBg,1102 | ||
| wheel/macosx_libfile.py,sha256=pL0wm88jRMl_4ASgGlNg_mz69Zmv5xm8JSkjLdwyvIQ,16712 | ||
| wheel/metadata.py,sha256=GknOO7JJiZMlcEe_fiD7nqnDTTLd0sX_-IgipM4L3-4,757 | ||
| wheel/wheelfile.py,sha256=m_g_7TNsEp-j-xnvSr5yDLEFb1nhyObueq9Q5_1_lBA,8720 |
Sorry, the diff of this file is not supported yet
| Wheel-Version: 1.0 | ||
| Generator: flit 3.12.0 | ||
| Root-Is-Purelib: true | ||
| Tag: py3-none-any |
| """ | ||
| Wheel command-line utility. | ||
| """ | ||
| from __future__ import annotations | ||
| import argparse | ||
| import os | ||
| import sys | ||
| from argparse import ArgumentTypeError | ||
| from ..wheelfile import WheelError | ||
| def unpack_f(args: argparse.Namespace) -> None: | ||
| from .unpack import unpack | ||
| unpack(args.wheelfile, args.dest) | ||
| def pack_f(args: argparse.Namespace) -> None: | ||
| from .pack import pack | ||
| pack(args.directory, args.dest_dir, args.build_number) | ||
| def convert_f(args: argparse.Namespace) -> None: | ||
| from .convert import convert | ||
| convert(args.files, args.dest_dir, args.verbose) | ||
| def tags_f(args: argparse.Namespace) -> None: | ||
| from .tags import tags | ||
| names = ( | ||
| tags( | ||
| wheel, | ||
| args.python_tag, | ||
| args.abi_tag, | ||
| args.platform_tag, | ||
| args.build, | ||
| args.remove, | ||
| ) | ||
| for wheel in args.wheel | ||
| ) | ||
| for name in names: | ||
| print(name) | ||
| def version_f(args: argparse.Namespace) -> None: | ||
| from .. import __version__ | ||
| print(f"wheel {__version__}") | ||
| def parse_build_tag(build_tag: str) -> str: | ||
| if build_tag and not build_tag[0].isdigit(): | ||
| raise ArgumentTypeError("build tag must begin with a digit") | ||
| elif "-" in build_tag: | ||
| raise ArgumentTypeError("invalid character ('-') in build tag") | ||
| return build_tag | ||
| TAGS_HELP = """\ | ||
| Make a new wheel with given tags. Any tags unspecified will remain the same. | ||
| Starting the tags with a "+" will append to the existing tags. Starting with a | ||
| "-" will remove a tag (use --option=-TAG syntax). Multiple tags can be | ||
| separated by ".". The original file will remain unless --remove is given. The | ||
| output filename(s) will be displayed on stdout for further processing. | ||
| """ | ||
| def parser() -> argparse.ArgumentParser: | ||
| p = argparse.ArgumentParser() | ||
| s = p.add_subparsers(help="commands") | ||
| unpack_parser = s.add_parser("unpack", help="Unpack wheel") | ||
| unpack_parser.add_argument( | ||
| "--dest", "-d", help="Destination directory", default="." | ||
| ) | ||
| unpack_parser.add_argument("wheelfile", help="Wheel file") | ||
| unpack_parser.set_defaults(func=unpack_f) | ||
| repack_parser = s.add_parser("pack", help="Repack wheel") | ||
| repack_parser.add_argument("directory", help="Root directory of the unpacked wheel") | ||
| repack_parser.add_argument( | ||
| "--dest-dir", | ||
| "-d", | ||
| default=os.path.curdir, | ||
| help="Directory to store the wheel (default %(default)s)", | ||
| ) | ||
| repack_parser.add_argument( | ||
| "--build-number", help="Build tag to use in the wheel name" | ||
| ) | ||
| repack_parser.set_defaults(func=pack_f) | ||
| convert_parser = s.add_parser("convert", help="Convert egg or wininst to wheel") | ||
| convert_parser.add_argument("files", nargs="*", help="Files to convert") | ||
| convert_parser.add_argument( | ||
| "--dest-dir", | ||
| "-d", | ||
| default=os.path.curdir, | ||
| help="Directory to store wheels (default %(default)s)", | ||
| ) | ||
| convert_parser.add_argument("--verbose", "-v", action="store_true") | ||
| convert_parser.set_defaults(func=convert_f) | ||
| tags_parser = s.add_parser( | ||
| "tags", help="Add or replace the tags on a wheel", description=TAGS_HELP | ||
| ) | ||
| tags_parser.add_argument("wheel", nargs="*", help="Existing wheel(s) to retag") | ||
| tags_parser.add_argument( | ||
| "--remove", | ||
| action="store_true", | ||
| help="Remove the original files, keeping only the renamed ones", | ||
| ) | ||
| tags_parser.add_argument( | ||
| "--python-tag", metavar="TAG", help="Specify an interpreter tag(s)" | ||
| ) | ||
| tags_parser.add_argument("--abi-tag", metavar="TAG", help="Specify an ABI tag(s)") | ||
| tags_parser.add_argument( | ||
| "--platform-tag", metavar="TAG", help="Specify a platform tag(s)" | ||
| ) | ||
| tags_parser.add_argument( | ||
| "--build", type=parse_build_tag, metavar="BUILD", help="Specify a build tag" | ||
| ) | ||
| tags_parser.set_defaults(func=tags_f) | ||
| version_parser = s.add_parser("version", help="Print version and exit") | ||
| version_parser.set_defaults(func=version_f) | ||
| help_parser = s.add_parser("help", help="Show this help") | ||
| help_parser.set_defaults(func=lambda args: p.print_help()) | ||
| return p | ||
| def main() -> int: | ||
| p = parser() | ||
| args = p.parse_args() | ||
| if not hasattr(args, "func"): | ||
| p.print_help() | ||
| else: | ||
| try: | ||
| args.func(args) | ||
| return 0 | ||
| except WheelError as e: | ||
| print(e, file=sys.stderr) | ||
| return 1 |
| from __future__ import annotations | ||
| import os.path | ||
| import re | ||
| from abc import ABCMeta, abstractmethod | ||
| from collections import defaultdict | ||
| from collections.abc import Iterator | ||
| from email.message import Message | ||
| from email.parser import Parser | ||
| from email.policy import EmailPolicy | ||
| from glob import iglob | ||
| from pathlib import Path | ||
| from textwrap import dedent | ||
| from zipfile import ZipFile | ||
| from packaging.tags import parse_tag | ||
| from .. import __version__ | ||
| from .._metadata import generate_requirements | ||
| from ..wheelfile import WheelFile | ||
| egg_filename_re = re.compile( | ||
| r""" | ||
| (?P<name>.+?)-(?P<ver>.+?) | ||
| (-(?P<pyver>py\d\.\d+) | ||
| (-(?P<arch>.+?))? | ||
| )?.egg$""", | ||
| re.VERBOSE, | ||
| ) | ||
| egg_info_re = re.compile( | ||
| r""" | ||
| ^(?P<name>.+?)-(?P<ver>.+?) | ||
| (-(?P<pyver>py\d\.\d+) | ||
| )?.egg-info/""", | ||
| re.VERBOSE, | ||
| ) | ||
| wininst_re = re.compile( | ||
| r"\.(?P<platform>win32|win-amd64)(?:-(?P<pyver>py\d\.\d))?\.exe$" | ||
| ) | ||
| pyd_re = re.compile(r"\.(?P<abi>[a-z0-9]+)-(?P<platform>win32|win_amd64)\.pyd$") | ||
| serialization_policy = EmailPolicy( | ||
| utf8=True, | ||
| mangle_from_=False, | ||
| max_line_length=0, | ||
| ) | ||
| GENERATOR = f"wheel {__version__}" | ||
| def convert_requires(requires: str, metadata: Message) -> None: | ||
| extra: str | None = None | ||
| requirements: dict[str | None, list[str]] = defaultdict(list) | ||
| for line in requires.splitlines(): | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| if line.startswith("[") and line.endswith("]"): | ||
| extra = line[1:-1] | ||
| continue | ||
| requirements[extra].append(line) | ||
| for key, value in generate_requirements(requirements): | ||
| metadata.add_header(key, value) | ||
| def convert_pkg_info(pkginfo: str, metadata: Message) -> None: | ||
| parsed_message = Parser().parsestr(pkginfo) | ||
| for key, value in parsed_message.items(): | ||
| key_lower = key.lower() | ||
| if value == "UNKNOWN": | ||
| continue | ||
| if key_lower == "description": | ||
| description_lines = value.splitlines() | ||
| if description_lines: | ||
| value = "\n".join( | ||
| ( | ||
| description_lines[0].lstrip(), | ||
| dedent("\n".join(description_lines[1:])), | ||
| "\n", | ||
| ) | ||
| ) | ||
| else: | ||
| value = "\n" | ||
| metadata.set_payload(value) | ||
| elif key_lower == "home-page": | ||
| metadata.add_header("Project-URL", f"Homepage, {value}") | ||
| elif key_lower == "download-url": | ||
| metadata.add_header("Project-URL", f"Download, {value}") | ||
| else: | ||
| metadata.add_header(key, value) | ||
| metadata.replace_header("Metadata-Version", "2.4") | ||
| def normalize(name: str) -> str: | ||
| return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_") | ||
| class ConvertSource(metaclass=ABCMeta): | ||
| name: str | ||
| version: str | ||
| pyver: str = "py2.py3" | ||
| abi: str = "none" | ||
| platform: str = "any" | ||
| metadata: Message | ||
| @property | ||
| def dist_info_dir(self) -> str: | ||
| return f"{self.name}-{self.version}.dist-info" | ||
| @abstractmethod | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| pass | ||
| class EggFileSource(ConvertSource): | ||
| def __init__(self, path: Path): | ||
| if not (match := egg_filename_re.match(path.name)): | ||
| raise ValueError(f"Invalid egg file name: {path.name}") | ||
| # Binary wheels are assumed to be for CPython | ||
| self.path = path | ||
| self.name = normalize(match.group("name")) | ||
| self.version = match.group("ver") | ||
| if pyver := match.group("pyver"): | ||
| self.pyver = pyver.replace(".", "") | ||
| if arch := match.group("arch"): | ||
| self.abi = self.pyver.replace("py", "cp") | ||
| self.platform = normalize(arch) | ||
| self.metadata = Message() | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| with ZipFile(self.path, "r") as zip_file: | ||
| for filename in sorted(zip_file.namelist()): | ||
| # Skip pure directory entries | ||
| if filename.endswith("/"): | ||
| continue | ||
| # Handle files in the egg-info directory specially, selectively moving | ||
| # them to the dist-info directory while converting as needed | ||
| if filename.startswith("EGG-INFO/"): | ||
| if filename == "EGG-INFO/requires.txt": | ||
| requires = zip_file.read(filename).decode("utf-8") | ||
| convert_requires(requires, self.metadata) | ||
| elif filename == "EGG-INFO/PKG-INFO": | ||
| pkginfo = zip_file.read(filename).decode("utf-8") | ||
| convert_pkg_info(pkginfo, self.metadata) | ||
| elif filename == "EGG-INFO/entry_points.txt": | ||
| yield ( | ||
| f"{self.dist_info_dir}/entry_points.txt", | ||
| zip_file.read(filename), | ||
| ) | ||
| continue | ||
| # For any other file, just pass it through | ||
| yield filename, zip_file.read(filename) | ||
| class EggDirectorySource(EggFileSource): | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| for dirpath, _, filenames in os.walk(self.path): | ||
| for filename in sorted(filenames): | ||
| path = Path(dirpath, filename) | ||
| if path.parent.name == "EGG-INFO": | ||
| if path.name == "requires.txt": | ||
| requires = path.read_text("utf-8") | ||
| convert_requires(requires, self.metadata) | ||
| elif path.name == "PKG-INFO": | ||
| pkginfo = path.read_text("utf-8") | ||
| convert_pkg_info(pkginfo, self.metadata) | ||
| if name := self.metadata.get("Name"): | ||
| self.name = normalize(name) | ||
| if version := self.metadata.get("Version"): | ||
| self.version = version | ||
| elif path.name == "entry_points.txt": | ||
| yield ( | ||
| f"{self.dist_info_dir}/entry_points.txt", | ||
| path.read_bytes(), | ||
| ) | ||
| continue | ||
| # For any other file, just pass it through | ||
| yield str(path.relative_to(self.path)), path.read_bytes() | ||
| class WininstFileSource(ConvertSource): | ||
| """ | ||
| Handles distributions created with ``bdist_wininst``. | ||
| The egginfo filename has the format:: | ||
| name-ver(-pyver)(-arch).egg-info | ||
| The installer filename has the format:: | ||
| name-ver.arch(-pyver).exe | ||
| Some things to note: | ||
| 1. The installer filename is not definitive. An installer can be renamed | ||
| and work perfectly well as an installer. So more reliable data should | ||
| be used whenever possible. | ||
| 2. The egg-info data should be preferred for the name and version, because | ||
| these come straight from the distutils metadata, and are mandatory. | ||
| 3. The pyver from the egg-info data should be ignored, as it is | ||
| constructed from the version of Python used to build the installer, | ||
| which is irrelevant - the installer filename is correct here (even to | ||
| the point that when it's not there, any version is implied). | ||
| 4. The architecture must be taken from the installer filename, as it is | ||
| not included in the egg-info data. | ||
| 5. Architecture-neutral installers still have an architecture because the | ||
| installer format itself (being executable) is architecture-specific. We | ||
| should therefore ignore the architecture if the content is pure-python. | ||
| """ | ||
| def __init__(self, path: Path): | ||
| self.path = path | ||
| self.metadata = Message() | ||
| # Determine the initial architecture and Python version from the file name | ||
| # (if possible) | ||
| if match := wininst_re.search(path.name): | ||
| self.platform = normalize(match.group("platform")) | ||
| if pyver := match.group("pyver"): | ||
| self.pyver = pyver.replace(".", "") | ||
| # Look for an .egg-info directory and any .pyd files for more precise info | ||
| egg_info_found = pyd_found = False | ||
| with ZipFile(self.path) as zip_file: | ||
| for filename in zip_file.namelist(): | ||
| prefix, filename = filename.split("/", 1) | ||
| if not egg_info_found and (match := egg_info_re.match(filename)): | ||
| egg_info_found = True | ||
| self.name = normalize(match.group("name")) | ||
| self.version = match.group("ver") | ||
| if pyver := match.group("pyver"): | ||
| self.pyver = pyver.replace(".", "") | ||
| elif not pyd_found and (match := pyd_re.search(filename)): | ||
| pyd_found = True | ||
| self.abi = match.group("abi") | ||
| self.platform = match.group("platform") | ||
| if egg_info_found and pyd_found: | ||
| break | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| dist_info_dir = f"{self.name}-{self.version}.dist-info" | ||
| data_dir = f"{self.name}-{self.version}.data" | ||
| with ZipFile(self.path, "r") as zip_file: | ||
| for filename in sorted(zip_file.namelist()): | ||
| # Skip pure directory entries | ||
| if filename.endswith("/"): | ||
| continue | ||
| # Handle files in the egg-info directory specially, selectively moving | ||
| # them to the dist-info directory while converting as needed | ||
| prefix, target_filename = filename.split("/", 1) | ||
| if egg_info_re.search(target_filename): | ||
| basename = target_filename.rsplit("/", 1)[-1] | ||
| if basename == "requires.txt": | ||
| requires = zip_file.read(filename).decode("utf-8") | ||
| convert_requires(requires, self.metadata) | ||
| elif basename == "PKG-INFO": | ||
| pkginfo = zip_file.read(filename).decode("utf-8") | ||
| convert_pkg_info(pkginfo, self.metadata) | ||
| elif basename == "entry_points.txt": | ||
| yield ( | ||
| f"{dist_info_dir}/entry_points.txt", | ||
| zip_file.read(filename), | ||
| ) | ||
| continue | ||
| elif prefix == "SCRIPTS": | ||
| target_filename = f"{data_dir}/scripts/{target_filename}" | ||
| # For any other file, just pass it through | ||
| yield target_filename, zip_file.read(filename) | ||
| def convert(files: list[str], dest_dir: str, verbose: bool) -> None: | ||
| for pat in files: | ||
| for archive in iglob(pat): | ||
| path = Path(archive) | ||
| if path.suffix == ".egg": | ||
| if path.is_dir(): | ||
| source: ConvertSource = EggDirectorySource(path) | ||
| else: | ||
| source = EggFileSource(path) | ||
| else: | ||
| source = WininstFileSource(path) | ||
| if verbose: | ||
| print(f"{archive}...", flush=True, end="") | ||
| dest_path = Path(dest_dir) / ( | ||
| f"{source.name}-{source.version}-{source.pyver}-{source.abi}" | ||
| f"-{source.platform}.whl" | ||
| ) | ||
| with WheelFile(dest_path, "w") as wheelfile: | ||
| for name_or_zinfo, contents in source.generate_contents(): | ||
| wheelfile.writestr(name_or_zinfo, contents) | ||
| # Write the METADATA file | ||
| wheelfile.writestr( | ||
| f"{source.dist_info_dir}/METADATA", | ||
| source.metadata.as_string(policy=serialization_policy).encode( | ||
| "utf-8" | ||
| ), | ||
| ) | ||
| # Write the WHEEL file | ||
| wheel_message = Message() | ||
| wheel_message.add_header("Wheel-Version", "1.0") | ||
| wheel_message.add_header("Generator", GENERATOR) | ||
| wheel_message.add_header( | ||
| "Root-Is-Purelib", str(source.platform == "any").lower() | ||
| ) | ||
| tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}") | ||
| for tag in sorted(tags, key=lambda tag: tag.interpreter): | ||
| wheel_message.add_header("Tag", str(tag)) | ||
| wheelfile.writestr( | ||
| f"{source.dist_info_dir}/WHEEL", | ||
| wheel_message.as_string(policy=serialization_policy).encode( | ||
| "utf-8" | ||
| ), | ||
| ) | ||
| if verbose: | ||
| print("OK") |
| from __future__ import annotations | ||
| import email.policy | ||
| import os.path | ||
| import re | ||
| from email.generator import BytesGenerator | ||
| from email.parser import BytesParser | ||
| from ..wheelfile import WheelError, WheelFile | ||
| DIST_INFO_RE = re.compile(r"^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))\.dist-info$") | ||
| def pack(directory: str, dest_dir: str, build_number: str | None) -> None: | ||
| """Repack a previously unpacked wheel directory into a new wheel file. | ||
| The .dist-info/WHEEL file must contain one or more tags so that the target | ||
| wheel file name can be determined. | ||
| :param directory: The unpacked wheel directory | ||
| :param dest_dir: Destination directory (defaults to the current directory) | ||
| """ | ||
| # Find the .dist-info directory | ||
| dist_info_dirs = [ | ||
| fn | ||
| for fn in os.listdir(directory) | ||
| if os.path.isdir(os.path.join(directory, fn)) and DIST_INFO_RE.match(fn) | ||
| ] | ||
| if len(dist_info_dirs) > 1: | ||
| raise WheelError(f"Multiple .dist-info directories found in {directory}") | ||
| elif not dist_info_dirs: | ||
| raise WheelError(f"No .dist-info directories found in {directory}") | ||
| # Determine the target wheel filename | ||
| dist_info_dir = dist_info_dirs[0] | ||
| name_version = DIST_INFO_RE.match(dist_info_dir).group("namever") | ||
| # Read the tags and the existing build number from .dist-info/WHEEL | ||
| wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL") | ||
| with open(wheel_file_path, "rb") as f: | ||
| info = BytesParser(policy=email.policy.compat32).parse(f) | ||
| tags: list[str] = info.get_all("Tag", []) | ||
| existing_build_number = info.get("Build") | ||
| if not tags: | ||
| raise WheelError( | ||
| f"No tags present in {dist_info_dir}/WHEEL; cannot determine target " | ||
| f"wheel filename" | ||
| ) | ||
| # Set the wheel file name and add/replace/remove the Build tag in .dist-info/WHEEL | ||
| build_number = build_number if build_number is not None else existing_build_number | ||
| if build_number is not None: | ||
| del info["Build"] | ||
| if build_number: | ||
| info["Build"] = build_number | ||
| name_version += "-" + build_number | ||
| if build_number != existing_build_number: | ||
| with open(wheel_file_path, "wb") as f: | ||
| BytesGenerator(f, maxheaderlen=0).flatten(info) | ||
| # Reassemble the tags for the wheel file | ||
| tagline = compute_tagline(tags) | ||
| # Repack the wheel | ||
| wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl") | ||
| with WheelFile(wheel_path, "w") as wf: | ||
| print(f"Repacking wheel as {wheel_path}...", end="", flush=True) | ||
| wf.write_files(directory) | ||
| print("OK") | ||
| def compute_tagline(tags: list[str]) -> str: | ||
| """Compute a tagline from a list of tags. | ||
| :param tags: A list of tags | ||
| :return: A tagline | ||
| """ | ||
| impls = sorted({tag.split("-")[0] for tag in tags}) | ||
| abivers = sorted({tag.split("-")[1] for tag in tags}) | ||
| platforms = sorted({tag.split("-")[2] for tag in tags}) | ||
| return "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)]) |
| from __future__ import annotations | ||
| import email.policy | ||
| import itertools | ||
| import os | ||
| from collections.abc import Iterable | ||
| from email.parser import BytesParser | ||
| from ..wheelfile import WheelFile | ||
| def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str]: | ||
| """Add or replace tags. Supports dot-separated tags""" | ||
| if new_tags is None: | ||
| return set(original_tags) | ||
| if new_tags.startswith("+"): | ||
| return {*original_tags, *new_tags[1:].split(".")} | ||
| if new_tags.startswith("-"): | ||
| return set(original_tags) - set(new_tags[1:].split(".")) | ||
| return set(new_tags.split(".")) | ||
| def tags( | ||
| wheel: str, | ||
| python_tags: str | None = None, | ||
| abi_tags: str | None = None, | ||
| platform_tags: str | None = None, | ||
| build_tag: str | None = None, | ||
| remove: bool = False, | ||
| ) -> str: | ||
| """Change the tags on a wheel file. | ||
| The tags are left unchanged if they are not specified. To specify "none", | ||
| use ["none"]. To append to the previous tags, a tag should start with a | ||
| "+". If a tag starts with "-", it will be removed from existing tags. | ||
| Processing is done left to right. | ||
| :param wheel: The paths to the wheels | ||
| :param python_tags: The Python tags to set | ||
| :param abi_tags: The ABI tags to set | ||
| :param platform_tags: The platform tags to set | ||
| :param build_tag: The build tag to set | ||
| :param remove: Remove the original wheel | ||
| """ | ||
| with WheelFile(wheel, "r") as f: | ||
| assert f.filename, f"{f.filename} must be available" | ||
| wheel_info = f.read(f.dist_info_path + "/WHEEL") | ||
| info = BytesParser(policy=email.policy.compat32).parsebytes(wheel_info) | ||
| original_wheel_name = os.path.basename(f.filename) | ||
| namever = f.parsed_filename.group("namever") | ||
| build = f.parsed_filename.group("build") | ||
| original_python_tags = f.parsed_filename.group("pyver").split(".") | ||
| original_abi_tags = f.parsed_filename.group("abi").split(".") | ||
| original_plat_tags = f.parsed_filename.group("plat").split(".") | ||
| tags: list[str] = info.get_all("Tag", []) | ||
| existing_build_tag = info.get("Build") | ||
| impls = {tag.split("-")[0] for tag in tags} | ||
| abivers = {tag.split("-")[1] for tag in tags} | ||
| platforms = {tag.split("-")[2] for tag in tags} | ||
| if impls != set(original_python_tags): | ||
| msg = f"Wheel internal tags {impls!r} != filename tags {original_python_tags!r}" | ||
| raise AssertionError(msg) | ||
| if abivers != set(original_abi_tags): | ||
| msg = f"Wheel internal tags {abivers!r} != filename tags {original_abi_tags!r}" | ||
| raise AssertionError(msg) | ||
| if platforms != set(original_plat_tags): | ||
| msg = ( | ||
| f"Wheel internal tags {platforms!r} != filename tags {original_plat_tags!r}" | ||
| ) | ||
| raise AssertionError(msg) | ||
| if existing_build_tag != build: | ||
| msg = ( | ||
| f"Incorrect filename '{build}' " | ||
| f"& *.dist-info/WHEEL '{existing_build_tag}' build numbers" | ||
| ) | ||
| raise AssertionError(msg) | ||
| # Start changing as needed | ||
| if build_tag is not None: | ||
| build = build_tag | ||
| final_python_tags = sorted(_compute_tags(original_python_tags, python_tags)) | ||
| final_abi_tags = sorted(_compute_tags(original_abi_tags, abi_tags)) | ||
| final_plat_tags = sorted(_compute_tags(original_plat_tags, platform_tags)) | ||
| final_tags = [ | ||
| namever, | ||
| ".".join(final_python_tags), | ||
| ".".join(final_abi_tags), | ||
| ".".join(final_plat_tags), | ||
| ] | ||
| if build: | ||
| final_tags.insert(1, build) | ||
| final_wheel_name = "-".join(final_tags) + ".whl" | ||
| if original_wheel_name != final_wheel_name: | ||
| del info["Tag"], info["Build"] | ||
| for a, b, c in itertools.product( | ||
| final_python_tags, final_abi_tags, final_plat_tags | ||
| ): | ||
| info["Tag"] = f"{a}-{b}-{c}" | ||
| if build: | ||
| info["Build"] = build | ||
| original_wheel_path = os.path.join( | ||
| os.path.dirname(f.filename), original_wheel_name | ||
| ) | ||
| final_wheel_path = os.path.join(os.path.dirname(f.filename), final_wheel_name) | ||
| with ( | ||
| WheelFile(original_wheel_path, "r") as fin, | ||
| WheelFile(final_wheel_path, "w") as fout, | ||
| ): | ||
| fout.comment = fin.comment # preserve the comment | ||
| for item in fin.infolist(): | ||
| if item.is_dir(): | ||
| continue | ||
| if item.filename == f.dist_info_path + "/RECORD": | ||
| continue | ||
| if item.filename == f.dist_info_path + "/WHEEL": | ||
| fout.writestr(item, info.as_bytes()) | ||
| else: | ||
| fout.writestr(item, fin.read(item)) | ||
| if remove: | ||
| os.remove(original_wheel_path) | ||
| return final_wheel_name |
| from __future__ import annotations | ||
| from pathlib import Path | ||
| from ..wheelfile import WheelFile | ||
| def unpack(path: str, dest: str = ".") -> None: | ||
| """Unpack a wheel. | ||
| Wheel content will be unpacked to {dest}/{name}-{ver}, where {name} | ||
| is the package name and {ver} its version. | ||
| :param path: The path to the wheel. | ||
| :param dest: Destination directory (default to current directory). | ||
| """ | ||
| with WheelFile(path) as wf: | ||
| namever = wf.parsed_filename.group("namever") | ||
| destination = Path(dest) / namever | ||
| print(f"Unpacking to: {destination}...", end="", flush=True) | ||
| for zinfo in wf.filelist: | ||
| target_path = Path(wf.extract(zinfo, destination)) | ||
| # Set permissions to the same values as they were set in the archive | ||
| # We have to do this manually due to | ||
| # https://github.com/python/cpython/issues/59999 | ||
| permissions = zinfo.external_attr >> 16 & 0o777 | ||
| target_path.chmod(permissions) | ||
| print("OK") |
| """ | ||
| Tools for converting old- to new-style metadata. | ||
| """ | ||
| from __future__ import annotations | ||
| import functools | ||
| import itertools | ||
| import os.path | ||
| import re | ||
| import textwrap | ||
| from collections.abc import Generator, Iterable, Iterator | ||
| from email.message import Message | ||
| from email.parser import Parser | ||
| from typing import Literal | ||
| from packaging.requirements import Requirement | ||
| def _nonblank(str: str) -> bool | Literal[""]: | ||
| return str and not str.startswith("#") | ||
| @functools.singledispatch | ||
| def yield_lines(iterable: Iterable[str]) -> Iterator[str]: | ||
| r""" | ||
| Yield valid lines of a string or iterable. | ||
| >>> list(yield_lines('')) | ||
| [] | ||
| >>> list(yield_lines(['foo', 'bar'])) | ||
| ['foo', 'bar'] | ||
| >>> list(yield_lines('foo\nbar')) | ||
| ['foo', 'bar'] | ||
| >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) | ||
| ['foo', 'baz #comment'] | ||
| >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) | ||
| ['foo', 'bar', 'baz', 'bing'] | ||
| """ | ||
| return itertools.chain.from_iterable(map(yield_lines, iterable)) | ||
| @yield_lines.register(str) | ||
| def _(text: str) -> Iterator[str]: | ||
| return filter(_nonblank, map(str.strip, text.splitlines())) | ||
| def split_sections( | ||
| s: str | Iterator[str], | ||
| ) -> Generator[tuple[str | None, list[str]], None, None]: | ||
| """Split a string or iterable thereof into (section, content) pairs | ||
| Each ``section`` is a stripped version of the section header ("[section]") | ||
| and each ``content`` is a list of stripped lines excluding blank lines and | ||
| comment-only lines. If there are any such lines before the first section | ||
| header, they're returned in a first ``section`` of ``None``. | ||
| """ | ||
| section = None | ||
| content: list[str] = [] | ||
| for line in yield_lines(s): | ||
| if line.startswith("["): | ||
| if line.endswith("]"): | ||
| if section or content: | ||
| yield section, content | ||
| section = line[1:-1].strip() | ||
| content = [] | ||
| else: | ||
| raise ValueError("Invalid section heading", line) | ||
| else: | ||
| content.append(line) | ||
| # wrap up last segment | ||
| yield section, content | ||
| def safe_extra(extra: str) -> str: | ||
| """Convert an arbitrary string to a standard 'extra' name | ||
| Any runs of non-alphanumeric characters are replaced with a single '_', | ||
| and the result is always lowercased. | ||
| """ | ||
| return re.sub("[^A-Za-z0-9.-]+", "_", extra).lower() | ||
| def safe_name(name: str) -> str: | ||
| """Convert an arbitrary string to a standard distribution name | ||
| Any runs of non-alphanumeric/. characters are replaced with a single '-'. | ||
| """ | ||
| return re.sub("[^A-Za-z0-9.]+", "-", name) | ||
| def requires_to_requires_dist(requirement: Requirement) -> str: | ||
| """Return the version specifier for a requirement in PEP 345/566 fashion.""" | ||
| if requirement.url: | ||
| return " @ " + requirement.url | ||
| requires_dist: list[str] = [] | ||
| for spec in requirement.specifier: | ||
| requires_dist.append(spec.operator + spec.version) | ||
| if requires_dist: | ||
| return " " + ",".join(sorted(requires_dist)) | ||
| else: | ||
| return "" | ||
| def convert_requirements(requirements: list[str]) -> Iterator[str]: | ||
| """Yield Requires-Dist: strings for parsed requirements strings.""" | ||
| for req in requirements: | ||
| parsed_requirement = Requirement(req) | ||
| spec = requires_to_requires_dist(parsed_requirement) | ||
| extras = ",".join(sorted(safe_extra(e) for e in parsed_requirement.extras)) | ||
| if extras: | ||
| extras = f"[{extras}]" | ||
| yield safe_name(parsed_requirement.name) + extras + spec | ||
| def generate_requirements( | ||
| extras_require: dict[str | None, list[str]], | ||
| ) -> Iterator[tuple[str, str]]: | ||
| """ | ||
| Convert requirements from a setup()-style dictionary to | ||
| ('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples. | ||
| extras_require is a dictionary of {extra: [requirements]} as passed to setup(), | ||
| using the empty extra {'': [requirements]} to hold install_requires. | ||
| """ | ||
| for extra, depends in extras_require.items(): | ||
| condition = "" | ||
| extra = extra or "" | ||
| if ":" in extra: # setuptools extra:condition syntax | ||
| extra, condition = extra.split(":", 1) | ||
| extra = safe_extra(extra) | ||
| if extra: | ||
| yield "Provides-Extra", extra | ||
| if condition: | ||
| condition = "(" + condition + ") and " | ||
| condition += f"extra == '{extra}'" | ||
| if condition: | ||
| condition = " ; " + condition | ||
| for new_req in convert_requirements(depends): | ||
| canonical_req = str(Requirement(new_req + condition)) | ||
| yield "Requires-Dist", canonical_req | ||
| def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message: | ||
| """ | ||
| Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format | ||
| """ | ||
| with open(pkginfo_path, encoding="utf-8") as headers: | ||
| pkg_info = Parser().parse(headers) | ||
| pkg_info.replace_header("Metadata-Version", "2.1") | ||
| # Those will be regenerated from `requires.txt`. | ||
| del pkg_info["Provides-Extra"] | ||
| del pkg_info["Requires-Dist"] | ||
| requires_path = os.path.join(egg_info_path, "requires.txt") | ||
| if os.path.exists(requires_path): | ||
| with open(requires_path, encoding="utf-8") as requires_file: | ||
| requires = requires_file.read() | ||
| parsed_requirements = sorted(split_sections(requires), key=lambda x: x[0] or "") | ||
| for extra, reqs in parsed_requirements: | ||
| for key, value in generate_requirements({extra: reqs}): | ||
| if (key, value) not in pkg_info.items(): | ||
| pkg_info[key] = value | ||
| description = pkg_info["Description"] | ||
| if description: | ||
| description_lines = pkg_info["Description"].splitlines() | ||
| dedented_description = "\n".join( | ||
| # if the first line of long_description is blank, | ||
| # the first line here will be indented. | ||
| ( | ||
| description_lines[0].lstrip(), | ||
| textwrap.dedent("\n".join(description_lines[1:])), | ||
| "\n", | ||
| ) | ||
| ) | ||
| pkg_info.set_payload(dedented_description) | ||
| del pkg_info["Description"] | ||
| return pkg_info |
+1
-2
| Metadata-Version: 2.4 | ||
| Name: setuptools | ||
| Version: 80.10.1 | ||
| Version: 80.10.2 | ||
| Summary: Easily download, build, install, upgrade, and uninstall Python packages | ||
@@ -37,3 +37,2 @@ Author-email: Python Packaging Authority <distutils-sig@python.org> | ||
| Requires-Dist: pytest-perf; sys_platform != "cygwin" and extra == "test" | ||
| Requires-Dist: pyobjc<12; (sys_platform == "darwin" and python_version <= "3.9") and extra == "test" | ||
| Requires-Dist: jaraco.develop>=7.21; (python_version >= "3.9" and sys_platform != "cygwin") and extra == "test" | ||
@@ -40,0 +39,0 @@ Requires-Dist: pytest-home>=0.5; extra == "test" |
+1
-2
@@ -13,3 +13,3 @@ [build-system] | ||
| name = "setuptools" | ||
| version = "80.10.1" | ||
| version = "80.10.2" | ||
| authors = [ | ||
@@ -60,3 +60,2 @@ { name = "Python Packaging Authority", email = "distutils-sig@python.org" }, | ||
| 'pytest-perf; sys_platform != "cygwin"', # workaround for jaraco/inflect#195, pydantic/pydantic-core#773 (see #3986) | ||
| 'pyobjc<12; sys_platform == "darwin" and python_version <= "3.9"', # workaround for #5105 | ||
| # for tools/finalize.py | ||
@@ -63,0 +62,0 @@ 'jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin"', |
+3
-0
@@ -102,1 +102,4 @@ [pytest] | ||
| ignore:Couldn't import C tracer:coverage.exceptions.CoverageWarning | ||
| # https://github.com/pypa/setuptools/issues/5167 | ||
| ignore:The 'wheel.metadata' package has been made private:DeprecationWarning |
| Metadata-Version: 2.4 | ||
| Name: setuptools | ||
| Version: 80.10.1 | ||
| Version: 80.10.2 | ||
| Summary: Easily download, build, install, upgrade, and uninstall Python packages | ||
@@ -37,3 +37,2 @@ Author-email: Python Packaging Authority <distutils-sig@python.org> | ||
| Requires-Dist: pytest-perf; sys_platform != "cygwin" and extra == "test" | ||
| Requires-Dist: pyobjc<12; (sys_platform == "darwin" and python_version <= "3.9") and extra == "test" | ||
| Requires-Dist: jaraco.develop>=7.21; (python_version >= "3.9" and sys_platform != "cygwin") and extra == "test" | ||
@@ -40,0 +39,0 @@ Requires-Dist: pytest-home>=0.5; extra == "test" |
@@ -75,5 +75,2 @@ | ||
| [test:sys_platform == "darwin" and python_version <= "3.9"] | ||
| pyobjc<12 | ||
| [type] | ||
@@ -80,0 +77,0 @@ pytest-mypy |
@@ -262,3 +262,2 @@ LICENSE | ||
| setuptools/_vendor/backports/tarfile/compat/py38.py | ||
| setuptools/_vendor/bin/wheel | ||
| setuptools/_vendor/importlib_metadata/__init__.py | ||
@@ -341,2 +340,3 @@ setuptools/_vendor/importlib_metadata/_adapters.py | ||
| setuptools/_vendor/packaging/py.typed | ||
| setuptools/_vendor/packaging/pylock.py | ||
| setuptools/_vendor/packaging/requirements.py | ||
@@ -347,10 +347,10 @@ setuptools/_vendor/packaging/specifiers.py | ||
| setuptools/_vendor/packaging/version.py | ||
| setuptools/_vendor/packaging-25.0.dist-info/INSTALLER | ||
| setuptools/_vendor/packaging-25.0.dist-info/METADATA | ||
| setuptools/_vendor/packaging-25.0.dist-info/RECORD | ||
| setuptools/_vendor/packaging-25.0.dist-info/REQUESTED | ||
| setuptools/_vendor/packaging-25.0.dist-info/WHEEL | ||
| setuptools/_vendor/packaging-25.0.dist-info/licenses/LICENSE | ||
| setuptools/_vendor/packaging-25.0.dist-info/licenses/LICENSE.APACHE | ||
| setuptools/_vendor/packaging-25.0.dist-info/licenses/LICENSE.BSD | ||
| setuptools/_vendor/packaging-26.0.dist-info/INSTALLER | ||
| setuptools/_vendor/packaging-26.0.dist-info/METADATA | ||
| setuptools/_vendor/packaging-26.0.dist-info/RECORD | ||
| setuptools/_vendor/packaging-26.0.dist-info/REQUESTED | ||
| setuptools/_vendor/packaging-26.0.dist-info/WHEEL | ||
| setuptools/_vendor/packaging-26.0.dist-info/licenses/LICENSE | ||
| setuptools/_vendor/packaging-26.0.dist-info/licenses/LICENSE.APACHE | ||
| setuptools/_vendor/packaging-26.0.dist-info/licenses/LICENSE.BSD | ||
| setuptools/_vendor/packaging/licenses/__init__.py | ||
@@ -387,2 +387,3 @@ setuptools/_vendor/packaging/licenses/_spdx.py | ||
| setuptools/_vendor/wheel/_bdist_wheel.py | ||
| setuptools/_vendor/wheel/_metadata.py | ||
| setuptools/_vendor/wheel/_setuptools_logging.py | ||
@@ -392,34 +393,15 @@ setuptools/_vendor/wheel/bdist_wheel.py | ||
| setuptools/_vendor/wheel/metadata.py | ||
| setuptools/_vendor/wheel/util.py | ||
| setuptools/_vendor/wheel/wheelfile.py | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/INSTALLER | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/LICENSE.txt | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/METADATA | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/RECORD | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/REQUESTED | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/WHEEL | ||
| setuptools/_vendor/wheel-0.45.1.dist-info/entry_points.txt | ||
| setuptools/_vendor/wheel/cli/__init__.py | ||
| setuptools/_vendor/wheel/cli/convert.py | ||
| setuptools/_vendor/wheel/cli/pack.py | ||
| setuptools/_vendor/wheel/cli/tags.py | ||
| setuptools/_vendor/wheel/cli/unpack.py | ||
| setuptools/_vendor/wheel/vendored/__init__.py | ||
| setuptools/_vendor/wheel/vendored/vendor.txt | ||
| setuptools/_vendor/wheel/vendored/packaging/LICENSE | ||
| setuptools/_vendor/wheel/vendored/packaging/LICENSE.APACHE | ||
| setuptools/_vendor/wheel/vendored/packaging/LICENSE.BSD | ||
| setuptools/_vendor/wheel/vendored/packaging/__init__.py | ||
| setuptools/_vendor/wheel/vendored/packaging/_elffile.py | ||
| setuptools/_vendor/wheel/vendored/packaging/_manylinux.py | ||
| setuptools/_vendor/wheel/vendored/packaging/_musllinux.py | ||
| setuptools/_vendor/wheel/vendored/packaging/_parser.py | ||
| setuptools/_vendor/wheel/vendored/packaging/_structures.py | ||
| setuptools/_vendor/wheel/vendored/packaging/_tokenizer.py | ||
| setuptools/_vendor/wheel/vendored/packaging/markers.py | ||
| setuptools/_vendor/wheel/vendored/packaging/requirements.py | ||
| setuptools/_vendor/wheel/vendored/packaging/specifiers.py | ||
| setuptools/_vendor/wheel/vendored/packaging/tags.py | ||
| setuptools/_vendor/wheel/vendored/packaging/utils.py | ||
| setuptools/_vendor/wheel/vendored/packaging/version.py | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/INSTALLER | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/METADATA | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/RECORD | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/REQUESTED | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/WHEEL | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/entry_points.txt | ||
| setuptools/_vendor/wheel-0.46.3.dist-info/licenses/LICENSE.txt | ||
| setuptools/_vendor/wheel/_commands/__init__.py | ||
| setuptools/_vendor/wheel/_commands/convert.py | ||
| setuptools/_vendor/wheel/_commands/pack.py | ||
| setuptools/_vendor/wheel/_commands/tags.py | ||
| setuptools/_vendor/wheel/_commands/unpack.py | ||
| setuptools/_vendor/zipp/__init__.py | ||
@@ -426,0 +408,0 @@ setuptools/_vendor/zipp/_functools.py |
@@ -9,3 +9,3 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| __version__ = "25.0" | ||
| __version__ = "26.0" | ||
@@ -12,0 +12,0 @@ __author__ = "Donald Stufft and individual contributors" |
@@ -7,3 +7,2 @@ """ | ||
| Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca | ||
| ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html | ||
@@ -10,0 +9,0 @@ """ |
@@ -18,3 +18,13 @@ from __future__ import annotations | ||
| _ALLOWED_ARCHS = { | ||
| "x86_64", | ||
| "aarch64", | ||
| "ppc64", | ||
| "ppc64le", | ||
| "s390x", | ||
| "loongarch64", | ||
| "riscv64", | ||
| } | ||
| # `os.PathLike` not a generic type until Python 3.9, so sticking with `str` | ||
@@ -61,12 +71,3 @@ # as the type for `path` until then. | ||
| return _is_linux_i686(executable) | ||
| allowed_archs = { | ||
| "x86_64", | ||
| "aarch64", | ||
| "ppc64", | ||
| "ppc64le", | ||
| "s390x", | ||
| "loongarch64", | ||
| "riscv64", | ||
| } | ||
| return any(arch in allowed_archs for arch in archs) | ||
| return any(arch in _ALLOWED_ARCHS for arch in archs) | ||
@@ -111,3 +112,3 @@ | ||
| try: | ||
| import ctypes | ||
| import ctypes # noqa: PLC0415 | ||
| except ImportError: | ||
@@ -156,3 +157,3 @@ return None | ||
| def _parse_glibc_version(version_str: str) -> tuple[int, int]: | ||
| def _parse_glibc_version(version_str: str) -> _GLibCVersion: | ||
| """Parse glibc version. | ||
@@ -172,11 +173,11 @@ | ||
| ) | ||
| return -1, -1 | ||
| return int(m.group("major")), int(m.group("minor")) | ||
| return _GLibCVersion(-1, -1) | ||
| return _GLibCVersion(int(m.group("major")), int(m.group("minor"))) | ||
| @functools.lru_cache | ||
| def _get_glibc_version() -> tuple[int, int]: | ||
| def _get_glibc_version() -> _GLibCVersion: | ||
| version_str = _glibc_version_string() | ||
| if version_str is None: | ||
| return (-1, -1) | ||
| return _GLibCVersion(-1, -1) | ||
| return _parse_glibc_version(version_str) | ||
@@ -192,3 +193,3 @@ | ||
| try: | ||
| import _manylinux | ||
| import _manylinux # noqa: PLC0415 | ||
| except ImportError: | ||
@@ -201,21 +202,22 @@ return True | ||
| return True | ||
| if version == _GLibCVersion(2, 5): | ||
| if hasattr(_manylinux, "manylinux1_compatible"): | ||
| return bool(_manylinux.manylinux1_compatible) | ||
| if version == _GLibCVersion(2, 12): | ||
| if hasattr(_manylinux, "manylinux2010_compatible"): | ||
| return bool(_manylinux.manylinux2010_compatible) | ||
| if version == _GLibCVersion(2, 17): | ||
| if hasattr(_manylinux, "manylinux2014_compatible"): | ||
| return bool(_manylinux.manylinux2014_compatible) | ||
| if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"): | ||
| return bool(_manylinux.manylinux1_compatible) | ||
| if version == _GLibCVersion(2, 12) and hasattr( | ||
| _manylinux, "manylinux2010_compatible" | ||
| ): | ||
| return bool(_manylinux.manylinux2010_compatible) | ||
| if version == _GLibCVersion(2, 17) and hasattr( | ||
| _manylinux, "manylinux2014_compatible" | ||
| ): | ||
| return bool(_manylinux.manylinux2014_compatible) | ||
| return True | ||
| _LEGACY_MANYLINUX_MAP = { | ||
| _LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = { | ||
| # CentOS 7 w/ glibc 2.17 (PEP 599) | ||
| (2, 17): "manylinux2014", | ||
| _GLibCVersion(2, 17): "manylinux2014", | ||
| # CentOS 6 w/ glibc 2.12 (PEP 571) | ||
| (2, 12): "manylinux2010", | ||
| _GLibCVersion(2, 12): "manylinux2010", | ||
| # CentOS 5 w/ glibc 2.5 (PEP 513) | ||
| (2, 5): "manylinux1", | ||
| _GLibCVersion(2, 5): "manylinux1", | ||
| } | ||
@@ -262,9 +264,7 @@ | ||
| glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) | ||
| tag = "manylinux_{}_{}".format(*glibc_version) | ||
| if _is_compatible(arch, glibc_version): | ||
| yield f"{tag}_{arch}" | ||
| # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. | ||
| if glibc_version in _LEGACY_MANYLINUX_MAP: | ||
| legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] | ||
| if _is_compatible(arch, glibc_version): | ||
| yield "manylinux_{}_{}_{}".format(*glibc_version, arch) | ||
| # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. | ||
| if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version): | ||
| yield f"{legacy_tag}_{arch}" |
@@ -52,3 +52,3 @@ """PEP 656 support. | ||
| return None | ||
| proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) | ||
| proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True) | ||
| return _parse_musl_version(proc.stderr) | ||
@@ -55,0 +55,0 @@ |
@@ -10,3 +10,3 @@ """Handwritten parser of dependency specifiers. | ||
| import ast | ||
| from typing import NamedTuple, Sequence, Tuple, Union | ||
| from typing import List, Literal, NamedTuple, Sequence, Tuple, Union | ||
@@ -17,2 +17,4 @@ from ._tokenizer import DEFAULT_RULES, Tokenizer | ||
| class Node: | ||
| __slots__ = ("value",) | ||
| def __init__(self, value: str) -> None: | ||
@@ -25,3 +27,3 @@ self.value = value | ||
| def __repr__(self) -> str: | ||
| return f"<{self.__class__.__name__}('{self}')>" | ||
| return f"<{self.__class__.__name__}({self.value!r})>" | ||
@@ -33,2 +35,4 @@ def serialize(self) -> str: | ||
| class Variable(Node): | ||
| __slots__ = () | ||
| def serialize(self) -> str: | ||
@@ -39,2 +43,4 @@ return str(self) | ||
| class Value(Node): | ||
| __slots__ = () | ||
| def serialize(self) -> str: | ||
@@ -45,2 +51,4 @@ return f'"{self}"' | ||
| class Op(Node): | ||
| __slots__ = () | ||
| def serialize(self) -> str: | ||
@@ -50,6 +58,7 @@ return str(self) | ||
| MarkerLogical = Literal["and", "or"] | ||
| MarkerVar = Union[Variable, Value] | ||
| MarkerItem = Tuple[MarkerVar, Op, MarkerVar] | ||
| MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]] | ||
| MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]] | ||
| MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]] | ||
@@ -121,3 +130,5 @@ | ||
| marker = _parse_requirement_marker( | ||
| tokenizer, span_start=url_start, after="URL and whitespace" | ||
| tokenizer, | ||
| span_start=url_start, | ||
| expected="semicolon (after URL and whitespace)", | ||
| ) | ||
@@ -135,6 +146,6 @@ else: | ||
| span_start=specifier_start, | ||
| after=( | ||
| "version specifier" | ||
| expected=( | ||
| "comma (within version specifier), semicolon (after version specifier)" | ||
| if specifier | ||
| else "name and no valid version specifier" | ||
| else "semicolon (after name with no version specifier)" | ||
| ), | ||
@@ -147,3 +158,3 @@ ) | ||
| def _parse_requirement_marker( | ||
| tokenizer: Tokenizer, *, span_start: int, after: str | ||
| tokenizer: Tokenizer, *, span_start: int, expected: str | ||
| ) -> MarkerList: | ||
@@ -156,4 +167,5 @@ """ | ||
| tokenizer.raise_syntax_error( | ||
| f"Expected end or semicolon (after {after})", | ||
| f"Expected {expected} or end", | ||
| span_start=span_start, | ||
| span_end=None, | ||
| ) | ||
@@ -321,3 +333,3 @@ tokenizer.read() | ||
| def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: | ||
| def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503 | ||
| """ | ||
@@ -324,0 +336,0 @@ marker_var = VARIABLE | QUOTED_STRING |
@@ -5,4 +5,9 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| import typing | ||
| @typing.final | ||
| class InfinityType: | ||
| __slots__ = () | ||
| def __repr__(self) -> str: | ||
@@ -36,3 +41,6 @@ return "Infinity" | ||
| @typing.final | ||
| class NegativeInfinityType: | ||
| __slots__ = () | ||
| def __repr__(self) -> str: | ||
@@ -39,0 +47,0 @@ return "-Infinity" |
@@ -6,3 +6,3 @@ from __future__ import annotations | ||
| from dataclasses import dataclass | ||
| from typing import Iterator, NoReturn | ||
| from typing import Generator, Mapping, NoReturn | ||
@@ -37,12 +37,12 @@ from .specifiers import Specifier | ||
| marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" | ||
| return "\n ".join([self.message, self.source, marker]) | ||
| return f"{self.message}\n {self.source}\n {marker}" | ||
| DEFAULT_RULES: dict[str, str | re.Pattern[str]] = { | ||
| "LEFT_PARENTHESIS": r"\(", | ||
| "RIGHT_PARENTHESIS": r"\)", | ||
| "LEFT_BRACKET": r"\[", | ||
| "RIGHT_BRACKET": r"\]", | ||
| "SEMICOLON": r";", | ||
| "COMMA": r",", | ||
| DEFAULT_RULES: dict[str, re.Pattern[str]] = { | ||
| "LEFT_PARENTHESIS": re.compile(r"\("), | ||
| "RIGHT_PARENTHESIS": re.compile(r"\)"), | ||
| "LEFT_BRACKET": re.compile(r"\["), | ||
| "RIGHT_BRACKET": re.compile(r"\]"), | ||
| "SEMICOLON": re.compile(r";"), | ||
| "COMMA": re.compile(r","), | ||
| "QUOTED_STRING": re.compile( | ||
@@ -58,6 +58,6 @@ r""" | ||
| ), | ||
| "OP": r"(===|==|~=|!=|<=|>=|<|>)", | ||
| "BOOLOP": r"\b(or|and)\b", | ||
| "IN": r"\bin\b", | ||
| "NOT": r"\bnot\b", | ||
| "OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"), | ||
| "BOOLOP": re.compile(r"\b(or|and)\b"), | ||
| "IN": re.compile(r"\bin\b"), | ||
| "NOT": re.compile(r"\bnot\b"), | ||
| "VARIABLE": re.compile( | ||
@@ -84,9 +84,9 @@ r""" | ||
| ), | ||
| "AT": r"\@", | ||
| "URL": r"[^ \t]+", | ||
| "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", | ||
| "VERSION_PREFIX_TRAIL": r"\.\*", | ||
| "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", | ||
| "WS": r"[ \t]+", | ||
| "END": r"$", | ||
| "AT": re.compile(r"\@"), | ||
| "URL": re.compile(r"[^ \t]+"), | ||
| "IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"), | ||
| "VERSION_PREFIX_TRAIL": re.compile(r"\.\*"), | ||
| "VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"), | ||
| "WS": re.compile(r"[ \t]+"), | ||
| "END": re.compile(r"$"), | ||
| } | ||
@@ -106,8 +106,6 @@ | ||
| *, | ||
| rules: dict[str, str | re.Pattern[str]], | ||
| rules: Mapping[str, re.Pattern[str]], | ||
| ) -> None: | ||
| self.source = source | ||
| self.rules: dict[str, re.Pattern[str]] = { | ||
| name: re.compile(pattern) for name, pattern in rules.items() | ||
| } | ||
| self.rules = rules | ||
| self.next_token: Token | None = None | ||
@@ -182,3 +180,3 @@ self.position = 0 | ||
| self, open_token: str, close_token: str, *, around: str | ||
| ) -> Iterator[None]: | ||
| ) -> Generator[None, None, None]: | ||
| if self.check(open_token): | ||
@@ -185,0 +183,0 @@ open_position = self.position |
@@ -37,3 +37,3 @@ ####################################################################################### | ||
| from packaging.licenses._spdx import EXCEPTIONS, LICENSES | ||
| from ._spdx import EXCEPTIONS, LICENSES | ||
@@ -84,5 +84,6 @@ __all__ = [ | ||
| # Rather than implementing boolean logic, we create an expression that Python can | ||
| # parse. Everything that is not involved with the grammar itself is treated as | ||
| # `False` and the expression should evaluate as such. | ||
| # Rather than implementing a parenthesis/boolean logic parser, create an | ||
| # expression that Python can parse. Everything that is not involved with the | ||
| # grammar itself is replaced with the placeholder `False` and the resultant | ||
| # expression should become a valid Python expression. | ||
| python_tokens = [] | ||
@@ -94,3 +95,7 @@ for token in tokens: | ||
| python_tokens.append("or") | ||
| elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}: | ||
| elif ( | ||
| token == "(" | ||
| and python_tokens | ||
| and python_tokens[-1] not in {"or", "and", "("} | ||
| ) or (token == ")" and python_tokens and python_tokens[-1] == "("): | ||
| message = f"Invalid license expression: {raw_license_expression!r}" | ||
@@ -103,7 +108,4 @@ raise InvalidLicenseExpression(message) | ||
| try: | ||
| invalid = eval(python_expression, globals(), locals()) | ||
| except Exception: | ||
| invalid = True | ||
| if invalid is not False: | ||
| compile(python_expression, "", "eval") | ||
| except SyntaxError: | ||
| message = f"Invalid license expression: {raw_license_expression!r}" | ||
@@ -147,4 +149,4 @@ raise InvalidLicenseExpression(message) from None | ||
| return cast( | ||
| NormalizedLicenseExpression, | ||
| "NormalizedLicenseExpression", | ||
| normalized_expression.replace("( ", "(").replace(" )", ")"), | ||
| ) |
@@ -15,3 +15,3 @@ | ||
| VERSION = '3.25.0' | ||
| VERSION = '3.27.0' | ||
@@ -50,2 +50,3 @@ LICENSES: dict[str, SPDXLicense] = { | ||
| 'any-osi': {'id': 'any-OSI', 'deprecated': False}, | ||
| 'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False}, | ||
| 'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False}, | ||
@@ -66,2 +67,4 @@ 'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False}, | ||
| 'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False}, | ||
| 'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False}, | ||
| 'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False}, | ||
| 'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False}, | ||
@@ -81,2 +84,3 @@ 'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False}, | ||
| 'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False}, | ||
| 'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False}, | ||
| 'borceux': {'id': 'Borceux', 'deprecated': False}, | ||
@@ -92,2 +96,3 @@ 'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False}, | ||
| 'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False}, | ||
| 'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False}, | ||
| 'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False}, | ||
@@ -184,2 +189,4 @@ 'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False}, | ||
| 'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False}, | ||
| 'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False}, | ||
| 'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False}, | ||
| 'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False}, | ||
@@ -224,2 +231,3 @@ 'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False}, | ||
| 'crossword': {'id': 'Crossword', 'deprecated': False}, | ||
| 'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False}, | ||
| 'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False}, | ||
@@ -236,3 +244,5 @@ 'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False}, | ||
| 'doc': {'id': 'DOC', 'deprecated': False}, | ||
| 'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False}, | ||
| 'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False}, | ||
| 'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False}, | ||
| 'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False}, | ||
@@ -274,8 +284,13 @@ 'dotseqn': {'id': 'Dotseqn', 'deprecated': False}, | ||
| 'fsfullr': {'id': 'FSFULLR', 'deprecated': False}, | ||
| 'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False}, | ||
| 'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False}, | ||
| 'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False}, | ||
| 'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False}, | ||
| 'ftl': {'id': 'FTL', 'deprecated': False}, | ||
| 'furuseth': {'id': 'Furuseth', 'deprecated': False}, | ||
| 'fwlw': {'id': 'fwlw', 'deprecated': False}, | ||
| 'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False}, | ||
| 'gcr-docs': {'id': 'GCR-docs', 'deprecated': False}, | ||
| 'gd': {'id': 'GD', 'deprecated': False}, | ||
| 'generic-xts': {'id': 'generic-xts', 'deprecated': False}, | ||
| 'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True}, | ||
@@ -332,2 +347,3 @@ 'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False}, | ||
| 'haskellreport': {'id': 'HaskellReport', 'deprecated': False}, | ||
| 'hdf5': {'id': 'HDF5', 'deprecated': False}, | ||
| 'hdparm': {'id': 'hdparm', 'deprecated': False}, | ||
@@ -373,2 +389,3 @@ 'hidapi': {'id': 'HIDAPI', 'deprecated': False}, | ||
| 'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False}, | ||
| 'innosetup': {'id': 'InnoSetup', 'deprecated': False}, | ||
| 'intel': {'id': 'Intel', 'deprecated': False}, | ||
@@ -383,2 +400,3 @@ 'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False}, | ||
| 'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False}, | ||
| 'jove': {'id': 'jove', 'deprecated': False}, | ||
| 'jpl-image': {'id': 'JPL-image', 'deprecated': False}, | ||
@@ -409,2 +427,3 @@ 'jpnic': {'id': 'JPNIC', 'deprecated': False}, | ||
| 'libpng': {'id': 'Libpng', 'deprecated': False}, | ||
| 'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False}, | ||
| 'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False}, | ||
@@ -440,2 +459,3 @@ 'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False}, | ||
| 'makeindex': {'id': 'MakeIndex', 'deprecated': False}, | ||
| 'man2html': {'id': 'man2html', 'deprecated': False}, | ||
| 'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False}, | ||
@@ -445,2 +465,3 @@ 'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False}, | ||
| 'minpack': {'id': 'Minpack', 'deprecated': False}, | ||
| 'mips': {'id': 'MIPS', 'deprecated': False}, | ||
| 'miros': {'id': 'MirOS', 'deprecated': False}, | ||
@@ -450,2 +471,3 @@ 'mit': {'id': 'MIT', 'deprecated': False}, | ||
| 'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False}, | ||
| 'mit-click': {'id': 'MIT-Click', 'deprecated': False}, | ||
| 'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False}, | ||
@@ -491,2 +513,3 @@ 'mit-enna': {'id': 'MIT-enna', 'deprecated': False}, | ||
| 'ngpl': {'id': 'NGPL', 'deprecated': False}, | ||
| 'ngrep': {'id': 'ngrep', 'deprecated': False}, | ||
| 'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False}, | ||
@@ -506,2 +529,3 @@ 'nist-pd': {'id': 'NIST-PD', 'deprecated': False}, | ||
| 'nrl': {'id': 'NRL', 'deprecated': False}, | ||
| 'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False}, | ||
| 'ntp': {'id': 'NTP', 'deprecated': False}, | ||
@@ -601,2 +625,3 @@ 'ntp-0': {'id': 'NTP-0', 'deprecated': False}, | ||
| 'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False}, | ||
| 'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False}, | ||
| 'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False}, | ||
@@ -614,2 +639,3 @@ 'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False}, | ||
| 'sleepycat': {'id': 'Sleepycat', 'deprecated': False}, | ||
| 'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False}, | ||
| 'smlnj': {'id': 'SMLNJ', 'deprecated': False}, | ||
@@ -619,2 +645,3 @@ 'smppl': {'id': 'SMPPL', 'deprecated': False}, | ||
| 'snprintf': {'id': 'snprintf', 'deprecated': False}, | ||
| 'sofa': {'id': 'SOFA', 'deprecated': False}, | ||
| 'softsurfer': {'id': 'softSurfer', 'deprecated': False}, | ||
@@ -633,2 +660,3 @@ 'soundex': {'id': 'Soundex', 'deprecated': False}, | ||
| 'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False}, | ||
| 'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False}, | ||
| 'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False}, | ||
@@ -645,2 +673,3 @@ 'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False}, | ||
| 'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False}, | ||
| 'thirdeye': {'id': 'ThirdEye', 'deprecated': False}, | ||
| 'threeparttable': {'id': 'threeparttable', 'deprecated': False}, | ||
@@ -652,2 +681,3 @@ 'tmate': {'id': 'TMate', 'deprecated': False}, | ||
| 'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False}, | ||
| 'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False}, | ||
| 'ttwl': {'id': 'TTWL', 'deprecated': False}, | ||
@@ -668,2 +698,4 @@ 'ttyp0': {'id': 'TTYP0', 'deprecated': False}, | ||
| 'unlicense': {'id': 'Unlicense', 'deprecated': False}, | ||
| 'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False}, | ||
| 'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False}, | ||
| 'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False}, | ||
@@ -682,2 +714,3 @@ 'urt-rle': {'id': 'URT-RLE', 'deprecated': False}, | ||
| 'wtfpl': {'id': 'WTFPL', 'deprecated': False}, | ||
| 'wwl': {'id': 'wwl', 'deprecated': False}, | ||
| 'wxwindows': {'id': 'wxWindows', 'deprecated': True}, | ||
@@ -724,5 +757,7 @@ 'x11': {'id': 'X11', 'deprecated': False}, | ||
| 'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False}, | ||
| 'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False}, | ||
| 'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False}, | ||
| 'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False}, | ||
| 'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False}, | ||
| 'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False}, | ||
| 'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False}, | ||
@@ -744,2 +779,3 @@ 'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False}, | ||
| 'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False}, | ||
| 'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False}, | ||
| 'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False}, | ||
@@ -751,3 +787,5 @@ 'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False}, | ||
| 'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False}, | ||
| 'harbour-exception': {'id': 'harbour-exception', 'deprecated': False}, | ||
| 'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False}, | ||
| 'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False}, | ||
| 'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False}, | ||
@@ -762,2 +800,3 @@ 'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False}, | ||
| 'mif-exception': {'id': 'mif-exception', 'deprecated': False}, | ||
| 'mxml-exception': {'id': 'mxml-exception', 'deprecated': False}, | ||
| 'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True}, | ||
@@ -769,2 +808,3 @@ 'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False}, | ||
| 'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False}, | ||
| 'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False}, | ||
| 'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False}, | ||
@@ -771,0 +811,0 @@ 'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False}, |
@@ -11,3 +11,3 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| import sys | ||
| from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast | ||
| from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast | ||
@@ -21,2 +21,3 @@ from ._parser import MarkerAtom, MarkerList, Op, Value, Variable | ||
| __all__ = [ | ||
| "Environment", | ||
| "EvaluateContext", | ||
@@ -33,2 +34,8 @@ "InvalidMarker", | ||
| MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} | ||
| MARKERS_REQUIRING_VERSION = { | ||
| "implementation_version", | ||
| "platform_release", | ||
| "python_full_version", | ||
| "python_version", | ||
| } | ||
@@ -127,18 +134,26 @@ | ||
| def _normalize_extra_values(results: Any) -> Any: | ||
| def _normalize_extras( | ||
| result: MarkerList | MarkerAtom | str, | ||
| ) -> MarkerList | MarkerAtom | str: | ||
| if not isinstance(result, tuple): | ||
| return result | ||
| lhs, op, rhs = result | ||
| if isinstance(lhs, Variable) and lhs.value == "extra": | ||
| normalized_extra = canonicalize_name(rhs.value) | ||
| rhs = Value(normalized_extra) | ||
| elif isinstance(rhs, Variable) and rhs.value == "extra": | ||
| normalized_extra = canonicalize_name(lhs.value) | ||
| lhs = Value(normalized_extra) | ||
| return lhs, op, rhs | ||
| def _normalize_extra_values(results: MarkerList) -> MarkerList: | ||
| """ | ||
| Normalize extra values. | ||
| """ | ||
| if isinstance(results[0], tuple): | ||
| lhs, op, rhs = results[0] | ||
| if isinstance(lhs, Variable) and lhs.value == "extra": | ||
| normalized_extra = canonicalize_name(rhs.value) | ||
| rhs = Value(normalized_extra) | ||
| elif isinstance(rhs, Variable) and rhs.value == "extra": | ||
| normalized_extra = canonicalize_name(lhs.value) | ||
| lhs = Value(normalized_extra) | ||
| results[0] = lhs, op, rhs | ||
| return results | ||
| return [_normalize_extras(r) for r in results] | ||
| def _format_marker( | ||
@@ -175,15 +190,16 @@ marker: list[str] | MarkerAtom | str, first: bool | None = True | ||
| "not in": lambda lhs, rhs: lhs not in rhs, | ||
| "<": operator.lt, | ||
| "<=": operator.le, | ||
| "<": lambda _lhs, _rhs: False, | ||
| "<=": operator.eq, | ||
| "==": operator.eq, | ||
| "!=": operator.ne, | ||
| ">=": operator.ge, | ||
| ">": operator.gt, | ||
| ">=": operator.eq, | ||
| ">": lambda _lhs, _rhs: False, | ||
| } | ||
| def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool: | ||
| if isinstance(rhs, str): | ||
| def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool: | ||
| op_str = op.serialize() | ||
| if key in MARKERS_REQUIRING_VERSION: | ||
| try: | ||
| spec = Specifier("".join([op.serialize(), rhs])) | ||
| spec = Specifier(f"{op_str}{rhs}") | ||
| except InvalidSpecifier: | ||
@@ -194,3 +210,3 @@ pass | ||
| oper: Operator | None = _operators.get(op.serialize()) | ||
| oper: Operator | None = _operators.get(op_str) | ||
| if oper is None: | ||
@@ -205,3 +221,3 @@ raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") | ||
| ) -> tuple[str, str | AbstractSet[str]]: | ||
| # PEP 685 – Comparison of extra names for optional distribution dependencies | ||
| # PEP 685 - Comparison of extra names for optional distribution dependencies | ||
| # https://peps.python.org/pep-0685/ | ||
@@ -212,3 +228,4 @@ # > When comparing extra names, tools MUST normalize the names being | ||
| assert isinstance(rhs, str), "extra value must be a string" | ||
| return (canonicalize_name(lhs), canonicalize_name(rhs)) | ||
| # Both sides are normalized at this point already | ||
| return (lhs, rhs) | ||
| if key in MARKERS_ALLOWING_SET: | ||
@@ -230,4 +247,2 @@ if isinstance(rhs, str): # pragma: no cover | ||
| for marker in markers: | ||
| assert isinstance(marker, (list, tuple, str)) | ||
| if isinstance(marker, list): | ||
@@ -246,9 +261,12 @@ groups[-1].append(_evaluate_markers(marker, environment)) | ||
| rhs_value = environment[environment_key] | ||
| assert isinstance(lhs_value, str), "lhs must be a string" | ||
| lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) | ||
| groups[-1].append(_eval_op(lhs_value, op, rhs_value)) | ||
| else: | ||
| assert marker in ["and", "or"] | ||
| if marker == "or": | ||
| groups.append([]) | ||
| groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key)) | ||
| elif marker == "or": | ||
| groups.append([]) | ||
| elif marker == "and": | ||
| pass | ||
| else: # pragma: nocover | ||
| raise TypeError(f"Unexpected marker {marker!r}") | ||
@@ -289,2 +307,7 @@ return any(all(item) for item in groups) | ||
| # added here, make sure to mirror/adapt Requirement. | ||
| # If this fails and throws an error, the repr still expects _markers to | ||
| # be defined. | ||
| self._markers: MarkerList = [] | ||
| try: | ||
@@ -315,8 +338,8 @@ self._markers = _normalize_extra_values(_parse_marker(marker)) | ||
| def __repr__(self) -> str: | ||
| return f"<Marker('{self}')>" | ||
| return f"<{self.__class__.__name__}('{self}')>" | ||
| def __hash__(self) -> int: | ||
| return hash((self.__class__.__name__, str(self))) | ||
| return hash(str(self)) | ||
| def __eq__(self, other: Any) -> bool: | ||
| def __eq__(self, other: object) -> bool: | ||
| if not isinstance(other, Marker): | ||
@@ -329,3 +352,3 @@ return NotImplemented | ||
| self, | ||
| environment: dict[str, str] | None = None, | ||
| environment: Mapping[str, str | AbstractSet[str]] | None = None, | ||
| context: EvaluateContext = "metadata", | ||
@@ -353,8 +376,11 @@ ) -> bool: | ||
| current_environment["extra"] = "" | ||
| if environment is not None: | ||
| current_environment.update(environment) | ||
| # The API used to allow setting extra to None. We need to handle this | ||
| # case for backwards compatibility. | ||
| if "extra" in current_environment and current_environment["extra"] is None: | ||
| current_environment["extra"] = "" | ||
| if "extra" in current_environment: | ||
| # The API used to allow setting extra to None. We need to handle | ||
| # this case for backwards compatibility. Also skip running | ||
| # normalize name if extra is empty. | ||
| extra = cast("str | None", current_environment["extra"]) | ||
| current_environment["extra"] = canonicalize_name(extra) if extra else "" | ||
@@ -373,5 +399,5 @@ return _evaluate_markers( | ||
| """ | ||
| python_full_version = cast(str, env["python_full_version"]) | ||
| python_full_version = cast("str", env["python_full_version"]) | ||
| if python_full_version.endswith("+"): | ||
| env["python_full_version"] = f"{python_full_version}local" | ||
| return env |
@@ -8,2 +8,3 @@ from __future__ import annotations | ||
| import email.policy | ||
| import keyword | ||
| import pathlib | ||
@@ -23,4 +24,6 @@ import sys | ||
| from . import version as version_module | ||
| from .licenses import NormalizedLicenseExpression | ||
| if typing.TYPE_CHECKING: | ||
| from .licenses import NormalizedLicenseExpression | ||
| T = typing.TypeVar("T") | ||
@@ -30,3 +33,3 @@ | ||
| if sys.version_info >= (3, 11): # pragma: no cover | ||
| ExceptionGroup = ExceptionGroup | ||
| ExceptionGroup = ExceptionGroup # noqa: F821 | ||
| else: # pragma: no cover | ||
@@ -132,3 +135,3 @@ | ||
| # No new fields were added in PEP 685, just some edge case were | ||
| # tightened up to provide better interoptability. | ||
| # tightened up to provide better interoperability. | ||
@@ -139,3 +142,9 @@ # Metadata 2.4 - PEP 639 | ||
| # Metadata 2.5 - PEP 794 | ||
| import_names: list[str] | ||
| import_namespaces: list[str] | ||
| # 'keywords' is special as it's a string in the core metadata spec, but we | ||
| # represent it as a list. | ||
| _STRING_FIELDS = { | ||
@@ -173,2 +182,4 @@ "author", | ||
| "supported_platforms", | ||
| "import_names", | ||
| "import_namespaces", | ||
| } | ||
@@ -202,10 +213,8 @@ | ||
| # | ||
| # The other potentional issue is that it's possible to have the | ||
| # The other potential issue is that it's possible to have the | ||
| # same label multiple times in the metadata, with no solid "right" | ||
| # answer with what to do in that case. As such, we'll do the only | ||
| # thing we can, which is treat the field as unparseable and add it | ||
| # thing we can, which is treat the field as unparsable and add it | ||
| # to our list of unparsed fields. | ||
| parts = [p.strip() for p in pair.split(",", 1)] | ||
| parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items | ||
| # | ||
| # TODO: The spec doesn't say anything about if the keys should be | ||
@@ -216,7 +225,8 @@ # considered case sensitive or not... logically they should | ||
| # entries. | ||
| label, url = parts | ||
| label, _, url = (s.strip() for s in pair.partition(",")) | ||
| if label in urls: | ||
| # The label already exists in our set of urls, so this field | ||
| # is unparseable, and we can just add the whole thing to our | ||
| # unparseable data and stop processing it. | ||
| # is unparsable, and we can just add the whole thing to our | ||
| # unparsable data and stop processing it. | ||
| raise KeyError("duplicate labels in project urls") | ||
@@ -268,2 +278,4 @@ urls[label] = url | ||
| "home-page": "home_page", | ||
| "import-name": "import_names", | ||
| "import-namespace": "import_namespaces", | ||
| "keywords": "keywords", | ||
@@ -295,2 +307,41 @@ "license": "license", | ||
| # This class is for writing RFC822 messages | ||
| class RFC822Policy(email.policy.EmailPolicy): | ||
| """ | ||
| This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse`` | ||
| implementation that handles multi-line values, and some nice defaults. | ||
| """ | ||
| utf8 = True | ||
| mangle_from_ = False | ||
| max_line_length = 0 | ||
| def header_store_parse(self, name: str, value: str) -> tuple[str, str]: | ||
| size = len(name) + 2 | ||
| value = value.replace("\n", "\n" + " " * size) | ||
| return (name, value) | ||
| # This class is for writing RFC822 messages | ||
| class RFC822Message(email.message.EmailMessage): | ||
| """ | ||
| This is :class:`email.message.EmailMessage` with two small changes: it defaults to | ||
| our `RFC822Policy`, and it correctly writes unicode when being called | ||
| with `bytes()`. | ||
| """ | ||
| def __init__(self) -> None: | ||
| super().__init__(policy=RFC822Policy()) | ||
| def as_bytes( | ||
| self, unixfrom: bool = False, policy: email.policy.Policy | None = None | ||
| ) -> bytes: | ||
| """ | ||
| Return the bytes representation of the message. | ||
| This handles unicode encoding. | ||
| """ | ||
| return self.as_string(unixfrom, policy=policy).encode("utf-8") | ||
| def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: | ||
@@ -323,6 +374,6 @@ """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``). | ||
| # list of keys, but we're avoiding that by using get_all(). | ||
| for name in frozenset(parsed.keys()): | ||
| for name_with_case in frozenset(parsed.keys()): | ||
| # Header names in RFC are case insensitive, so we'll normalize to all | ||
| # lower case to make comparisons easier. | ||
| name = name.lower() | ||
| name = name_with_case.lower() | ||
@@ -363,5 +414,5 @@ # We use get_all() here, even for fields that aren't multiple use, | ||
| chunks: list[tuple[bytes, str | None]] = [] | ||
| for bin, encoding in email.header.decode_header(h): | ||
| for binary, _encoding in email.header.decode_header(h): | ||
| try: | ||
| bin.decode("utf8", "strict") | ||
| binary.decode("utf8", "strict") | ||
| except UnicodeDecodeError: | ||
@@ -373,3 +424,3 @@ # Enable mojibake. | ||
| encoding = "utf8" | ||
| chunks.append((bin, encoding)) | ||
| chunks.append((binary, encoding)) | ||
@@ -413,2 +464,7 @@ # Turn our chunks back into a Header object, then let that | ||
| raw[raw_name] = value[0] | ||
| # If this is import_names, we need to special case the empty field | ||
| # case, which converts to an empty list instead of None. We can't let | ||
| # the empty case slip through, as it will fail validation. | ||
| elif raw_name == "import_names" and value == [""]: | ||
| raw[raw_name] = [] | ||
| # If this is one of our list of string fields, then we can just assign | ||
@@ -441,3 +497,3 @@ # the value, since email *only* has strings, and our get_all() call | ||
| # Nothing that we've done has managed to parse this, so it'll just | ||
| # throw it in our unparseable data and move on. | ||
| # throw it in our unparsable data and move on. | ||
| else: | ||
@@ -459,5 +515,5 @@ unparsed[name] = value | ||
| # Check to see if we've already got a description, if so then both | ||
| # it, and this body move to unparseable. | ||
| # it, and this body move to unparsable. | ||
| if "description" in raw: | ||
| description_header = cast(str, raw.pop("description")) | ||
| description_header = cast("str", raw.pop("description")) | ||
| unparsed.setdefault("description", []).extend( | ||
@@ -475,3 +531,3 @@ [description_header, payload] | ||
| # names. | ||
| return cast(RawMetadata, raw), unparsed | ||
| return cast("RawMetadata", raw), unparsed | ||
@@ -483,4 +539,4 @@ | ||
| # Keep the two values in sync. | ||
| _VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"] | ||
| _MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"] | ||
| _VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] | ||
| _MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] | ||
@@ -540,3 +596,3 @@ _REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) | ||
| return cast(T, value) | ||
| return cast("T", value) | ||
@@ -556,3 +612,3 @@ def _invalid_metadata( | ||
| raise self._invalid_metadata(f"{value!r} is not a valid metadata version") | ||
| return cast(_MetadataVersion, value) | ||
| return cast("_MetadataVersion", value) | ||
@@ -670,5 +726,3 @@ def _process_name(self, value: str) -> str: | ||
| def _process_license_expression( | ||
| self, value: str | ||
| ) -> NormalizedLicenseExpression | None: | ||
| def _process_license_expression(self, value: str) -> NormalizedLicenseExpression: | ||
| try: | ||
@@ -707,3 +761,27 @@ return licenses.canonicalize_license_expression(value) | ||
| def _process_import_names(self, value: list[str]) -> list[str]: | ||
| for import_name in value: | ||
| name, semicolon, private = import_name.partition(";") | ||
| name = name.rstrip() | ||
| for identifier in name.split("."): | ||
| if not identifier.isidentifier(): | ||
| raise self._invalid_metadata( | ||
| f"{name!r} is invalid for {{field}}; " | ||
| f"{identifier!r} is not a valid identifier" | ||
| ) | ||
| elif keyword.iskeyword(identifier): | ||
| raise self._invalid_metadata( | ||
| f"{name!r} is invalid for {{field}}; " | ||
| f"{identifier!r} is a keyword" | ||
| ) | ||
| if semicolon and private.lstrip() != "private": | ||
| raise self._invalid_metadata( | ||
| f"{import_name!r} is invalid for {{field}}; " | ||
| "the only valid option is 'private'" | ||
| ) | ||
| return value | ||
| _process_import_namespaces = _process_import_names | ||
| class Metadata: | ||
@@ -879,2 +957,6 @@ """Representation of distribution metadata. | ||
| """:external:ref:`core-metadata-obsoletes-dist`""" | ||
| import_names: _Validator[list[str] | None] = _Validator(added="2.5") | ||
| """:external:ref:`core-metadata-import-name`""" | ||
| import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5") | ||
| """:external:ref:`core-metadata-import-namespace`""" | ||
| requires: _Validator[list[str] | None] = _Validator(added="1.1") | ||
@@ -886,1 +968,35 @@ """``Requires`` (deprecated)""" | ||
| """``Obsoletes`` (deprecated)""" | ||
| def as_rfc822(self) -> RFC822Message: | ||
| """ | ||
| Return an RFC822 message with the metadata. | ||
| """ | ||
| message = RFC822Message() | ||
| self._write_metadata(message) | ||
| return message | ||
| def _write_metadata(self, message: RFC822Message) -> None: | ||
| """ | ||
| Return an RFC822 message with the metadata. | ||
| """ | ||
| for name, validator in self.__class__.__dict__.items(): | ||
| if isinstance(validator, _Validator) and name != "description": | ||
| value = getattr(self, name) | ||
| email_name = _RAW_TO_EMAIL_MAPPING[name] | ||
| if value is not None: | ||
| if email_name == "project-url": | ||
| for label, url in value.items(): | ||
| message[email_name] = f"{label}, {url}" | ||
| elif email_name == "keywords": | ||
| message[email_name] = ",".join(value) | ||
| elif email_name == "import-name" and value == []: | ||
| message[email_name] = "" | ||
| elif isinstance(value, list): | ||
| for item in value: | ||
| message[email_name] = str(item) | ||
| else: | ||
| message[email_name] = str(value) | ||
| # The description is a special case because it is in the body of the message. | ||
| if self.description is not None: | ||
| message.set_payload(self.description) |
@@ -6,3 +6,3 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| from typing import Any, Iterator | ||
| from typing import Iterator | ||
@@ -61,3 +61,3 @@ from ._parser import parse_requirement as _parse_requirement | ||
| if self.url: | ||
| yield f"@ {self.url}" | ||
| yield f" @ {self.url}" | ||
| if self.marker: | ||
@@ -73,13 +73,8 @@ yield " " | ||
| def __repr__(self) -> str: | ||
| return f"<Requirement('{self}')>" | ||
| return f"<{self.__class__.__name__}('{self}')>" | ||
| def __hash__(self) -> int: | ||
| return hash( | ||
| ( | ||
| self.__class__.__name__, | ||
| *self._iter_parts(canonicalize_name(self.name)), | ||
| ) | ||
| ) | ||
| return hash(tuple(self._iter_parts(canonicalize_name(self.name)))) | ||
| def __eq__(self, other: Any) -> bool: | ||
| def __eq__(self, other: object) -> bool: | ||
| if not isinstance(other, Requirement): | ||
@@ -86,0 +81,0 @@ return NotImplemented |
@@ -16,6 +16,6 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| import re | ||
| from typing import Callable, Iterable, Iterator, TypeVar, Union | ||
| from typing import Callable, Final, Iterable, Iterator, TypeVar, Union | ||
| from .utils import canonicalize_version | ||
| from .version import Version | ||
| from .version import InvalidVersion, Version | ||
@@ -27,8 +27,19 @@ UnparsedVersion = Union[Version, str] | ||
| def _coerce_version(version: UnparsedVersion) -> Version: | ||
| def _coerce_version(version: UnparsedVersion) -> Version | None: | ||
| if not isinstance(version, Version): | ||
| version = Version(version) | ||
| try: | ||
| version = Version(version) | ||
| except InvalidVersion: | ||
| return None | ||
| return version | ||
| def _public_version(version: Version) -> Version: | ||
| return version.__replace__(local=None) | ||
| def _base_version(version: Version) -> Version: | ||
| return version.__replace__(pre=None, post=None, dev=None, local=None) | ||
| class InvalidSpecifier(ValueError): | ||
@@ -47,2 +58,10 @@ """ | ||
| class BaseSpecifier(metaclass=abc.ABCMeta): | ||
| __slots__ = () | ||
| __match_args__ = ("_str",) | ||
| @property | ||
| def _str(self) -> str: | ||
| """Internal property for match_args""" | ||
| return str(self) | ||
| @abc.abstractmethod | ||
@@ -79,3 +98,3 @@ def __str__(self) -> str: | ||
| @prereleases.setter | ||
| @prereleases.setter # noqa: B027 | ||
| def prereleases(self, value: bool) -> None: | ||
@@ -113,2 +132,4 @@ """Setter for :attr:`prereleases`. | ||
| __slots__ = ("_prereleases", "_spec", "_spec_version") | ||
| _operator_regex_str = r""" | ||
@@ -212,7 +233,7 @@ (?P<operator>(~=|==|!=|<=|>=|<|>|===)) | ||
| _regex = re.compile( | ||
| r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$", | ||
| r"\s*" + _operator_regex_str + _version_regex_str + r"\s*", | ||
| re.VERBOSE | re.IGNORECASE, | ||
| ) | ||
| _operators = { | ||
| _operators: Final = { | ||
| "~=": "compatible", | ||
@@ -241,3 +262,3 @@ "==": "equal", | ||
| """ | ||
| match = self._regex.search(spec) | ||
| match = self._regex.fullmatch(spec) | ||
| if not match: | ||
@@ -254,5 +275,29 @@ raise InvalidSpecifier(f"Invalid specifier: {spec!r}") | ||
| # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 | ||
| @property # type: ignore[override] | ||
| def prereleases(self) -> bool: | ||
| # Specifier version cache | ||
| self._spec_version: tuple[str, Version] | None = None | ||
| def _get_spec_version(self, version: str) -> Version | None: | ||
| """One element cache, as only one spec Version is needed per Specifier.""" | ||
| if self._spec_version is not None and self._spec_version[0] == version: | ||
| return self._spec_version[1] | ||
| version_specifier = _coerce_version(version) | ||
| if version_specifier is None: | ||
| return None | ||
| self._spec_version = (version, version_specifier) | ||
| return version_specifier | ||
| def _require_spec_version(self, version: str) -> Version: | ||
| """Get spec version, asserting it's valid (not for === operator). | ||
| This method should only be called for operators where version | ||
| strings are guaranteed to be valid PEP 440 versions (not ===). | ||
| """ | ||
| spec_version = self._get_spec_version(version) | ||
| assert spec_version is not None | ||
| return spec_version | ||
| @property | ||
| def prereleases(self) -> bool | None: | ||
| # If there is an explicit prereleases set for this, then we'll just | ||
@@ -263,15 +308,20 @@ # blindly use that. | ||
| # Look at all of our specifiers and determine if they are inclusive | ||
| # operators, and if they are if they are including an explicit | ||
| # prerelease. | ||
| operator, version = self._spec | ||
| if operator in ["==", ">=", "<=", "~=", "===", ">", "<"]: | ||
| # The == specifier can include a trailing .*, if it does we | ||
| # want to remove before parsing. | ||
| if operator == "==" and version.endswith(".*"): | ||
| version = version[:-2] | ||
| # Only the "!=" operator does not imply prereleases when | ||
| # the version in the specifier is a prerelease. | ||
| operator, version_str = self._spec | ||
| if operator != "!=": | ||
| # The == specifier with trailing .* cannot include prereleases | ||
| # e.g. "==1.0a1.*" is not valid. | ||
| if operator == "==" and version_str.endswith(".*"): | ||
| return False | ||
| # Parse the version, and if it is a pre-release than this | ||
| # specifier allows pre-releases. | ||
| if Version(version).is_prerelease: | ||
| # "===" can have arbitrary string versions, so we cannot parse | ||
| # those, we take prereleases as unknown (None) for those. | ||
| version = self._get_spec_version(version_str) | ||
| if version is None: | ||
| return None | ||
| # For all other operators, use the check if spec Version | ||
| # object implies pre-releases. | ||
| if version.is_prerelease: | ||
| return True | ||
@@ -282,3 +332,3 @@ | ||
| @prereleases.setter | ||
| def prereleases(self, value: bool) -> None: | ||
| def prereleases(self, value: bool | None) -> None: | ||
| self._prereleases = value | ||
@@ -334,8 +384,14 @@ | ||
| def _canonical_spec(self) -> tuple[str, str]: | ||
| operator, version = self._spec | ||
| if operator == "===" or version.endswith(".*"): | ||
| return operator, version | ||
| spec_version = self._require_spec_version(version) | ||
| canonical_version = canonicalize_version( | ||
| self._spec[1], | ||
| strip_trailing_zero=(self._spec[0] != "~="), | ||
| spec_version, strip_trailing_zero=(operator != "~=") | ||
| ) | ||
| return self._spec[0], canonical_version | ||
| return operator, canonical_version | ||
| def __hash__(self) -> int: | ||
@@ -404,3 +460,3 @@ return hash(self._canonical_spec) | ||
| normalized_prospective = canonicalize_version( | ||
| prospective.public, strip_trailing_zero=False | ||
| _public_version(prospective), strip_trailing_zero=False | ||
| ) | ||
@@ -430,3 +486,3 @@ # Get the normalized version string ignoring the trailing .* | ||
| # Convert our spec string into a Version | ||
| spec_version = Version(spec) | ||
| spec_version = self._require_spec_version(spec) | ||
@@ -437,3 +493,3 @@ # If the specifier does not have a local segment, then we want to | ||
| if not spec_version.local: | ||
| prospective = Version(prospective.public) | ||
| prospective = _public_version(prospective) | ||
@@ -449,3 +505,3 @@ return prospective == spec_version | ||
| # the prospective version. | ||
| return Version(prospective.public) <= Version(spec) | ||
| return _public_version(prospective) <= self._require_spec_version(spec) | ||
@@ -456,3 +512,3 @@ def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: | ||
| # the prospective version. | ||
| return Version(prospective.public) >= Version(spec) | ||
| return _public_version(prospective) >= self._require_spec_version(spec) | ||
@@ -462,3 +518,3 @@ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: | ||
| # it as a version. | ||
| spec = Version(spec_str) | ||
| spec = self._require_spec_version(spec_str) | ||
@@ -475,5 +531,8 @@ # Check to see if the prospective version is less than the spec | ||
| # not match 3.1.dev0, but should match 3.0.dev0). | ||
| if not spec.is_prerelease and prospective.is_prerelease: | ||
| if Version(prospective.base_version) == Version(spec.base_version): | ||
| return False | ||
| if ( | ||
| not spec.is_prerelease | ||
| and prospective.is_prerelease | ||
| and _base_version(prospective) == _base_version(spec) | ||
| ): | ||
| return False | ||
@@ -488,3 +547,3 @@ # If we've gotten to here, it means that prospective version is both | ||
| # it as a version. | ||
| spec = Version(spec_str) | ||
| spec = self._require_spec_version(spec_str) | ||
@@ -501,11 +560,15 @@ # Check to see if the prospective version is greater than the spec | ||
| # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). | ||
| if not spec.is_postrelease and prospective.is_postrelease: | ||
| if Version(prospective.base_version) == Version(spec.base_version): | ||
| return False | ||
| if ( | ||
| not spec.is_postrelease | ||
| and prospective.is_postrelease | ||
| and _base_version(prospective) == _base_version(spec) | ||
| ): | ||
| return False | ||
| # Ensure that we do not allow a local version of the version mentioned | ||
| # in the specifier, which is technically greater than, to match. | ||
| if prospective.local is not None: | ||
| if Version(prospective.base_version) == Version(spec.base_version): | ||
| return False | ||
| if prospective.local is not None and _base_version( | ||
| prospective | ||
| ) == _base_version(spec): | ||
| return False | ||
@@ -517,3 +580,3 @@ # If we've gotten to here, it means that prospective version is both | ||
| def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: | ||
| def _compare_arbitrary(self, prospective: Version | str, spec: str) -> bool: | ||
| return str(prospective).lower() == str(spec).lower() | ||
@@ -536,3 +599,3 @@ | ||
| >>> "1.3.0a1" in Specifier(">=1.2.3") | ||
| False | ||
| True | ||
| >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) | ||
@@ -551,4 +614,4 @@ True | ||
| Whether or not to match prereleases with this Specifier. If set to | ||
| ``None`` (the default), it uses :attr:`prereleases` to determine | ||
| whether or not prereleases are allowed. | ||
| ``None`` (the default), it will follow the recommendation from | ||
| :pep:`440` and match prereleases, as there are no other versions. | ||
@@ -562,28 +625,11 @@ >>> Specifier(">=1.2.3").contains("1.2.3") | ||
| >>> Specifier(">=1.2.3").contains("1.3.0a1") | ||
| True | ||
| >>> Specifier(">=1.2.3", prereleases=False).contains("1.3.0a1") | ||
| False | ||
| >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") | ||
| >>> Specifier(">=1.2.3").contains("1.3.0a1") | ||
| True | ||
| >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) | ||
| True | ||
| """ | ||
| # Determine if prereleases are to be allowed or not. | ||
| if prereleases is None: | ||
| prereleases = self.prereleases | ||
| return bool(list(self.filter([item], prereleases=prereleases))) | ||
| # Normalize item to a Version, this allows us to have a shortcut for | ||
| # "2.0" in Specifier(">=2") | ||
| normalized_item = _coerce_version(item) | ||
| # Determine if we should be supporting prereleases in this specifier | ||
| # or not, if we do not support prereleases than we can short circuit | ||
| # logic if this version is a prereleases. | ||
| if normalized_item.is_prerelease and not prereleases: | ||
| return False | ||
| # Actually do the comparison to determine if this item is contained | ||
| # within this Specifier or not. | ||
| operator_callable: CallableOperator = self._get_operator(self.operator) | ||
| return operator_callable(normalized_item, self.version) | ||
| def filter( | ||
@@ -599,10 +645,5 @@ self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None | ||
| Whether or not to allow prereleases in the returned iterator. If set to | ||
| ``None`` (the default), it will be intelligently decide whether to allow | ||
| prereleases or not (based on the :attr:`prereleases` attribute, and | ||
| whether the only versions matching are prereleases). | ||
| ``None`` (the default), it will follow the recommendation from :pep:`440` | ||
| and match prereleases if there are no other versions. | ||
| This method is smarter than just ``filter(Specifier().contains, [...])`` | ||
| because it implements the rule from :pep:`440` that a prerelease item | ||
| SHOULD be accepted if no other versions match the given specifier. | ||
| >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) | ||
@@ -619,36 +660,42 @@ ['1.3'] | ||
| """ | ||
| prereleases_versions = [] | ||
| found_non_prereleases = False | ||
| yielded = False | ||
| found_prereleases = [] | ||
| # Determine if to include prereleases by default | ||
| include_prereleases = ( | ||
| prereleases if prereleases is not None else self.prereleases | ||
| ) | ||
| kw = {"prereleases": prereleases if prereleases is not None else True} | ||
| # Get the matching operator | ||
| operator_callable = self._get_operator(self.operator) | ||
| # Attempt to iterate over all the values in the iterable and if any of | ||
| # them match, yield them. | ||
| # Filter versions | ||
| for version in iterable: | ||
| parsed_version = _coerce_version(version) | ||
| if self.contains(parsed_version, **kw): | ||
| # If our version is a prerelease, and we were not set to allow | ||
| # prereleases, then we'll store it for later in case nothing | ||
| # else matches this specifier. | ||
| if parsed_version.is_prerelease and not ( | ||
| prereleases or self.prereleases | ||
| if parsed_version is None: | ||
| # === operator can match arbitrary (non-version) strings | ||
| if self.operator == "===" and self._compare_arbitrary( | ||
| version, self.version | ||
| ): | ||
| found_prereleases.append(version) | ||
| # Either this is not a prerelease, or we should have been | ||
| # accepting prereleases from the beginning. | ||
| else: | ||
| yielded = True | ||
| yield version | ||
| elif operator_callable(parsed_version, self.version): | ||
| # If it's not a prerelease or prereleases are allowed, yield it directly | ||
| if not parsed_version.is_prerelease or include_prereleases: | ||
| found_non_prereleases = True | ||
| yield version | ||
| # Otherwise collect prereleases for potential later use | ||
| elif prereleases is None and self._prereleases is not False: | ||
| prereleases_versions.append(version) | ||
| # Now that we've iterated over everything, determine if we've yielded | ||
| # any values, and if we have not and we have any prereleases stored up | ||
| # then we will go ahead and yield the prereleases. | ||
| if not yielded and found_prereleases: | ||
| for version in found_prereleases: | ||
| yield version | ||
| # If no non-prereleases were found and prereleases weren't | ||
| # explicitly forbidden, yield the collected prereleases | ||
| if ( | ||
| not found_non_prereleases | ||
| and prereleases is None | ||
| and self._prereleases is not False | ||
| ): | ||
| yield from prereleases_versions | ||
| _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") | ||
| _prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)") | ||
@@ -670,3 +717,3 @@ | ||
| for item in rest.split("."): | ||
| match = _prefix_regex.search(item) | ||
| match = _prefix_regex.fullmatch(item) | ||
| if match: | ||
@@ -724,2 +771,4 @@ result.extend(match.groups()) | ||
| __slots__ = ("_prereleases", "_specs") | ||
| def __init__( | ||
@@ -778,6 +827,9 @@ self, | ||
| # prereleases, if any of them do we'll return True, otherwise False. | ||
| return any(s.prereleases for s in self._specs) | ||
| if any(s.prereleases for s in self._specs): | ||
| return True | ||
| return None | ||
| @prereleases.setter | ||
| def prereleases(self, value: bool) -> None: | ||
| def prereleases(self, value: bool | None) -> None: | ||
| self._prereleases = value | ||
@@ -842,6 +894,6 @@ | ||
| specifier._prereleases = other._prereleases | ||
| elif self._prereleases is not None and other._prereleases is None: | ||
| elif ( | ||
| self._prereleases is not None and other._prereleases is None | ||
| ) or self._prereleases == other._prereleases: | ||
| specifier._prereleases = self._prereleases | ||
| elif self._prereleases == other._prereleases: | ||
| specifier._prereleases = self._prereleases | ||
| else: | ||
@@ -909,3 +961,3 @@ raise ValueError( | ||
| >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") | ||
| False | ||
| True | ||
| >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) | ||
@@ -929,4 +981,7 @@ True | ||
| Whether or not to match prereleases with this SpecifierSet. If set to | ||
| ``None`` (the default), it uses :attr:`prereleases` to determine | ||
| whether or not prereleases are allowed. | ||
| ``None`` (the default), it will follow the recommendation from :pep:`440` | ||
| and match prereleases, as there are no other versions. | ||
| :param installed: | ||
| Whether or not the item is installed. If set to ``True``, it will | ||
| accept prerelease versions even if the specifier does not allow them. | ||
@@ -940,36 +995,16 @@ >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False).contains("1.3.0a1") | ||
| False | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) | ||
| True | ||
| """ | ||
| # Ensure that our item is a Version instance. | ||
| if not isinstance(item, Version): | ||
| item = Version(item) | ||
| version = _coerce_version(item) | ||
| # Determine if we're forcing a prerelease or not, if we're not forcing | ||
| # one for this particular filter call, then we'll use whatever the | ||
| # SpecifierSet thinks for whether or not we should support prereleases. | ||
| if prereleases is None: | ||
| prereleases = self.prereleases | ||
| if version is not None and installed and version.is_prerelease: | ||
| prereleases = True | ||
| # We can determine if we're going to allow pre-releases by looking to | ||
| # see if any of the underlying items supports them. If none of them do | ||
| # and this item is a pre-release then we do not allow it and we can | ||
| # short circuit that here. | ||
| # Note: This means that 1.0.dev1 would not be contained in something | ||
| # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 | ||
| if not prereleases and item.is_prerelease: | ||
| return False | ||
| check_item = item if version is None else version | ||
| return bool(list(self.filter([check_item], prereleases=prereleases))) | ||
| if installed and item.is_prerelease: | ||
| item = Version(item.base_version) | ||
| # We simply dispatch to the underlying specs here to make sure that the | ||
| # given version is contained within all of them. | ||
| # Note: This use of all() here means that an empty set of specifiers | ||
| # will always return True, this is an explicit design decision. | ||
| return all(s.contains(item, prereleases=prereleases) for s in self._specs) | ||
| def filter( | ||
@@ -985,10 +1020,5 @@ self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None | ||
| Whether or not to allow prereleases in the returned iterator. If set to | ||
| ``None`` (the default), it will be intelligently decide whether to allow | ||
| prereleases or not (based on the :attr:`prereleases` attribute, and | ||
| whether the only versions matching are prereleases). | ||
| ``None`` (the default), it will follow the recommendation from :pep:`440` | ||
| and match prereleases if there are no other versions. | ||
| This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` | ||
| because it implements the rule from :pep:`440` that a prerelease item | ||
| SHOULD be accepted if no other versions match the given specifier. | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) | ||
@@ -999,3 +1029,3 @@ ['1.3'] | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) | ||
| [] | ||
| ['1.5a1'] | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) | ||
@@ -1021,3 +1051,3 @@ ['1.3', '1.5a1'] | ||
| # SpecifierSet thinks for whether or not we should support prereleases. | ||
| if prereleases is None: | ||
| if prereleases is None and self.prereleases is not None: | ||
| prereleases = self.prereleases | ||
@@ -1029,28 +1059,47 @@ | ||
| if self._specs: | ||
| # When prereleases is None, we need to let all versions through | ||
| # the individual filters, then decide about prereleases at the end | ||
| # based on whether any non-prereleases matched ALL specs. | ||
| for spec in self._specs: | ||
| iterable = spec.filter(iterable, prereleases=bool(prereleases)) | ||
| return iter(iterable) | ||
| # If we do not have any specifiers, then we need to have a rough filter | ||
| # which will filter out any pre-releases, unless there are no final | ||
| # releases. | ||
| iterable = spec.filter( | ||
| iterable, prereleases=True if prereleases is None else prereleases | ||
| ) | ||
| if prereleases is not None: | ||
| # If we have a forced prereleases value, | ||
| # we can immediately return the iterator. | ||
| return iter(iterable) | ||
| else: | ||
| filtered: list[UnparsedVersionVar] = [] | ||
| found_prereleases: list[UnparsedVersionVar] = [] | ||
| # Handle empty SpecifierSet cases where prereleases is not None. | ||
| if prereleases is True: | ||
| return iter(iterable) | ||
| for item in iterable: | ||
| parsed_version = _coerce_version(item) | ||
| if prereleases is False: | ||
| return ( | ||
| item | ||
| for item in iterable | ||
| if (version := _coerce_version(item)) is None | ||
| or not version.is_prerelease | ||
| ) | ||
| # Store any item which is a pre-release for later unless we've | ||
| # already found a final version or we are accepting prereleases | ||
| if parsed_version.is_prerelease and not prereleases: | ||
| if not filtered: | ||
| found_prereleases.append(item) | ||
| else: | ||
| filtered.append(item) | ||
| # Finally if prereleases is None, apply PEP 440 logic: | ||
| # exclude prereleases unless there are no final releases that matched. | ||
| filtered_items: list[UnparsedVersionVar] = [] | ||
| found_prereleases: list[UnparsedVersionVar] = [] | ||
| found_final_release = False | ||
| # If we've found no items except for pre-releases, then we'll go | ||
| # ahead and use the pre-releases | ||
| if not filtered and found_prereleases and prereleases is None: | ||
| return iter(found_prereleases) | ||
| for item in iterable: | ||
| parsed_version = _coerce_version(item) | ||
| # Arbitrary strings are always included as it is not | ||
| # possible to determine if they are prereleases, | ||
| # and they have already passed all specifiers. | ||
| if parsed_version is None: | ||
| filtered_items.append(item) | ||
| found_prereleases.append(item) | ||
| elif parsed_version.is_prerelease: | ||
| found_prereleases.append(item) | ||
| else: | ||
| filtered_items.append(item) | ||
| found_final_release = True | ||
| return iter(filtered) | ||
| return iter(filtered_items if found_final_release else found_prereleases) |
@@ -16,2 +16,3 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| from typing import ( | ||
| Any, | ||
| Iterable, | ||
@@ -96,3 +97,10 @@ Iterator, | ||
| def __setstate__(self, state: tuple[None, dict[str, Any]]) -> None: | ||
| # The cached _hash is wrong when unpickling. | ||
| _, slots = state | ||
| for k, v in slots.items(): | ||
| setattr(self, k, v) | ||
| self._hash = hash((self._interpreter, self._abi, self._platform)) | ||
| def parse_tag(tag: str) -> frozenset[Tag]: | ||
@@ -214,6 +222,3 @@ """ | ||
| if abis is None: | ||
| if len(python_version) > 1: | ||
| abis = _cpython_abis(python_version, warn) | ||
| else: | ||
| abis = [] | ||
| abis = _cpython_abis(python_version, warn) if len(python_version) > 1 else [] | ||
| abis = list(abis) | ||
@@ -224,3 +229,3 @@ # 'abi3' and 'none' are explicitly handled later. | ||
| abis.remove(explicit_abi) | ||
| except ValueError: | ||
| except ValueError: # noqa: PERF203 | ||
| pass | ||
@@ -306,7 +311,4 @@ | ||
| interp_version = interpreter_version(warn=warn) | ||
| interpreter = "".join([interp_name, interp_version]) | ||
| if abis is None: | ||
| abis = _generic_abi() | ||
| else: | ||
| abis = list(abis) | ||
| interpreter = f"{interp_name}{interp_version}" | ||
| abis = _generic_abi() if abis is None else list(abis) | ||
| platforms = list(platforms or platform_tags()) | ||
@@ -432,10 +434,7 @@ if "none" not in abis: | ||
| version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2]))) | ||
| else: | ||
| version = version | ||
| if arch is None: | ||
| arch = _mac_arch(cpu_arch) | ||
| else: | ||
| arch = arch | ||
| if (10, 0) <= version and version < (11, 0): | ||
| if (10, 0) <= version < (11, 0): | ||
| # Prior to Mac OS 11, each yearly release of Mac OS bumped the | ||
@@ -631,7 +630,3 @@ # "minor" version number. The major version was always 10. | ||
| version = _get_config_var("py_version_nodot", warn=warn) | ||
| if version: | ||
| version = str(version) | ||
| else: | ||
| version = _version_nodot(sys.version_info[:2]) | ||
| return version | ||
| return str(version) if version else _version_nodot(sys.version_info[:2]) | ||
@@ -638,0 +633,0 @@ |
@@ -7,3 +7,2 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| import functools | ||
| import re | ||
@@ -38,7 +37,4 @@ from typing import NewType, Tuple, Union, cast | ||
| # Core metadata spec for `Name` | ||
| _validate_regex = re.compile( | ||
| r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE | ||
| ) | ||
| _canonicalize_regex = re.compile(r"[-_.]+") | ||
| _normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") | ||
| _validate_regex = re.compile(r"[A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9]", re.IGNORECASE) | ||
| _normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]") | ||
| # PEP 427: The build number must start with a digit. | ||
@@ -49,14 +45,18 @@ _build_tag_regex = re.compile(r"(\d+)(.*)") | ||
| def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: | ||
| if validate and not _validate_regex.match(name): | ||
| if validate and not _validate_regex.fullmatch(name): | ||
| raise InvalidName(f"name is invalid: {name!r}") | ||
| # This is taken from PEP 503. | ||
| value = _canonicalize_regex.sub("-", name).lower() | ||
| return cast(NormalizedName, value) | ||
| # Ensure all ``.`` and ``_`` are ``-`` | ||
| # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 | ||
| # Much faster than re, and even faster than str.translate | ||
| value = name.lower().replace("_", "-").replace(".", "-") | ||
| # Condense repeats (faster than regex) | ||
| while "--" in value: | ||
| value = value.replace("--", "-") | ||
| return cast("NormalizedName", value) | ||
| def is_normalized_name(name: str) -> bool: | ||
| return _normalized_regex.match(name) is not None | ||
| return _normalized_regex.fullmatch(name) is not None | ||
| @functools.singledispatch | ||
| def canonicalize_version( | ||
@@ -84,15 +84,10 @@ version: Version | str, *, strip_trailing_zero: bool = True | ||
| """ | ||
| return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version) | ||
| if isinstance(version, str): | ||
| try: | ||
| version = Version(version) | ||
| except InvalidVersion: | ||
| return str(version) | ||
| return str(_TrimmedRelease(version) if strip_trailing_zero else version) | ||
| @canonicalize_version.register | ||
| def _(version: str, *, strip_trailing_zero: bool = True) -> str: | ||
| try: | ||
| parsed = Version(version) | ||
| except InvalidVersion: | ||
| # Legacy versions cannot be normalized | ||
| return version | ||
| return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero) | ||
| def parse_wheel_filename( | ||
@@ -134,3 +129,3 @@ filename: str, | ||
| ) | ||
| build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) | ||
| build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2))) | ||
| else: | ||
@@ -137,0 +132,0 @@ build = () |
@@ -12,8 +12,55 @@ # This file is dual licensed under the terms of the Apache License, Version | ||
| import itertools | ||
| import re | ||
| from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union | ||
| import sys | ||
| import typing | ||
| from typing import ( | ||
| Any, | ||
| Callable, | ||
| Literal, | ||
| NamedTuple, | ||
| SupportsInt, | ||
| Tuple, | ||
| TypedDict, | ||
| Union, | ||
| ) | ||
| from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType | ||
| if typing.TYPE_CHECKING: | ||
| from typing_extensions import Self, Unpack | ||
| if sys.version_info >= (3, 13): # pragma: no cover | ||
| from warnings import deprecated as _deprecated | ||
| elif typing.TYPE_CHECKING: | ||
| from typing_extensions import deprecated as _deprecated | ||
| else: # pragma: no cover | ||
| import functools | ||
| import warnings | ||
| def _deprecated(message: str) -> object: | ||
| def decorator(func: object) -> object: | ||
| @functools.wraps(func) | ||
| def wrapper(*args: object, **kwargs: object) -> object: | ||
| warnings.warn( | ||
| message, | ||
| category=DeprecationWarning, | ||
| stacklevel=2, | ||
| ) | ||
| return func(*args, **kwargs) | ||
| return wrapper | ||
| return decorator | ||
| _LETTER_NORMALIZATION = { | ||
| "alpha": "a", | ||
| "beta": "b", | ||
| "c": "rc", | ||
| "pre": "rc", | ||
| "preview": "rc", | ||
| "rev": "post", | ||
| "r": "post", | ||
| } | ||
| __all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"] | ||
@@ -39,9 +86,9 @@ | ||
| class _Version(NamedTuple): | ||
| epoch: int | ||
| release: tuple[int, ...] | ||
| dev: tuple[str, int] | None | ||
| pre: tuple[str, int] | None | ||
| post: tuple[str, int] | None | ||
| local: LocalType | None | ||
| class _VersionReplace(TypedDict, total=False): | ||
| epoch: int | None | ||
| release: tuple[int, ...] | None | ||
| pre: tuple[Literal["a", "b", "rc"], int] | None | ||
| post: int | None | ||
| dev: int | None | ||
| local: str | None | ||
@@ -72,4 +119,12 @@ | ||
| class _BaseVersion: | ||
| _key: tuple[Any, ...] | ||
| __slots__ = () | ||
| # This can also be a normal member (see the packaging_legacy package); | ||
| # we are just requiring it to be readable. Actually defining a property | ||
| # has runtime effect on subclasses, so it's typing only. | ||
| if typing.TYPE_CHECKING: | ||
| @property | ||
| def _key(self) -> tuple[Any, ...]: ... | ||
| def __hash__(self) -> int: | ||
@@ -120,13 +175,15 @@ return hash(self._key) | ||
| # easier for 3rd party code to reuse | ||
| # Note that ++ doesn't behave identically on CPython and PyPy, so not using it here | ||
| _VERSION_PATTERN = r""" | ||
| v? | ||
| v?+ # optional leading v | ||
| (?: | ||
| (?:(?P<epoch>[0-9]+)!)? # epoch | ||
| (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment | ||
| (?:(?P<epoch>[0-9]+)!)?+ # epoch | ||
| (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment | ||
| (?P<pre> # pre-release | ||
| [-_\.]? | ||
| [._-]?+ | ||
| (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) | ||
| [-_\.]? | ||
| [._-]?+ | ||
| (?P<pre_n>[0-9]+)? | ||
| )? | ||
| )?+ | ||
| (?P<post> # post release | ||
@@ -136,19 +193,35 @@ (?:-(?P<post_n1>[0-9]+)) | ||
| (?: | ||
| [-_\.]? | ||
| [._-]? | ||
| (?P<post_l>post|rev|r) | ||
| [-_\.]? | ||
| [._-]? | ||
| (?P<post_n2>[0-9]+)? | ||
| ) | ||
| )? | ||
| )?+ | ||
| (?P<dev> # dev release | ||
| [-_\.]? | ||
| [._-]?+ | ||
| (?P<dev_l>dev) | ||
| [-_\.]? | ||
| [._-]?+ | ||
| (?P<dev_n>[0-9]+)? | ||
| )? | ||
| )?+ | ||
| ) | ||
| (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version | ||
| (?:\+ | ||
| (?P<local> # local version | ||
| [a-z0-9]+ | ||
| (?:[._-][a-z0-9]+)*+ | ||
| ) | ||
| )?+ | ||
| """ | ||
| VERSION_PATTERN = _VERSION_PATTERN | ||
| _VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?") | ||
| # Possessive qualifiers were added in Python 3.11. | ||
| # CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795 | ||
| # Older PyPy also had a bug. | ||
| VERSION_PATTERN = ( | ||
| _VERSION_PATTERN_OLD | ||
| if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5)) | ||
| or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13)) | ||
| or sys.version_info < (3, 11) | ||
| else _VERSION_PATTERN | ||
| ) | ||
| """ | ||
@@ -166,2 +239,78 @@ A string containing the regular expression used to match a valid version. | ||
| # Validation pattern for local version in replace() | ||
| _LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE) | ||
| def _validate_epoch(value: object, /) -> int: | ||
| epoch = value or 0 | ||
| if isinstance(epoch, int) and epoch >= 0: | ||
| return epoch | ||
| msg = f"epoch must be non-negative integer, got {epoch}" | ||
| raise InvalidVersion(msg) | ||
| def _validate_release(value: object, /) -> tuple[int, ...]: | ||
| release = (0,) if value is None else value | ||
| if ( | ||
| isinstance(release, tuple) | ||
| and len(release) > 0 | ||
| and all(isinstance(i, int) and i >= 0 for i in release) | ||
| ): | ||
| return release | ||
| msg = f"release must be a non-empty tuple of non-negative integers, got {release}" | ||
| raise InvalidVersion(msg) | ||
| def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None: | ||
| if value is None: | ||
| return value | ||
| if ( | ||
| isinstance(value, tuple) | ||
| and len(value) == 2 | ||
| and value[0] in ("a", "b", "rc") | ||
| and isinstance(value[1], int) | ||
| and value[1] >= 0 | ||
| ): | ||
| return value | ||
| msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}" | ||
| raise InvalidVersion(msg) | ||
| def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None: | ||
| if value is None: | ||
| return value | ||
| if isinstance(value, int) and value >= 0: | ||
| return ("post", value) | ||
| msg = f"post must be non-negative integer, got {value}" | ||
| raise InvalidVersion(msg) | ||
| def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None: | ||
| if value is None: | ||
| return value | ||
| if isinstance(value, int) and value >= 0: | ||
| return ("dev", value) | ||
| msg = f"dev must be non-negative integer, got {value}" | ||
| raise InvalidVersion(msg) | ||
| def _validate_local(value: object, /) -> LocalType | None: | ||
| if value is None: | ||
| return value | ||
| if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value): | ||
| return _parse_local_version(value) | ||
| msg = f"local must be a valid version string, got {value!r}" | ||
| raise InvalidVersion(msg) | ||
| # Backward compatibility for internals before 26.0. Do not use. | ||
| class _Version(NamedTuple): | ||
| epoch: int | ||
| release: tuple[int, ...] | ||
| dev: tuple[str, int] | None | ||
| pre: tuple[str, int] | None | ||
| post: tuple[str, int] | None | ||
| local: LocalType | None | ||
| class Version(_BaseVersion): | ||
@@ -191,5 +340,16 @@ """This class abstracts handling of a project's versions. | ||
| _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) | ||
| _key: CmpKey | ||
| __slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release") | ||
| __match_args__ = ("_str",) | ||
| _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE) | ||
| _epoch: int | ||
| _release: tuple[int, ...] | ||
| _dev: tuple[str, int] | None | ||
| _pre: tuple[str, int] | None | ||
| _post: tuple[str, int] | None | ||
| _local: LocalType | None | ||
| _key_cache: CmpKey | None | ||
| def __init__(self, version: str) -> None: | ||
@@ -205,30 +365,82 @@ """Initialize a Version object. | ||
| """ | ||
| # Validate the version and parse it into pieces | ||
| match = self._regex.search(version) | ||
| match = self._regex.fullmatch(version) | ||
| if not match: | ||
| raise InvalidVersion(f"Invalid version: {version!r}") | ||
| self._epoch = int(match.group("epoch")) if match.group("epoch") else 0 | ||
| self._release = tuple(map(int, match.group("release").split("."))) | ||
| self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) | ||
| self._post = _parse_letter_version( | ||
| match.group("post_l"), match.group("post_n1") or match.group("post_n2") | ||
| ) | ||
| self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) | ||
| self._local = _parse_local_version(match.group("local")) | ||
| # Store the parsed out pieces of the version | ||
| self._version = _Version( | ||
| epoch=int(match.group("epoch")) if match.group("epoch") else 0, | ||
| release=tuple(int(i) for i in match.group("release").split(".")), | ||
| pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), | ||
| post=_parse_letter_version( | ||
| match.group("post_l"), match.group("post_n1") or match.group("post_n2") | ||
| ), | ||
| dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), | ||
| local=_parse_local_version(match.group("local")), | ||
| # Key which will be used for sorting | ||
| self._key_cache = None | ||
| def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self: | ||
| epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch | ||
| release = ( | ||
| _validate_release(kwargs["release"]) | ||
| if "release" in kwargs | ||
| else self._release | ||
| ) | ||
| pre = _validate_pre(kwargs["pre"]) if "pre" in kwargs else self._pre | ||
| post = _validate_post(kwargs["post"]) if "post" in kwargs else self._post | ||
| dev = _validate_dev(kwargs["dev"]) if "dev" in kwargs else self._dev | ||
| local = _validate_local(kwargs["local"]) if "local" in kwargs else self._local | ||
| # Generate a key which will be used for sorting | ||
| self._key = _cmpkey( | ||
| self._version.epoch, | ||
| self._version.release, | ||
| self._version.pre, | ||
| self._version.post, | ||
| self._version.dev, | ||
| self._version.local, | ||
| if ( | ||
| epoch == self._epoch | ||
| and release == self._release | ||
| and pre == self._pre | ||
| and post == self._post | ||
| and dev == self._dev | ||
| and local == self._local | ||
| ): | ||
| return self | ||
| new_version = self.__class__.__new__(self.__class__) | ||
| new_version._key_cache = None | ||
| new_version._epoch = epoch | ||
| new_version._release = release | ||
| new_version._pre = pre | ||
| new_version._post = post | ||
| new_version._dev = dev | ||
| new_version._local = local | ||
| return new_version | ||
| @property | ||
| def _key(self) -> CmpKey: | ||
| if self._key_cache is None: | ||
| self._key_cache = _cmpkey( | ||
| self._epoch, | ||
| self._release, | ||
| self._pre, | ||
| self._post, | ||
| self._dev, | ||
| self._local, | ||
| ) | ||
| return self._key_cache | ||
| @property | ||
| @_deprecated("Version._version is private and will be removed soon") | ||
| def _version(self) -> _Version: | ||
| return _Version( | ||
| self._epoch, self._release, self._dev, self._pre, self._post, self._local | ||
| ) | ||
| @_version.setter | ||
| @_deprecated("Version._version is private and will be removed soon") | ||
| def _version(self, value: _Version) -> None: | ||
| self._epoch = value.epoch | ||
| self._release = value.release | ||
| self._dev = value.dev | ||
| self._pre = value.pre | ||
| self._post = value.post | ||
| self._local = value.local | ||
| self._key_cache = None | ||
| def __repr__(self) -> str: | ||
@@ -248,30 +460,33 @@ """A representation of the Version that shows all internal state. | ||
| """ | ||
| parts = [] | ||
| # This is a hot function, so not calling self.base_version | ||
| version = ".".join(map(str, self.release)) | ||
| # Epoch | ||
| if self.epoch != 0: | ||
| parts.append(f"{self.epoch}!") | ||
| if self.epoch: | ||
| version = f"{self.epoch}!{version}" | ||
| # Release segment | ||
| parts.append(".".join(str(x) for x in self.release)) | ||
| # Pre-release | ||
| if self.pre is not None: | ||
| parts.append("".join(str(x) for x in self.pre)) | ||
| version += "".join(map(str, self.pre)) | ||
| # Post-release | ||
| if self.post is not None: | ||
| parts.append(f".post{self.post}") | ||
| version += f".post{self.post}" | ||
| # Development release | ||
| if self.dev is not None: | ||
| parts.append(f".dev{self.dev}") | ||
| version += f".dev{self.dev}" | ||
| # Local version segment | ||
| if self.local is not None: | ||
| parts.append(f"+{self.local}") | ||
| version += f"+{self.local}" | ||
| return "".join(parts) | ||
| return version | ||
| @property | ||
| def _str(self) -> str: | ||
| """Internal property for match_args""" | ||
| return str(self) | ||
| @property | ||
| def epoch(self) -> int: | ||
@@ -285,3 +500,3 @@ """The epoch of the version. | ||
| """ | ||
| return self._version.epoch | ||
| return self._epoch | ||
@@ -302,3 +517,3 @@ @property | ||
| """ | ||
| return self._version.release | ||
| return self._release | ||
@@ -318,3 +533,3 @@ @property | ||
| """ | ||
| return self._version.pre | ||
| return self._pre | ||
@@ -330,3 +545,3 @@ @property | ||
| """ | ||
| return self._version.post[1] if self._version.post else None | ||
| return self._post[1] if self._post else None | ||
@@ -342,3 +557,3 @@ @property | ||
| """ | ||
| return self._version.dev[1] if self._version.dev else None | ||
| return self._dev[1] if self._dev else None | ||
@@ -354,4 +569,4 @@ @property | ||
| """ | ||
| if self._version.local: | ||
| return ".".join(str(x) for x in self._version.local) | ||
| if self._local: | ||
| return ".".join(str(x) for x in self._local) | ||
| else: | ||
@@ -387,13 +602,5 @@ return None | ||
| """ | ||
| parts = [] | ||
| release_segment = ".".join(map(str, self.release)) | ||
| return f"{self.epoch}!{release_segment}" if self.epoch else release_segment | ||
| # Epoch | ||
| if self.epoch != 0: | ||
| parts.append(f"{self.epoch}!") | ||
| # Release segment | ||
| parts.append(".".join(str(x) for x in self.release)) | ||
| return "".join(parts) | ||
| @property | ||
@@ -471,2 +678,16 @@ def is_prerelease(self) -> bool: | ||
| class _TrimmedRelease(Version): | ||
| __slots__ = () | ||
| def __init__(self, version: str | Version) -> None: | ||
| if isinstance(version, Version): | ||
| self._epoch = version._epoch | ||
| self._release = version._release | ||
| self._dev = version._dev | ||
| self._pre = version._pre | ||
| self._post = version._post | ||
| self._local = version._local | ||
| self._key_cache = version._key_cache | ||
| return | ||
| super().__init__(version) # pragma: no cover | ||
| @property | ||
@@ -482,6 +703,9 @@ def release(self) -> tuple[int, ...]: | ||
| """ | ||
| # This leaves one 0. | ||
| rel = super().release | ||
| nonzeros = (index for index, val in enumerate(rel) if val) | ||
| last_nonzero = max(nonzeros, default=0) | ||
| return rel[: last_nonzero + 1] | ||
| len_release = len(rel) | ||
| i = len_release | ||
| while i > 1 and rel[i - 1] == 0: | ||
| i -= 1 | ||
| return rel if i == len_release else rel[:i] | ||
@@ -493,7 +717,2 @@ | ||
| if letter: | ||
| # We consider there to be an implicit 0 in a pre-release if there is | ||
| # not a numeral associated with it. | ||
| if number is None: | ||
| number = 0 | ||
| # We normalize any letters to their lower case form | ||
@@ -505,21 +724,13 @@ letter = letter.lower() | ||
| # spelling. | ||
| if letter == "alpha": | ||
| letter = "a" | ||
| elif letter == "beta": | ||
| letter = "b" | ||
| elif letter in ["c", "pre", "preview"]: | ||
| letter = "rc" | ||
| elif letter in ["rev", "r"]: | ||
| letter = "post" | ||
| letter = _LETTER_NORMALIZATION.get(letter, letter) | ||
| return letter, int(number) | ||
| # We consider there to be an implicit 0 in a pre-release if there is | ||
| # not a numeral associated with it. | ||
| return letter, int(number or 0) | ||
| assert not letter | ||
| if number: | ||
| # We assume if we are given a number, but we are not given a letter | ||
| # then this is using the implicit post release syntax (e.g. 1.0-1) | ||
| letter = "post" | ||
| return "post", int(number) | ||
| return letter, int(number) | ||
| return None | ||
@@ -552,9 +763,8 @@ | ||
| # When we compare a release version, we want to compare it with all of the | ||
| # trailing zeros removed. So we'll use a reverse the list, drop all the now | ||
| # leading zeros until we come to something non zero, then take the rest | ||
| # re-reverse it back into the correct order and make it a tuple and use | ||
| # that for our sorting key. | ||
| _release = tuple( | ||
| reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) | ||
| ) | ||
| # trailing zeros removed. We will use this for our sorting key. | ||
| len_release = len(release) | ||
| i = len_release | ||
| while i and release[i - 1] == 0: | ||
| i -= 1 | ||
| _release = release if i == len_release else release[:i] | ||
@@ -561,0 +771,0 @@ # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. |
| from __future__ import annotations | ||
| __version__ = "0.45.1" | ||
| __version__ = "0.46.3" |
| """ | ||
| Wheel command line tool (enable python -m wheel syntax) | ||
| Wheel command line tool (enables the ``python -m wheel`` syntax) | ||
| """ | ||
@@ -8,5 +8,6 @@ | ||
| import sys | ||
| from typing import NoReturn | ||
| def main(): # needed for console script | ||
| def main() -> NoReturn: # needed for console script | ||
| if __package__ == "": | ||
@@ -18,8 +19,9 @@ # To be able to run 'python wheel-0.9.whl/wheel': | ||
| sys.path[0:0] = [path] | ||
| import wheel.cli | ||
| sys.exit(wheel.cli.main()) | ||
| from ._commands import main as cli_main | ||
| sys.exit(cli_main()) | ||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
| main() |
@@ -9,2 +9,3 @@ """ | ||
| import logging | ||
| import os | ||
@@ -18,2 +19,3 @@ import re | ||
| import warnings | ||
| from collections.abc import Iterable, Sequence | ||
| from email.generator import BytesGenerator, Generator | ||
@@ -23,13 +25,12 @@ from email.policy import EmailPolicy | ||
| from shutil import rmtree | ||
| from typing import TYPE_CHECKING, Callable, Iterable, Literal, Sequence, cast | ||
| from typing import TYPE_CHECKING, Callable, Literal, cast | ||
| from zipfile import ZIP_DEFLATED, ZIP_STORED | ||
| import setuptools | ||
| from packaging import tags | ||
| from packaging import version as _packaging_version | ||
| from setuptools import Command | ||
| from . import __version__ as wheel_version | ||
| from .metadata import pkginfo_to_metadata | ||
| from .util import log | ||
| from .vendored.packaging import tags | ||
| from .vendored.packaging import version as _packaging_version | ||
| from ._metadata import pkginfo_to_metadata | ||
| from .wheelfile import WheelFile | ||
@@ -49,3 +50,5 @@ | ||
| log = logging.getLogger("wheel") | ||
| def safe_name(name: str) -> str: | ||
@@ -377,5 +380,5 @@ """Convert an arbitrary string to a standard distribution name | ||
| ] | ||
| assert ( | ||
| tag in supported_tags | ||
| ), f"would build wheel with unsupported tag {tag}" | ||
| assert tag in supported_tags, ( | ||
| f"would build wheel with unsupported tag {tag}" | ||
| ) | ||
| return tag | ||
@@ -382,0 +385,0 @@ |
@@ -8,3 +8,3 @@ from typing import TYPE_CHECKING | ||
| "v70.1 or later which contains an integrated version of this command.", | ||
| DeprecationWarning, | ||
| FutureWarning, | ||
| stacklevel=1, | ||
@@ -11,0 +11,0 @@ ) |
| """ | ||
| IMPORTANT: DO NOT IMPORT THIS MODULE DIRECTLY. | ||
| THIS IS ONLY KEPT IN PLACE FOR BACKWARDS COMPATIBILITY WITH | ||
| setuptools.command.bdist_wheel. | ||
| This module contains function to analyse dynamic library | ||
@@ -3,0 +7,0 @@ headers to extract system information |
@@ -1,183 +0,17 @@ | ||
| """ | ||
| Tools for converting old- to new-style metadata. | ||
| """ | ||
| from warnings import warn | ||
| from __future__ import annotations | ||
| from ._metadata import convert_requirements as convert_requirements | ||
| from ._metadata import generate_requirements as generate_requirements | ||
| from ._metadata import pkginfo_to_metadata as pkginfo_to_metadata | ||
| from ._metadata import requires_to_requires_dist as requires_to_requires_dist | ||
| from ._metadata import safe_extra as safe_extra | ||
| from ._metadata import safe_name as safe_name | ||
| from ._metadata import split_sections as split_sections | ||
| import functools | ||
| import itertools | ||
| import os.path | ||
| import re | ||
| import textwrap | ||
| from email.message import Message | ||
| from email.parser import Parser | ||
| from typing import Generator, Iterable, Iterator, Literal | ||
| from .vendored.packaging.requirements import Requirement | ||
| def _nonblank(str: str) -> bool | Literal[""]: | ||
| return str and not str.startswith("#") | ||
| @functools.singledispatch | ||
| def yield_lines(iterable: Iterable[str]) -> Iterator[str]: | ||
| r""" | ||
| Yield valid lines of a string or iterable. | ||
| >>> list(yield_lines('')) | ||
| [] | ||
| >>> list(yield_lines(['foo', 'bar'])) | ||
| ['foo', 'bar'] | ||
| >>> list(yield_lines('foo\nbar')) | ||
| ['foo', 'bar'] | ||
| >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) | ||
| ['foo', 'baz #comment'] | ||
| >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) | ||
| ['foo', 'bar', 'baz', 'bing'] | ||
| """ | ||
| return itertools.chain.from_iterable(map(yield_lines, iterable)) | ||
| @yield_lines.register(str) | ||
| def _(text: str) -> Iterator[str]: | ||
| return filter(_nonblank, map(str.strip, text.splitlines())) | ||
| def split_sections( | ||
| s: str | Iterator[str], | ||
| ) -> Generator[tuple[str | None, list[str]], None, None]: | ||
| """Split a string or iterable thereof into (section, content) pairs | ||
| Each ``section`` is a stripped version of the section header ("[section]") | ||
| and each ``content`` is a list of stripped lines excluding blank lines and | ||
| comment-only lines. If there are any such lines before the first section | ||
| header, they're returned in a first ``section`` of ``None``. | ||
| """ | ||
| section = None | ||
| content: list[str] = [] | ||
| for line in yield_lines(s): | ||
| if line.startswith("["): | ||
| if line.endswith("]"): | ||
| if section or content: | ||
| yield section, content | ||
| section = line[1:-1].strip() | ||
| content = [] | ||
| else: | ||
| raise ValueError("Invalid section heading", line) | ||
| else: | ||
| content.append(line) | ||
| # wrap up last segment | ||
| yield section, content | ||
| def safe_extra(extra: str) -> str: | ||
| """Convert an arbitrary string to a standard 'extra' name | ||
| Any runs of non-alphanumeric characters are replaced with a single '_', | ||
| and the result is always lowercased. | ||
| """ | ||
| return re.sub("[^A-Za-z0-9.-]+", "_", extra).lower() | ||
| def safe_name(name: str) -> str: | ||
| """Convert an arbitrary string to a standard distribution name | ||
| Any runs of non-alphanumeric/. characters are replaced with a single '-'. | ||
| """ | ||
| return re.sub("[^A-Za-z0-9.]+", "-", name) | ||
| def requires_to_requires_dist(requirement: Requirement) -> str: | ||
| """Return the version specifier for a requirement in PEP 345/566 fashion.""" | ||
| if requirement.url: | ||
| return " @ " + requirement.url | ||
| requires_dist: list[str] = [] | ||
| for spec in requirement.specifier: | ||
| requires_dist.append(spec.operator + spec.version) | ||
| if requires_dist: | ||
| return " " + ",".join(sorted(requires_dist)) | ||
| else: | ||
| return "" | ||
| def convert_requirements(requirements: list[str]) -> Iterator[str]: | ||
| """Yield Requires-Dist: strings for parsed requirements strings.""" | ||
| for req in requirements: | ||
| parsed_requirement = Requirement(req) | ||
| spec = requires_to_requires_dist(parsed_requirement) | ||
| extras = ",".join(sorted(safe_extra(e) for e in parsed_requirement.extras)) | ||
| if extras: | ||
| extras = f"[{extras}]" | ||
| yield safe_name(parsed_requirement.name) + extras + spec | ||
| def generate_requirements( | ||
| extras_require: dict[str | None, list[str]], | ||
| ) -> Iterator[tuple[str, str]]: | ||
| """ | ||
| Convert requirements from a setup()-style dictionary to | ||
| ('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples. | ||
| extras_require is a dictionary of {extra: [requirements]} as passed to setup(), | ||
| using the empty extra {'': [requirements]} to hold install_requires. | ||
| """ | ||
| for extra, depends in extras_require.items(): | ||
| condition = "" | ||
| extra = extra or "" | ||
| if ":" in extra: # setuptools extra:condition syntax | ||
| extra, condition = extra.split(":", 1) | ||
| extra = safe_extra(extra) | ||
| if extra: | ||
| yield "Provides-Extra", extra | ||
| if condition: | ||
| condition = "(" + condition + ") and " | ||
| condition += f"extra == '{extra}'" | ||
| if condition: | ||
| condition = " ; " + condition | ||
| for new_req in convert_requirements(depends): | ||
| canonical_req = str(Requirement(new_req + condition)) | ||
| yield "Requires-Dist", canonical_req | ||
| def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message: | ||
| """ | ||
| Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format | ||
| """ | ||
| with open(pkginfo_path, encoding="utf-8") as headers: | ||
| pkg_info = Parser().parse(headers) | ||
| pkg_info.replace_header("Metadata-Version", "2.1") | ||
| # Those will be regenerated from `requires.txt`. | ||
| del pkg_info["Provides-Extra"] | ||
| del pkg_info["Requires-Dist"] | ||
| requires_path = os.path.join(egg_info_path, "requires.txt") | ||
| if os.path.exists(requires_path): | ||
| with open(requires_path, encoding="utf-8") as requires_file: | ||
| requires = requires_file.read() | ||
| parsed_requirements = sorted(split_sections(requires), key=lambda x: x[0] or "") | ||
| for extra, reqs in parsed_requirements: | ||
| for key, value in generate_requirements({extra: reqs}): | ||
| if (key, value) not in pkg_info.items(): | ||
| pkg_info[key] = value | ||
| description = pkg_info["Description"] | ||
| if description: | ||
| description_lines = pkg_info["Description"].splitlines() | ||
| dedented_description = "\n".join( | ||
| # if the first line of long_description is blank, | ||
| # the first line here will be indented. | ||
| ( | ||
| description_lines[0].lstrip(), | ||
| textwrap.dedent("\n".join(description_lines[1:])), | ||
| "\n", | ||
| ) | ||
| ) | ||
| pkg_info.set_payload(dedented_description) | ||
| del pkg_info["Description"] | ||
| return pkg_info | ||
| warn( | ||
| f"The {__name__!r} package has been made private and should no longer be imported. " | ||
| f"Please either copy the code or find an alternative library to import it from, as " | ||
| f"this warning will be removed in a future version of 'wheel'.", | ||
| DeprecationWarning, | ||
| stacklevel=2, | ||
| ) |
| from __future__ import annotations | ||
| __all__ = ["WHEEL_INFO_RE", "WheelFile", "WheelError"] | ||
| import base64 | ||
| import csv | ||
| import hashlib | ||
| import logging | ||
| import os.path | ||
@@ -13,15 +17,6 @@ import re | ||
| from wheel.cli import WheelError | ||
| from wheel.util import log, urlsafe_b64decode, urlsafe_b64encode | ||
| if TYPE_CHECKING: | ||
| from typing import Protocol, Sized, Union | ||
| from _typeshed import SizedBuffer, StrPath | ||
| from typing_extensions import Buffer | ||
| StrPath = Union[str, os.PathLike[str]] | ||
| class SizedBuffer(Sized, Buffer, Protocol): ... | ||
| # Non-greedy matching of an optional build number may be too clever (more | ||
@@ -36,4 +31,23 @@ # invalid wheel filenames will match). Separate regex for .dist-info? | ||
| log = logging.getLogger("wheel") | ||
| def get_zipinfo_datetime(timestamp: float | None = None): | ||
| class WheelError(Exception): | ||
| pass | ||
| def urlsafe_b64encode(data: bytes) -> bytes: | ||
| """urlsafe_b64encode without padding""" | ||
| return base64.urlsafe_b64encode(data).rstrip(b"=") | ||
| def urlsafe_b64decode(data: bytes) -> bytes: | ||
| """urlsafe_b64decode without padding""" | ||
| pad = b"=" * (4 - (len(data) & 3)) | ||
| return base64.urlsafe_b64decode(data + pad) | ||
| def get_zipinfo_datetime( | ||
| timestamp: float | None = None, | ||
| ) -> tuple[int, int, int, int, int]: | ||
| # Some applications need reproducible .whl files, but they can't do this without | ||
@@ -145,4 +159,4 @@ # forcing the timestamp of the individual ZipInfo objects. See issue #143. | ||
| def write_files(self, base_dir: str): | ||
| log.info(f"creating '{self.filename}' and adding '{base_dir}' to it") | ||
| def write_files(self, base_dir: str) -> None: | ||
| log.info("creating %r and adding %r to it", self.filename, base_dir) | ||
| deferred: list[tuple[str, str]] = [] | ||
@@ -190,3 +204,3 @@ for root, dirnames, filenames in os.walk(base_dir): | ||
| compress_type: int | None = None, | ||
| ): | ||
| ) -> None: | ||
| if isinstance(zinfo_or_arcname, str): | ||
@@ -208,3 +222,3 @@ zinfo_or_arcname = ZipInfo( | ||
| ) | ||
| log.info(f"adding '{fname}'") | ||
| log.info("adding %r", fname) | ||
| if fname != self.record_path: | ||
@@ -218,3 +232,3 @@ hash_ = self._default_algorithm(data) | ||
| def close(self): | ||
| def close(self) -> None: | ||
| # Write RECORD | ||
@@ -221,0 +235,0 @@ if self.fp is not None and self.mode == "w" and self._file_hashes: |
@@ -105,3 +105,3 @@ """Translation layer between pyproject config and setuptools distribution and | ||
| see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", | ||
| due_date=(2026, 2, 18), # Warning introduced on 2025-02-18 | ||
| due_date=(2027, 2, 18), # Warning introduced on 2025-02-18 | ||
| ) | ||
@@ -218,3 +218,3 @@ | ||
| see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", | ||
| due_date=(2026, 2, 18), # Introduced on 2025-02-18 | ||
| due_date=(2027, 2, 18), # Introduced on 2025-02-18 | ||
| ) | ||
@@ -221,0 +221,0 @@ if "file" in val: |
@@ -515,3 +515,3 @@ from __future__ import annotations | ||
| see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", | ||
| due_date=(2026, 3, 20), # Introduced in 2025-03-20 | ||
| due_date=(2027, 2, 18), # Introduced in 2025-03-20 | ||
| # Replace with InvalidConfigError after deprecation | ||
@@ -529,3 +529,3 @@ ) | ||
| see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", | ||
| due_date=(2026, 3, 20), # Introduced in 2025-02-20 | ||
| due_date=(2027, 2, 18), # Introduced in 2025-02-20 | ||
| ) | ||
@@ -540,3 +540,3 @@ | ||
| pattern=pattern, | ||
| due_date=(2026, 3, 20), # Introduced in 2025-02-20 | ||
| due_date=(2027, 2, 18), # Introduced in 2025-02-20 | ||
| # PEP 639 requires us to error, but as a transition period | ||
@@ -543,0 +543,0 @@ # we will only issue a warning to give people time to prepare. |
@@ -73,4 +73,6 @@ import functools | ||
| install_deps(deps, vendor) | ||
| # uv only installs non-portable shebangs (#5159) | ||
| (vendor / 'bin').rmtree() | ||
| __name__ == '__main__' and update_vendored() |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| This software is made available under the terms of *either* of the licenses | ||
| found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made | ||
| under the terms of *both* these licenses. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| Metadata-Version: 2.4 | ||
| Name: packaging | ||
| Version: 25.0 | ||
| Summary: Core utilities for Python packages | ||
| Author-email: Donald Stufft <donald@stufft.io> | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/x-rst | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: License :: OSI Approved :: Apache Software License | ||
| Classifier: License :: OSI Approved :: BSD License | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| 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 | ||
| License-File: LICENSE | ||
| License-File: LICENSE.APACHE | ||
| License-File: LICENSE.BSD | ||
| Project-URL: Documentation, https://packaging.pypa.io/ | ||
| Project-URL: Source, https://github.com/pypa/packaging | ||
| packaging | ||
| ========= | ||
| .. start-intro | ||
| Reusable core utilities for various Python Packaging | ||
| `interoperability specifications <https://packaging.python.org/specifications/>`_. | ||
| This library provides utilities that implement the interoperability | ||
| specifications which have clearly one correct behaviour (eg: :pep:`440`) | ||
| or benefit greatly from having a single shared implementation (eg: :pep:`425`). | ||
| .. end-intro | ||
| The ``packaging`` project includes the following: version handling, specifiers, | ||
| markers, requirements, tags, utilities. | ||
| Documentation | ||
| ------------- | ||
| The `documentation`_ provides information and the API for the following: | ||
| - Version Handling | ||
| - Specifiers | ||
| - Markers | ||
| - Requirements | ||
| - Tags | ||
| - Utilities | ||
| Installation | ||
| ------------ | ||
| Use ``pip`` to install these utilities:: | ||
| pip install packaging | ||
| The ``packaging`` library uses calendar-based versioning (``YY.N``). | ||
| Discussion | ||
| ---------- | ||
| If you run into bugs, you can file them in our `issue tracker`_. | ||
| You can also join ``#pypa`` on Freenode to ask questions or get involved. | ||
| .. _`documentation`: https://packaging.pypa.io/ | ||
| .. _`issue tracker`: https://github.com/pypa/packaging/issues | ||
| Code of Conduct | ||
| --------------- | ||
| Everyone interacting in the packaging project's codebases, issue trackers, chat | ||
| rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. | ||
| .. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md | ||
| Contributing | ||
| ------------ | ||
| The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as | ||
| well as how to report a potential security issue. The documentation for this | ||
| project also covers information about `project development`_ and `security`_. | ||
| .. _`project development`: https://packaging.pypa.io/en/latest/development/ | ||
| .. _`security`: https://packaging.pypa.io/en/latest/security/ | ||
| Project History | ||
| --------------- | ||
| Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for | ||
| recent changes and project history. | ||
| .. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ | ||
| packaging-25.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 | ||
| packaging-25.0.dist-info/METADATA,sha256=W2EaYJw4_vw9YWv0XSCuyY-31T8kXayp4sMPyFx6woI,3281 | ||
| packaging-25.0.dist-info/RECORD,, | ||
| packaging-25.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| packaging-25.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 | ||
| packaging-25.0.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 | ||
| packaging-25.0.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 | ||
| packaging-25.0.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 | ||
| packaging/__init__.py,sha256=_0cDiPVf2S-bNfVmZguxxzmrIYWlyASxpqph4qsJWUc,494 | ||
| packaging/_elffile.py,sha256=UkrbDtW7aeq3qqoAfU16ojyHZ1xsTvGke_WqMTKAKd0,3286 | ||
| packaging/_manylinux.py,sha256=t4y_-dTOcfr36gLY-ztiOpxxJFGO2ikC11HgfysGxiM,9596 | ||
| packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694 | ||
| packaging/_parser.py,sha256=gYfnj0pRHflVc4RHZit13KNTyN9iiVcU2RUCGi22BwM,10221 | ||
| packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 | ||
| packaging/_tokenizer.py,sha256=OYzt7qKxylOAJ-q0XyK1qAycyPRYLfMPdGQKRXkZWyI,5310 | ||
| packaging/licenses/__init__.py,sha256=VsK4o27CJXWfTi8r2ybJmsBoCdhpnBWuNrskaCVKP7U,5715 | ||
| packaging/licenses/_spdx.py,sha256=oAm1ztPFwlsmCKe7lAAsv_OIOfS1cWDu9bNBkeu-2ns,48398 | ||
| packaging/markers.py,sha256=P0we27jm1xUzgGMJxBjtUFCIWeBxTsMeJTOJ6chZmAY,12049 | ||
| packaging/metadata.py,sha256=8IZErqQQnNm53dZZuYq4FGU4_dpyinMeH1QFBIWIkfE,34739 | ||
| packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947 | ||
| packaging/specifiers.py,sha256=gtPu5DTc-F9baLq3FTGEK6dPhHGCuwwZetaY0PSV2gs,40055 | ||
| packaging/tags.py,sha256=41s97W9Zatrq2Ed7Rc3qeBDaHe8pKKvYq2mGjwahfXk,22745 | ||
| packaging/utils.py,sha256=0F3Hh9OFuRgrhTgGZUl5K22Fv1YP2tZl1z_2gO6kJiA,5050 | ||
| packaging/version.py,sha256=olfyuk_DPbflNkJ4wBWetXQ17c74x3DB501degUv7DY,16676 |
Sorry, the diff of this file is not supported yet
| Wheel-Version: 1.0 | ||
| Generator: flit 3.12.0 | ||
| Root-Is-Purelib: true | ||
| Tag: py3-none-any |
| [console_scripts] | ||
| wheel=wheel.cli:main | ||
| [distutils.commands] | ||
| bdist_wheel=wheel.bdist_wheel:bdist_wheel | ||
Sorry, the diff of this file is not supported yet
| MIT License | ||
| Copyright (c) 2012 Daniel Holth <dholth@fastmail.fm> and contributors | ||
| Permission is hereby granted, free of charge, to any person obtaining a | ||
| copy of this software and associated documentation files (the "Software"), | ||
| to deal in the Software without restriction, including without limitation | ||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||
| and/or sell copies of the Software, and to permit persons to whom the | ||
| Software is furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included | ||
| in all copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | ||
| THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR | ||
| OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | ||
| ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||
| OTHER DEALINGS IN THE SOFTWARE. |
| Metadata-Version: 2.3 | ||
| Name: wheel | ||
| Version: 0.45.1 | ||
| Summary: A built-package format for Python | ||
| Keywords: wheel,packaging | ||
| Author-email: Daniel Holth <dholth@fastmail.fm> | ||
| Maintainer-email: Alex Grönholm <alex.gronholm@nextday.fi> | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/x-rst | ||
| Classifier: Development Status :: 5 - Production/Stable | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: Topic :: System :: Archiving :: Packaging | ||
| Classifier: License :: OSI Approved :: MIT License | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| 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 | ||
| Requires-Dist: pytest >= 6.0.0 ; extra == "test" | ||
| Requires-Dist: setuptools >= 65 ; extra == "test" | ||
| Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html | ||
| Project-URL: Documentation, https://wheel.readthedocs.io/ | ||
| Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues | ||
| Project-URL: Source, https://github.com/pypa/wheel | ||
| Provides-Extra: test | ||
| wheel | ||
| ===== | ||
| This is a command line tool for manipulating Python wheel files, as defined in | ||
| `PEP 427`_. It contains the following functionality: | ||
| * Convert ``.egg`` archives into ``.whl`` | ||
| * Unpack wheel archives | ||
| * Repack wheel archives | ||
| * Add or remove tags in existing wheel archives | ||
| .. _PEP 427: https://www.python.org/dev/peps/pep-0427/ | ||
| Historical note | ||
| --------------- | ||
| This project used to contain the implementation of the setuptools_ ``bdist_wheel`` | ||
| command, but as of setuptools v70.1, it no longer needs ``wheel`` installed for that to | ||
| work. Thus, you should install this **only** if you intend to use the ``wheel`` command | ||
| line tool! | ||
| .. _setuptools: https://pypi.org/project/setuptools/ | ||
| Documentation | ||
| ------------- | ||
| The documentation_ can be found on Read The Docs. | ||
| .. _documentation: https://wheel.readthedocs.io/ | ||
| Code of Conduct | ||
| --------------- | ||
| Everyone interacting in the wheel project's codebases, issue trackers, chat | ||
| rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. | ||
| .. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md | ||
| bin/wheel,sha256=7ijYGJ2HkBAM2Ws5-L6nCHhN-DASp95DgxlQWfqY51A,333 | ||
| wheel-0.45.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 | ||
| wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107 | ||
| wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313 | ||
| wheel-0.45.1.dist-info/RECORD,, | ||
| wheel-0.45.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| wheel-0.45.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 | ||
| wheel-0.45.1.dist-info/entry_points.txt,sha256=rTY1BbkPHhkGMm4Q3F0pIzJBzW2kMxoG1oriffvGdA0,104 | ||
| wheel/__init__.py,sha256=mrxMnvdXACur_LWegbUfh5g5ysWZrd63UJn890wvGNk,59 | ||
| wheel/__main__.py,sha256=NkMUnuTCGcOkgY0IBLgBCVC_BGGcWORx2K8jYGS12UE,455 | ||
| wheel/_bdist_wheel.py,sha256=UghCQjSH_pVfcZh6oRjzSw_TQhcf3anSx1OkiLSL82M,21694 | ||
| wheel/_setuptools_logging.py,sha256=-5KC-lne0ilOUWIDfOkqapUWGMFZhuKYDIavIZiB5kM,781 | ||
| wheel/bdist_wheel.py,sha256=tpf9WufiSO1RuEMg5oPhIfSG8DMziCZ_4muCKF69Cqo,1107 | ||
| wheel/cli/__init__.py,sha256=Npq6_jKi03dhIcRnmbuFhwviVJxwO0tYEnEhWMv9cJo,4402 | ||
| wheel/cli/convert.py,sha256=Bi0ntEXb9nTllCxWeTRQ4j-nPs3szWSEKipG_GgnMkQ,12634 | ||
| wheel/cli/pack.py,sha256=CAFcHdBVulvsHYJlndKVO7KMI9JqBTZz5ii0PKxxCOs,3103 | ||
| wheel/cli/tags.py,sha256=lHw-LaWrkS5Jy_qWcw-6pSjeNM6yAjDnqKI3E5JTTCU,4760 | ||
| wheel/cli/unpack.py,sha256=Y_J7ynxPSoFFTT7H0fMgbBlVErwyDGcObgme5MBuz58,1021 | ||
| wheel/macosx_libfile.py,sha256=k1x7CE3LPtOVGqj6NXQ1nTGYVPaeRrhVzUG_KPq3zDs,16572 | ||
| wheel/metadata.py,sha256=JC4p7jlQZu2bUTAQ2fevkqLjg_X6gnNyRhLn6OUO1tc,6171 | ||
| wheel/util.py,sha256=aL7aibHwYUgfc8WlolL5tXdkV4DatbJxZHb1kwHFJAU,423 | ||
| wheel/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| wheel/vendored/packaging/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 | ||
| wheel/vendored/packaging/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 | ||
| wheel/vendored/packaging/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 | ||
| wheel/vendored/packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| wheel/vendored/packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 | ||
| wheel/vendored/packaging/_manylinux.py,sha256=P7sdR5_7XBY09LVYYPhHmydMJIIwPXWsh4olk74Uuj4,9588 | ||
| wheel/vendored/packaging/_musllinux.py,sha256=z1s8To2hQ0vpn_d-O2i5qxGwEK8WmGlLt3d_26V7NeY,2674 | ||
| wheel/vendored/packaging/_parser.py,sha256=4tT4emSl2qTaU7VTQE1Xa9o1jMPCsBezsYBxyNMUN-s,10347 | ||
| wheel/vendored/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 | ||
| wheel/vendored/packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292 | ||
| wheel/vendored/packaging/markers.py,sha256=_TSPI1BhJYO7Bp9AzTmHQxIqHEVXaTjmDh9G-w8qzPA,8232 | ||
| wheel/vendored/packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933 | ||
| wheel/vendored/packaging/specifiers.py,sha256=IWSt0SrLSP72heWhAC8UL0eGvas7XIQHjqiViVfmPKE,39778 | ||
| wheel/vendored/packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950 | ||
| wheel/vendored/packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268 | ||
| wheel/vendored/packaging/version.py,sha256=PFJaYZDxBgyxkfYhH3SQw4qfE9ICCWrTmitvq14y3bs,16234 | ||
| wheel/vendored/vendor.txt,sha256=Z2ENjB1i5prfez8CdM1Sdr3c6Zxv2rRRolMpLmBncAE,16 | ||
| wheel/wheelfile.py,sha256=USCttNlJwafxt51YYFFKG7jnxz8dfhbyqAZL6jMTA9s,8411 |
Sorry, the diff of this file is not supported yet
| Wheel-Version: 1.0 | ||
| Generator: flit 3.10.1 | ||
| Root-Is-Purelib: true | ||
| Tag: py3-none-any |
| """ | ||
| Wheel command-line utility. | ||
| """ | ||
| from __future__ import annotations | ||
| import argparse | ||
| import os | ||
| import sys | ||
| from argparse import ArgumentTypeError | ||
| class WheelError(Exception): | ||
| pass | ||
| def unpack_f(args: argparse.Namespace) -> None: | ||
| from .unpack import unpack | ||
| unpack(args.wheelfile, args.dest) | ||
| def pack_f(args: argparse.Namespace) -> None: | ||
| from .pack import pack | ||
| pack(args.directory, args.dest_dir, args.build_number) | ||
| def convert_f(args: argparse.Namespace) -> None: | ||
| from .convert import convert | ||
| convert(args.files, args.dest_dir, args.verbose) | ||
| def tags_f(args: argparse.Namespace) -> None: | ||
| from .tags import tags | ||
| names = ( | ||
| tags( | ||
| wheel, | ||
| args.python_tag, | ||
| args.abi_tag, | ||
| args.platform_tag, | ||
| args.build, | ||
| args.remove, | ||
| ) | ||
| for wheel in args.wheel | ||
| ) | ||
| for name in names: | ||
| print(name) | ||
| def version_f(args: argparse.Namespace) -> None: | ||
| from .. import __version__ | ||
| print(f"wheel {__version__}") | ||
| def parse_build_tag(build_tag: str) -> str: | ||
| if build_tag and not build_tag[0].isdigit(): | ||
| raise ArgumentTypeError("build tag must begin with a digit") | ||
| elif "-" in build_tag: | ||
| raise ArgumentTypeError("invalid character ('-') in build tag") | ||
| return build_tag | ||
| TAGS_HELP = """\ | ||
| Make a new wheel with given tags. Any tags unspecified will remain the same. | ||
| Starting the tags with a "+" will append to the existing tags. Starting with a | ||
| "-" will remove a tag (use --option=-TAG syntax). Multiple tags can be | ||
| separated by ".". The original file will remain unless --remove is given. The | ||
| output filename(s) will be displayed on stdout for further processing. | ||
| """ | ||
| def parser(): | ||
| p = argparse.ArgumentParser() | ||
| s = p.add_subparsers(help="commands") | ||
| unpack_parser = s.add_parser("unpack", help="Unpack wheel") | ||
| unpack_parser.add_argument( | ||
| "--dest", "-d", help="Destination directory", default="." | ||
| ) | ||
| unpack_parser.add_argument("wheelfile", help="Wheel file") | ||
| unpack_parser.set_defaults(func=unpack_f) | ||
| repack_parser = s.add_parser("pack", help="Repack wheel") | ||
| repack_parser.add_argument("directory", help="Root directory of the unpacked wheel") | ||
| repack_parser.add_argument( | ||
| "--dest-dir", | ||
| "-d", | ||
| default=os.path.curdir, | ||
| help="Directory to store the wheel (default %(default)s)", | ||
| ) | ||
| repack_parser.add_argument( | ||
| "--build-number", help="Build tag to use in the wheel name" | ||
| ) | ||
| repack_parser.set_defaults(func=pack_f) | ||
| convert_parser = s.add_parser("convert", help="Convert egg or wininst to wheel") | ||
| convert_parser.add_argument("files", nargs="*", help="Files to convert") | ||
| convert_parser.add_argument( | ||
| "--dest-dir", | ||
| "-d", | ||
| default=os.path.curdir, | ||
| help="Directory to store wheels (default %(default)s)", | ||
| ) | ||
| convert_parser.add_argument("--verbose", "-v", action="store_true") | ||
| convert_parser.set_defaults(func=convert_f) | ||
| tags_parser = s.add_parser( | ||
| "tags", help="Add or replace the tags on a wheel", description=TAGS_HELP | ||
| ) | ||
| tags_parser.add_argument("wheel", nargs="*", help="Existing wheel(s) to retag") | ||
| tags_parser.add_argument( | ||
| "--remove", | ||
| action="store_true", | ||
| help="Remove the original files, keeping only the renamed ones", | ||
| ) | ||
| tags_parser.add_argument( | ||
| "--python-tag", metavar="TAG", help="Specify an interpreter tag(s)" | ||
| ) | ||
| tags_parser.add_argument("--abi-tag", metavar="TAG", help="Specify an ABI tag(s)") | ||
| tags_parser.add_argument( | ||
| "--platform-tag", metavar="TAG", help="Specify a platform tag(s)" | ||
| ) | ||
| tags_parser.add_argument( | ||
| "--build", type=parse_build_tag, metavar="BUILD", help="Specify a build tag" | ||
| ) | ||
| tags_parser.set_defaults(func=tags_f) | ||
| version_parser = s.add_parser("version", help="Print version and exit") | ||
| version_parser.set_defaults(func=version_f) | ||
| help_parser = s.add_parser("help", help="Show this help") | ||
| help_parser.set_defaults(func=lambda args: p.print_help()) | ||
| return p | ||
| def main(): | ||
| p = parser() | ||
| args = p.parse_args() | ||
| if not hasattr(args, "func"): | ||
| p.print_help() | ||
| else: | ||
| try: | ||
| args.func(args) | ||
| return 0 | ||
| except WheelError as e: | ||
| print(e, file=sys.stderr) | ||
| return 1 |
| from __future__ import annotations | ||
| import os.path | ||
| import re | ||
| from abc import ABCMeta, abstractmethod | ||
| from collections import defaultdict | ||
| from collections.abc import Iterator | ||
| from email.message import Message | ||
| from email.parser import Parser | ||
| from email.policy import EmailPolicy | ||
| from glob import iglob | ||
| from pathlib import Path | ||
| from textwrap import dedent | ||
| from zipfile import ZipFile | ||
| from .. import __version__ | ||
| from ..metadata import generate_requirements | ||
| from ..vendored.packaging.tags import parse_tag | ||
| from ..wheelfile import WheelFile | ||
| egg_filename_re = re.compile( | ||
| r""" | ||
| (?P<name>.+?)-(?P<ver>.+?) | ||
| (-(?P<pyver>py\d\.\d+) | ||
| (-(?P<arch>.+?))? | ||
| )?.egg$""", | ||
| re.VERBOSE, | ||
| ) | ||
| egg_info_re = re.compile( | ||
| r""" | ||
| ^(?P<name>.+?)-(?P<ver>.+?) | ||
| (-(?P<pyver>py\d\.\d+) | ||
| )?.egg-info/""", | ||
| re.VERBOSE, | ||
| ) | ||
| wininst_re = re.compile( | ||
| r"\.(?P<platform>win32|win-amd64)(?:-(?P<pyver>py\d\.\d))?\.exe$" | ||
| ) | ||
| pyd_re = re.compile(r"\.(?P<abi>[a-z0-9]+)-(?P<platform>win32|win_amd64)\.pyd$") | ||
| serialization_policy = EmailPolicy( | ||
| utf8=True, | ||
| mangle_from_=False, | ||
| max_line_length=0, | ||
| ) | ||
| GENERATOR = f"wheel {__version__}" | ||
| def convert_requires(requires: str, metadata: Message) -> None: | ||
| extra: str | None = None | ||
| requirements: dict[str | None, list[str]] = defaultdict(list) | ||
| for line in requires.splitlines(): | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| if line.startswith("[") and line.endswith("]"): | ||
| extra = line[1:-1] | ||
| continue | ||
| requirements[extra].append(line) | ||
| for key, value in generate_requirements(requirements): | ||
| metadata.add_header(key, value) | ||
| def convert_pkg_info(pkginfo: str, metadata: Message): | ||
| parsed_message = Parser().parsestr(pkginfo) | ||
| for key, value in parsed_message.items(): | ||
| key_lower = key.lower() | ||
| if value == "UNKNOWN": | ||
| continue | ||
| if key_lower == "description": | ||
| description_lines = value.splitlines() | ||
| value = "\n".join( | ||
| ( | ||
| description_lines[0].lstrip(), | ||
| dedent("\n".join(description_lines[1:])), | ||
| "\n", | ||
| ) | ||
| ) | ||
| metadata.set_payload(value) | ||
| elif key_lower == "home-page": | ||
| metadata.add_header("Project-URL", f"Homepage, {value}") | ||
| elif key_lower == "download-url": | ||
| metadata.add_header("Project-URL", f"Download, {value}") | ||
| else: | ||
| metadata.add_header(key, value) | ||
| metadata.replace_header("Metadata-Version", "2.4") | ||
| def normalize(name: str) -> str: | ||
| return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_") | ||
| class ConvertSource(metaclass=ABCMeta): | ||
| name: str | ||
| version: str | ||
| pyver: str = "py2.py3" | ||
| abi: str = "none" | ||
| platform: str = "any" | ||
| metadata: Message | ||
| @property | ||
| def dist_info_dir(self) -> str: | ||
| return f"{self.name}-{self.version}.dist-info" | ||
| @abstractmethod | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| pass | ||
| class EggFileSource(ConvertSource): | ||
| def __init__(self, path: Path): | ||
| if not (match := egg_filename_re.match(path.name)): | ||
| raise ValueError(f"Invalid egg file name: {path.name}") | ||
| # Binary wheels are assumed to be for CPython | ||
| self.path = path | ||
| self.name = normalize(match.group("name")) | ||
| self.version = match.group("ver") | ||
| if pyver := match.group("pyver"): | ||
| self.pyver = pyver.replace(".", "") | ||
| if arch := match.group("arch"): | ||
| self.abi = self.pyver.replace("py", "cp") | ||
| self.platform = normalize(arch) | ||
| self.metadata = Message() | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| with ZipFile(self.path, "r") as zip_file: | ||
| for filename in sorted(zip_file.namelist()): | ||
| # Skip pure directory entries | ||
| if filename.endswith("/"): | ||
| continue | ||
| # Handle files in the egg-info directory specially, selectively moving | ||
| # them to the dist-info directory while converting as needed | ||
| if filename.startswith("EGG-INFO/"): | ||
| if filename == "EGG-INFO/requires.txt": | ||
| requires = zip_file.read(filename).decode("utf-8") | ||
| convert_requires(requires, self.metadata) | ||
| elif filename == "EGG-INFO/PKG-INFO": | ||
| pkginfo = zip_file.read(filename).decode("utf-8") | ||
| convert_pkg_info(pkginfo, self.metadata) | ||
| elif filename == "EGG-INFO/entry_points.txt": | ||
| yield ( | ||
| f"{self.dist_info_dir}/entry_points.txt", | ||
| zip_file.read(filename), | ||
| ) | ||
| continue | ||
| # For any other file, just pass it through | ||
| yield filename, zip_file.read(filename) | ||
| class EggDirectorySource(EggFileSource): | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| for dirpath, _, filenames in os.walk(self.path): | ||
| for filename in sorted(filenames): | ||
| path = Path(dirpath, filename) | ||
| if path.parent.name == "EGG-INFO": | ||
| if path.name == "requires.txt": | ||
| requires = path.read_text("utf-8") | ||
| convert_requires(requires, self.metadata) | ||
| elif path.name == "PKG-INFO": | ||
| pkginfo = path.read_text("utf-8") | ||
| convert_pkg_info(pkginfo, self.metadata) | ||
| if name := self.metadata.get("Name"): | ||
| self.name = normalize(name) | ||
| if version := self.metadata.get("Version"): | ||
| self.version = version | ||
| elif path.name == "entry_points.txt": | ||
| yield ( | ||
| f"{self.dist_info_dir}/entry_points.txt", | ||
| path.read_bytes(), | ||
| ) | ||
| continue | ||
| # For any other file, just pass it through | ||
| yield str(path.relative_to(self.path)), path.read_bytes() | ||
| class WininstFileSource(ConvertSource): | ||
| """ | ||
| Handles distributions created with ``bdist_wininst``. | ||
| The egginfo filename has the format:: | ||
| name-ver(-pyver)(-arch).egg-info | ||
| The installer filename has the format:: | ||
| name-ver.arch(-pyver).exe | ||
| Some things to note: | ||
| 1. The installer filename is not definitive. An installer can be renamed | ||
| and work perfectly well as an installer. So more reliable data should | ||
| be used whenever possible. | ||
| 2. The egg-info data should be preferred for the name and version, because | ||
| these come straight from the distutils metadata, and are mandatory. | ||
| 3. The pyver from the egg-info data should be ignored, as it is | ||
| constructed from the version of Python used to build the installer, | ||
| which is irrelevant - the installer filename is correct here (even to | ||
| the point that when it's not there, any version is implied). | ||
| 4. The architecture must be taken from the installer filename, as it is | ||
| not included in the egg-info data. | ||
| 5. Architecture-neutral installers still have an architecture because the | ||
| installer format itself (being executable) is architecture-specific. We | ||
| should therefore ignore the architecture if the content is pure-python. | ||
| """ | ||
| def __init__(self, path: Path): | ||
| self.path = path | ||
| self.metadata = Message() | ||
| # Determine the initial architecture and Python version from the file name | ||
| # (if possible) | ||
| if match := wininst_re.search(path.name): | ||
| self.platform = normalize(match.group("platform")) | ||
| if pyver := match.group("pyver"): | ||
| self.pyver = pyver.replace(".", "") | ||
| # Look for an .egg-info directory and any .pyd files for more precise info | ||
| egg_info_found = pyd_found = False | ||
| with ZipFile(self.path) as zip_file: | ||
| for filename in zip_file.namelist(): | ||
| prefix, filename = filename.split("/", 1) | ||
| if not egg_info_found and (match := egg_info_re.match(filename)): | ||
| egg_info_found = True | ||
| self.name = normalize(match.group("name")) | ||
| self.version = match.group("ver") | ||
| if pyver := match.group("pyver"): | ||
| self.pyver = pyver.replace(".", "") | ||
| elif not pyd_found and (match := pyd_re.search(filename)): | ||
| pyd_found = True | ||
| self.abi = match.group("abi") | ||
| self.platform = match.group("platform") | ||
| if egg_info_found and pyd_found: | ||
| break | ||
| def generate_contents(self) -> Iterator[tuple[str, bytes]]: | ||
| dist_info_dir = f"{self.name}-{self.version}.dist-info" | ||
| data_dir = f"{self.name}-{self.version}.data" | ||
| with ZipFile(self.path, "r") as zip_file: | ||
| for filename in sorted(zip_file.namelist()): | ||
| # Skip pure directory entries | ||
| if filename.endswith("/"): | ||
| continue | ||
| # Handle files in the egg-info directory specially, selectively moving | ||
| # them to the dist-info directory while converting as needed | ||
| prefix, target_filename = filename.split("/", 1) | ||
| if egg_info_re.search(target_filename): | ||
| basename = target_filename.rsplit("/", 1)[-1] | ||
| if basename == "requires.txt": | ||
| requires = zip_file.read(filename).decode("utf-8") | ||
| convert_requires(requires, self.metadata) | ||
| elif basename == "PKG-INFO": | ||
| pkginfo = zip_file.read(filename).decode("utf-8") | ||
| convert_pkg_info(pkginfo, self.metadata) | ||
| elif basename == "entry_points.txt": | ||
| yield ( | ||
| f"{dist_info_dir}/entry_points.txt", | ||
| zip_file.read(filename), | ||
| ) | ||
| continue | ||
| elif prefix == "SCRIPTS": | ||
| target_filename = f"{data_dir}/scripts/{target_filename}" | ||
| # For any other file, just pass it through | ||
| yield target_filename, zip_file.read(filename) | ||
| def convert(files: list[str], dest_dir: str, verbose: bool) -> None: | ||
| for pat in files: | ||
| for archive in iglob(pat): | ||
| path = Path(archive) | ||
| if path.suffix == ".egg": | ||
| if path.is_dir(): | ||
| source: ConvertSource = EggDirectorySource(path) | ||
| else: | ||
| source = EggFileSource(path) | ||
| else: | ||
| source = WininstFileSource(path) | ||
| if verbose: | ||
| print(f"{archive}...", flush=True, end="") | ||
| dest_path = Path(dest_dir) / ( | ||
| f"{source.name}-{source.version}-{source.pyver}-{source.abi}" | ||
| f"-{source.platform}.whl" | ||
| ) | ||
| with WheelFile(dest_path, "w") as wheelfile: | ||
| for name_or_zinfo, contents in source.generate_contents(): | ||
| wheelfile.writestr(name_or_zinfo, contents) | ||
| # Write the METADATA file | ||
| wheelfile.writestr( | ||
| f"{source.dist_info_dir}/METADATA", | ||
| source.metadata.as_string(policy=serialization_policy).encode( | ||
| "utf-8" | ||
| ), | ||
| ) | ||
| # Write the WHEEL file | ||
| wheel_message = Message() | ||
| wheel_message.add_header("Wheel-Version", "1.0") | ||
| wheel_message.add_header("Generator", GENERATOR) | ||
| wheel_message.add_header( | ||
| "Root-Is-Purelib", str(source.platform == "any").lower() | ||
| ) | ||
| tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}") | ||
| for tag in sorted(tags, key=lambda tag: tag.interpreter): | ||
| wheel_message.add_header("Tag", str(tag)) | ||
| wheelfile.writestr( | ||
| f"{source.dist_info_dir}/WHEEL", | ||
| wheel_message.as_string(policy=serialization_policy).encode( | ||
| "utf-8" | ||
| ), | ||
| ) | ||
| if verbose: | ||
| print("OK") |
| from __future__ import annotations | ||
| import email.policy | ||
| import os.path | ||
| import re | ||
| from email.generator import BytesGenerator | ||
| from email.parser import BytesParser | ||
| from wheel.cli import WheelError | ||
| from wheel.wheelfile import WheelFile | ||
| DIST_INFO_RE = re.compile(r"^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))\.dist-info$") | ||
| def pack(directory: str, dest_dir: str, build_number: str | None) -> None: | ||
| """Repack a previously unpacked wheel directory into a new wheel file. | ||
| The .dist-info/WHEEL file must contain one or more tags so that the target | ||
| wheel file name can be determined. | ||
| :param directory: The unpacked wheel directory | ||
| :param dest_dir: Destination directory (defaults to the current directory) | ||
| """ | ||
| # Find the .dist-info directory | ||
| dist_info_dirs = [ | ||
| fn | ||
| for fn in os.listdir(directory) | ||
| if os.path.isdir(os.path.join(directory, fn)) and DIST_INFO_RE.match(fn) | ||
| ] | ||
| if len(dist_info_dirs) > 1: | ||
| raise WheelError(f"Multiple .dist-info directories found in {directory}") | ||
| elif not dist_info_dirs: | ||
| raise WheelError(f"No .dist-info directories found in {directory}") | ||
| # Determine the target wheel filename | ||
| dist_info_dir = dist_info_dirs[0] | ||
| name_version = DIST_INFO_RE.match(dist_info_dir).group("namever") | ||
| # Read the tags and the existing build number from .dist-info/WHEEL | ||
| wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL") | ||
| with open(wheel_file_path, "rb") as f: | ||
| info = BytesParser(policy=email.policy.compat32).parse(f) | ||
| tags: list[str] = info.get_all("Tag", []) | ||
| existing_build_number = info.get("Build") | ||
| if not tags: | ||
| raise WheelError( | ||
| f"No tags present in {dist_info_dir}/WHEEL; cannot determine target " | ||
| f"wheel filename" | ||
| ) | ||
| # Set the wheel file name and add/replace/remove the Build tag in .dist-info/WHEEL | ||
| build_number = build_number if build_number is not None else existing_build_number | ||
| if build_number is not None: | ||
| del info["Build"] | ||
| if build_number: | ||
| info["Build"] = build_number | ||
| name_version += "-" + build_number | ||
| if build_number != existing_build_number: | ||
| with open(wheel_file_path, "wb") as f: | ||
| BytesGenerator(f, maxheaderlen=0).flatten(info) | ||
| # Reassemble the tags for the wheel file | ||
| tagline = compute_tagline(tags) | ||
| # Repack the wheel | ||
| wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl") | ||
| with WheelFile(wheel_path, "w") as wf: | ||
| print(f"Repacking wheel as {wheel_path}...", end="", flush=True) | ||
| wf.write_files(directory) | ||
| print("OK") | ||
| def compute_tagline(tags: list[str]) -> str: | ||
| """Compute a tagline from a list of tags. | ||
| :param tags: A list of tags | ||
| :return: A tagline | ||
| """ | ||
| impls = sorted({tag.split("-")[0] for tag in tags}) | ||
| abivers = sorted({tag.split("-")[1] for tag in tags}) | ||
| platforms = sorted({tag.split("-")[2] for tag in tags}) | ||
| return "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)]) |
| from __future__ import annotations | ||
| import email.policy | ||
| import itertools | ||
| import os | ||
| from collections.abc import Iterable | ||
| from email.parser import BytesParser | ||
| from ..wheelfile import WheelFile | ||
| def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str]: | ||
| """Add or replace tags. Supports dot-separated tags""" | ||
| if new_tags is None: | ||
| return set(original_tags) | ||
| if new_tags.startswith("+"): | ||
| return {*original_tags, *new_tags[1:].split(".")} | ||
| if new_tags.startswith("-"): | ||
| return set(original_tags) - set(new_tags[1:].split(".")) | ||
| return set(new_tags.split(".")) | ||
| def tags( | ||
| wheel: str, | ||
| python_tags: str | None = None, | ||
| abi_tags: str | None = None, | ||
| platform_tags: str | None = None, | ||
| build_tag: str | None = None, | ||
| remove: bool = False, | ||
| ) -> str: | ||
| """Change the tags on a wheel file. | ||
| The tags are left unchanged if they are not specified. To specify "none", | ||
| use ["none"]. To append to the previous tags, a tag should start with a | ||
| "+". If a tag starts with "-", it will be removed from existing tags. | ||
| Processing is done left to right. | ||
| :param wheel: The paths to the wheels | ||
| :param python_tags: The Python tags to set | ||
| :param abi_tags: The ABI tags to set | ||
| :param platform_tags: The platform tags to set | ||
| :param build_tag: The build tag to set | ||
| :param remove: Remove the original wheel | ||
| """ | ||
| with WheelFile(wheel, "r") as f: | ||
| assert f.filename, f"{f.filename} must be available" | ||
| wheel_info = f.read(f.dist_info_path + "/WHEEL") | ||
| info = BytesParser(policy=email.policy.compat32).parsebytes(wheel_info) | ||
| original_wheel_name = os.path.basename(f.filename) | ||
| namever = f.parsed_filename.group("namever") | ||
| build = f.parsed_filename.group("build") | ||
| original_python_tags = f.parsed_filename.group("pyver").split(".") | ||
| original_abi_tags = f.parsed_filename.group("abi").split(".") | ||
| original_plat_tags = f.parsed_filename.group("plat").split(".") | ||
| tags: list[str] = info.get_all("Tag", []) | ||
| existing_build_tag = info.get("Build") | ||
| impls = {tag.split("-")[0] for tag in tags} | ||
| abivers = {tag.split("-")[1] for tag in tags} | ||
| platforms = {tag.split("-")[2] for tag in tags} | ||
| if impls != set(original_python_tags): | ||
| msg = f"Wheel internal tags {impls!r} != filename tags {original_python_tags!r}" | ||
| raise AssertionError(msg) | ||
| if abivers != set(original_abi_tags): | ||
| msg = f"Wheel internal tags {abivers!r} != filename tags {original_abi_tags!r}" | ||
| raise AssertionError(msg) | ||
| if platforms != set(original_plat_tags): | ||
| msg = ( | ||
| f"Wheel internal tags {platforms!r} != filename tags {original_plat_tags!r}" | ||
| ) | ||
| raise AssertionError(msg) | ||
| if existing_build_tag != build: | ||
| msg = ( | ||
| f"Incorrect filename '{build}' " | ||
| f"& *.dist-info/WHEEL '{existing_build_tag}' build numbers" | ||
| ) | ||
| raise AssertionError(msg) | ||
| # Start changing as needed | ||
| if build_tag is not None: | ||
| build = build_tag | ||
| final_python_tags = sorted(_compute_tags(original_python_tags, python_tags)) | ||
| final_abi_tags = sorted(_compute_tags(original_abi_tags, abi_tags)) | ||
| final_plat_tags = sorted(_compute_tags(original_plat_tags, platform_tags)) | ||
| final_tags = [ | ||
| namever, | ||
| ".".join(final_python_tags), | ||
| ".".join(final_abi_tags), | ||
| ".".join(final_plat_tags), | ||
| ] | ||
| if build: | ||
| final_tags.insert(1, build) | ||
| final_wheel_name = "-".join(final_tags) + ".whl" | ||
| if original_wheel_name != final_wheel_name: | ||
| del info["Tag"], info["Build"] | ||
| for a, b, c in itertools.product( | ||
| final_python_tags, final_abi_tags, final_plat_tags | ||
| ): | ||
| info["Tag"] = f"{a}-{b}-{c}" | ||
| if build: | ||
| info["Build"] = build | ||
| original_wheel_path = os.path.join( | ||
| os.path.dirname(f.filename), original_wheel_name | ||
| ) | ||
| final_wheel_path = os.path.join(os.path.dirname(f.filename), final_wheel_name) | ||
| with WheelFile(original_wheel_path, "r") as fin, WheelFile( | ||
| final_wheel_path, "w" | ||
| ) as fout: | ||
| fout.comment = fin.comment # preserve the comment | ||
| for item in fin.infolist(): | ||
| if item.is_dir(): | ||
| continue | ||
| if item.filename == f.dist_info_path + "/RECORD": | ||
| continue | ||
| if item.filename == f.dist_info_path + "/WHEEL": | ||
| fout.writestr(item, info.as_bytes()) | ||
| else: | ||
| fout.writestr(item, fin.read(item)) | ||
| if remove: | ||
| os.remove(original_wheel_path) | ||
| return final_wheel_name |
| from __future__ import annotations | ||
| from pathlib import Path | ||
| from ..wheelfile import WheelFile | ||
| def unpack(path: str, dest: str = ".") -> None: | ||
| """Unpack a wheel. | ||
| Wheel content will be unpacked to {dest}/{name}-{ver}, where {name} | ||
| is the package name and {ver} its version. | ||
| :param path: The path to the wheel. | ||
| :param dest: Destination directory (default to current directory). | ||
| """ | ||
| with WheelFile(path) as wf: | ||
| namever = wf.parsed_filename.group("namever") | ||
| destination = Path(dest) / namever | ||
| print(f"Unpacking to: {destination}...", end="", flush=True) | ||
| for zinfo in wf.filelist: | ||
| wf.extract(zinfo, destination) | ||
| # Set permissions to the same values as they were set in the archive | ||
| # We have to do this manually due to | ||
| # https://github.com/python/cpython/issues/59999 | ||
| permissions = zinfo.external_attr >> 16 & 0o777 | ||
| destination.joinpath(zinfo.filename).chmod(permissions) | ||
| print("OK") |
| from __future__ import annotations | ||
| import base64 | ||
| import logging | ||
| log = logging.getLogger("wheel") | ||
| def urlsafe_b64encode(data: bytes) -> bytes: | ||
| """urlsafe_b64encode without padding""" | ||
| return base64.urlsafe_b64encode(data).rstrip(b"=") | ||
| def urlsafe_b64decode(data: bytes) -> bytes: | ||
| """urlsafe_b64decode without padding""" | ||
| pad = b"=" * (4 - (len(data) & 3)) | ||
| return base64.urlsafe_b64decode(data + pad) |
| """ | ||
| ELF file parser. | ||
| This provides a class ``ELFFile`` that parses an ELF executable in a similar | ||
| interface to ``ZipFile``. Only the read interface is implemented. | ||
| Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca | ||
| ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html | ||
| """ | ||
| import enum | ||
| import os | ||
| import struct | ||
| from typing import IO, Optional, Tuple | ||
| class ELFInvalid(ValueError): | ||
| pass | ||
| class EIClass(enum.IntEnum): | ||
| C32 = 1 | ||
| C64 = 2 | ||
| class EIData(enum.IntEnum): | ||
| Lsb = 1 | ||
| Msb = 2 | ||
| class EMachine(enum.IntEnum): | ||
| I386 = 3 | ||
| S390 = 22 | ||
| Arm = 40 | ||
| X8664 = 62 | ||
| AArc64 = 183 | ||
| class ELFFile: | ||
| """ | ||
| Representation of an ELF executable. | ||
| """ | ||
| def __init__(self, f: IO[bytes]) -> None: | ||
| self._f = f | ||
| try: | ||
| ident = self._read("16B") | ||
| except struct.error: | ||
| raise ELFInvalid("unable to parse identification") | ||
| magic = bytes(ident[:4]) | ||
| if magic != b"\x7fELF": | ||
| raise ELFInvalid(f"invalid magic: {magic!r}") | ||
| self.capacity = ident[4] # Format for program header (bitness). | ||
| self.encoding = ident[5] # Data structure encoding (endianness). | ||
| try: | ||
| # e_fmt: Format for program header. | ||
| # p_fmt: Format for section header. | ||
| # p_idx: Indexes to find p_type, p_offset, and p_filesz. | ||
| e_fmt, self._p_fmt, self._p_idx = { | ||
| (1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB. | ||
| (1, 2): (">HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB. | ||
| (2, 1): ("<HHIQQQIHHH", "<IIQQQQQQ", (0, 2, 5)), # 64-bit LSB. | ||
| (2, 2): (">HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB. | ||
| }[(self.capacity, self.encoding)] | ||
| except KeyError: | ||
| raise ELFInvalid( | ||
| f"unrecognized capacity ({self.capacity}) or " | ||
| f"encoding ({self.encoding})" | ||
| ) | ||
| try: | ||
| ( | ||
| _, | ||
| self.machine, # Architecture type. | ||
| _, | ||
| _, | ||
| self._e_phoff, # Offset of program header. | ||
| _, | ||
| self.flags, # Processor-specific flags. | ||
| _, | ||
| self._e_phentsize, # Size of section. | ||
| self._e_phnum, # Number of sections. | ||
| ) = self._read(e_fmt) | ||
| except struct.error as e: | ||
| raise ELFInvalid("unable to parse machine and section information") from e | ||
| def _read(self, fmt: str) -> Tuple[int, ...]: | ||
| return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) | ||
| @property | ||
| def interpreter(self) -> Optional[str]: | ||
| """ | ||
| The path recorded in the ``PT_INTERP`` section header. | ||
| """ | ||
| for index in range(self._e_phnum): | ||
| self._f.seek(self._e_phoff + self._e_phentsize * index) | ||
| try: | ||
| data = self._read(self._p_fmt) | ||
| except struct.error: | ||
| continue | ||
| if data[self._p_idx[0]] != 3: # Not PT_INTERP. | ||
| continue | ||
| self._f.seek(data[self._p_idx[1]]) | ||
| return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0") | ||
| return None |
| import collections | ||
| import contextlib | ||
| import functools | ||
| import os | ||
| import re | ||
| import sys | ||
| import warnings | ||
| from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple | ||
| from ._elffile import EIClass, EIData, ELFFile, EMachine | ||
| EF_ARM_ABIMASK = 0xFF000000 | ||
| EF_ARM_ABI_VER5 = 0x05000000 | ||
| 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]: | ||
| try: | ||
| with open(path, "rb") as f: | ||
| yield ELFFile(f) | ||
| except (OSError, TypeError, ValueError): | ||
| yield None | ||
| def _is_linux_armhf(executable: str) -> bool: | ||
| # hard-float ABI can be detected from the ELF header of the running | ||
| # process | ||
| # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf | ||
| with _parse_elf(executable) as f: | ||
| return ( | ||
| f is not None | ||
| and f.capacity == EIClass.C32 | ||
| and f.encoding == EIData.Lsb | ||
| and f.machine == EMachine.Arm | ||
| and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5 | ||
| and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD | ||
| ) | ||
| def _is_linux_i686(executable: str) -> bool: | ||
| with _parse_elf(executable) as f: | ||
| return ( | ||
| f is not None | ||
| and f.capacity == EIClass.C32 | ||
| and f.encoding == EIData.Lsb | ||
| and f.machine == EMachine.I386 | ||
| ) | ||
| def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: | ||
| if "armv7l" in archs: | ||
| return _is_linux_armhf(executable) | ||
| if "i686" in archs: | ||
| return _is_linux_i686(executable) | ||
| allowed_archs = { | ||
| "x86_64", | ||
| "aarch64", | ||
| "ppc64", | ||
| "ppc64le", | ||
| "s390x", | ||
| "loongarch64", | ||
| "riscv64", | ||
| } | ||
| return any(arch in allowed_archs for arch in archs) | ||
| # If glibc ever changes its major version, we need to know what the last | ||
| # minor version was, so we can build the complete list of all versions. | ||
| # 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) | ||
| class _GLibCVersion(NamedTuple): | ||
| major: int | ||
| minor: int | ||
| def _glibc_version_string_confstr() -> Optional[str]: | ||
| """ | ||
| Primary implementation of glibc_version_string using os.confstr. | ||
| """ | ||
| # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely | ||
| # to be broken or missing. This strategy is used in the standard library | ||
| # platform module. | ||
| # 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") | ||
| assert version_string is not None | ||
| _, version = version_string.rsplit() | ||
| except (AssertionError, AttributeError, OSError, ValueError): | ||
| # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... | ||
| return None | ||
| return version | ||
| def _glibc_version_string_ctypes() -> Optional[str]: | ||
| """ | ||
| Fallback implementation of glibc_version_string using ctypes. | ||
| """ | ||
| try: | ||
| import ctypes | ||
| except ImportError: | ||
| return None | ||
| # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen | ||
| # manpage says, "If filename is NULL, then the returned handle is for the | ||
| # main program". This way we can let the linker do the work to figure out | ||
| # which libc our process is actually using. | ||
| # | ||
| # We must also handle the special case where the executable is not a | ||
| # dynamically linked executable. This can occur when using musl libc, | ||
| # for example. In this situation, dlopen() will error, leading to an | ||
| # OSError. Interestingly, at least in the case of musl, there is no | ||
| # errno set on the OSError. The single string argument used to construct | ||
| # OSError comes from libc itself and is therefore not portable to | ||
| # hard code here. In any case, failure to call dlopen() means we | ||
| # can proceed, so we bail on our attempt. | ||
| try: | ||
| process_namespace = ctypes.CDLL(None) | ||
| except OSError: | ||
| return None | ||
| try: | ||
| gnu_get_libc_version = process_namespace.gnu_get_libc_version | ||
| except AttributeError: | ||
| # Symbol doesn't exist -> therefore, we are not linked to | ||
| # glibc. | ||
| return None | ||
| # Call gnu_get_libc_version, which returns a string like "2.5" | ||
| gnu_get_libc_version.restype = ctypes.c_char_p | ||
| version_str: str = gnu_get_libc_version() | ||
| # py2 / py3 compatibility: | ||
| if not isinstance(version_str, str): | ||
| version_str = version_str.decode("ascii") | ||
| return version_str | ||
| def _glibc_version_string() -> Optional[str]: | ||
| """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]: | ||
| """Parse glibc version. | ||
| We use a regexp instead of str.split because we want to discard any | ||
| random junk that might come after the minor version -- this might happen | ||
| in patched/forked versions of glibc (e.g. Linaro's version of glibc | ||
| uses version strings like "2.20-2014.11"). See gh-3588. | ||
| """ | ||
| m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) | ||
| if not m: | ||
| warnings.warn( | ||
| f"Expected glibc version with 2 components major.minor," | ||
| f" got: {version_str}", | ||
| RuntimeWarning, | ||
| ) | ||
| return -1, -1 | ||
| return int(m.group("major")), int(m.group("minor")) | ||
| @functools.lru_cache | ||
| def _get_glibc_version() -> Tuple[int, int]: | ||
| version_str = _glibc_version_string() | ||
| if version_str is None: | ||
| return (-1, -1) | ||
| return _parse_glibc_version(version_str) | ||
| # From PEP 513, PEP 600 | ||
| def _is_compatible(arch: str, version: _GLibCVersion) -> bool: | ||
| sys_glibc = _get_glibc_version() | ||
| if sys_glibc < version: | ||
| return False | ||
| # Check for presence of _manylinux module. | ||
| try: | ||
| import _manylinux | ||
| except ImportError: | ||
| return True | ||
| if hasattr(_manylinux, "manylinux_compatible"): | ||
| result = _manylinux.manylinux_compatible(version[0], version[1], arch) | ||
| if result is not None: | ||
| return bool(result) | ||
| return True | ||
| if version == _GLibCVersion(2, 5): | ||
| if hasattr(_manylinux, "manylinux1_compatible"): | ||
| return bool(_manylinux.manylinux1_compatible) | ||
| if version == _GLibCVersion(2, 12): | ||
| if hasattr(_manylinux, "manylinux2010_compatible"): | ||
| return bool(_manylinux.manylinux2010_compatible) | ||
| if version == _GLibCVersion(2, 17): | ||
| if hasattr(_manylinux, "manylinux2014_compatible"): | ||
| return bool(_manylinux.manylinux2014_compatible) | ||
| return True | ||
| _LEGACY_MANYLINUX_MAP = { | ||
| # CentOS 7 w/ glibc 2.17 (PEP 599) | ||
| (2, 17): "manylinux2014", | ||
| # CentOS 6 w/ glibc 2.12 (PEP 571) | ||
| (2, 12): "manylinux2010", | ||
| # CentOS 5 w/ glibc 2.5 (PEP 513) | ||
| (2, 5): "manylinux1", | ||
| } | ||
| def platform_tags(archs: Sequence[str]) -> Iterator[str]: | ||
| """Generate manylinux tags compatible to the current platform. | ||
| :param archs: Sequence of compatible architectures. | ||
| The first one shall be the closest to the actual architecture and be the part of | ||
| platform tag after the ``linux_`` prefix, e.g. ``x86_64``. | ||
| The ``linux_`` prefix is assumed as a prerequisite for the current platform to | ||
| be manylinux-compatible. | ||
| :returns: An iterator of compatible manylinux tags. | ||
| """ | ||
| if not _have_compatible_abi(sys.executable, archs): | ||
| return | ||
| # Oldest glibc to be supported regardless of architecture is (2, 17). | ||
| too_old_glibc2 = _GLibCVersion(2, 16) | ||
| if set(archs) & {"x86_64", "i686"}: | ||
| # On x86/i686 also oldest glibc to be supported is (2, 5). | ||
| too_old_glibc2 = _GLibCVersion(2, 4) | ||
| current_glibc = _GLibCVersion(*_get_glibc_version()) | ||
| glibc_max_list = [current_glibc] | ||
| # We can assume compatibility across glibc major versions. | ||
| # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 | ||
| # | ||
| # Build a list of maximum glibc versions so that we can | ||
| # output the canonical list of all glibc from current_glibc | ||
| # down to too_old_glibc2, including all intermediary versions. | ||
| for glibc_major in range(current_glibc.major - 1, 1, -1): | ||
| glibc_minor = _LAST_GLIBC_MINOR[glibc_major] | ||
| glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) | ||
| for arch in archs: | ||
| for glibc_max in glibc_max_list: | ||
| if glibc_max.major == too_old_glibc2.major: | ||
| min_minor = too_old_glibc2.minor | ||
| else: | ||
| # For other glibc major versions oldest supported is (x, 0). | ||
| min_minor = -1 | ||
| for glibc_minor in range(glibc_max.minor, min_minor, -1): | ||
| glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) | ||
| tag = "manylinux_{}_{}".format(*glibc_version) | ||
| if _is_compatible(arch, glibc_version): | ||
| yield f"{tag}_{arch}" | ||
| # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. | ||
| if glibc_version in _LEGACY_MANYLINUX_MAP: | ||
| legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] | ||
| if _is_compatible(arch, glibc_version): | ||
| yield f"{legacy_tag}_{arch}" |
| """PEP 656 support. | ||
| This module implements logic to detect if the currently running Python is | ||
| linked against musl, and what musl version is used. | ||
| """ | ||
| import functools | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| from typing import Iterator, NamedTuple, Optional, Sequence | ||
| from ._elffile import ELFFile | ||
| class _MuslVersion(NamedTuple): | ||
| major: int | ||
| minor: int | ||
| def _parse_musl_version(output: str) -> Optional[_MuslVersion]: | ||
| 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 | ||
| m = re.match(r"Version (\d+)\.(\d+)", lines[1]) | ||
| if not m: | ||
| return None | ||
| return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) | ||
| @functools.lru_cache | ||
| def _get_musl_version(executable: str) -> Optional[_MuslVersion]: | ||
| """Detect currently-running musl runtime version. | ||
| This is done by checking the specified executable's dynamic linking | ||
| information, and invoking the loader to parse its output for a version | ||
| string. If the loader is musl, the output would be something like:: | ||
| musl libc (x86_64) | ||
| Version 1.2.2 | ||
| Dynamic Program Loader | ||
| """ | ||
| try: | ||
| with open(executable, "rb") as f: | ||
| ld = ELFFile(f).interpreter | ||
| except (OSError, TypeError, ValueError): | ||
| return None | ||
| if ld is None or "musl" not in ld: | ||
| return None | ||
| proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) | ||
| return _parse_musl_version(proc.stderr) | ||
| def platform_tags(archs: Sequence[str]) -> Iterator[str]: | ||
| """Generate musllinux tags compatible to the current platform. | ||
| :param archs: Sequence of compatible architectures. | ||
| The first one shall be the closest to the actual architecture and be the part of | ||
| platform tag after the ``linux_`` prefix, e.g. ``x86_64``. | ||
| The ``linux_`` prefix is assumed as a prerequisite for the current platform to | ||
| be musllinux-compatible. | ||
| :returns: An iterator of compatible musllinux tags. | ||
| """ | ||
| sys_musl = _get_musl_version(sys.executable) | ||
| if sys_musl is None: # Python not dynamically linked against musl. | ||
| return | ||
| for arch in archs: | ||
| for minor in range(sys_musl.minor, -1, -1): | ||
| yield f"musllinux_{sys_musl.major}_{minor}_{arch}" | ||
| if __name__ == "__main__": # pragma: no cover | ||
| import sysconfig | ||
| plat = sysconfig.get_platform() | ||
| assert plat.startswith("linux-"), "not linux" | ||
| print("plat:", plat) | ||
| print("musl:", _get_musl_version(sys.executable)) | ||
| print("tags:", end=" ") | ||
| for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): | ||
| print(t, end="\n ") |
| """Handwritten parser of dependency specifiers. | ||
| The docstring for each __parse_* function contains EBNF-inspired grammar representing | ||
| the implementation. | ||
| """ | ||
| import ast | ||
| from typing import Any, List, NamedTuple, Optional, Tuple, Union | ||
| from ._tokenizer import DEFAULT_RULES, Tokenizer | ||
| class Node: | ||
| def __init__(self, value: str) -> None: | ||
| self.value = value | ||
| def __str__(self) -> str: | ||
| return self.value | ||
| def __repr__(self) -> str: | ||
| return f"<{self.__class__.__name__}('{self}')>" | ||
| def serialize(self) -> str: | ||
| raise NotImplementedError | ||
| class Variable(Node): | ||
| def serialize(self) -> str: | ||
| return str(self) | ||
| class Value(Node): | ||
| def serialize(self) -> str: | ||
| return f'"{self}"' | ||
| class Op(Node): | ||
| def serialize(self) -> str: | ||
| return str(self) | ||
| 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] | ||
| class ParsedRequirement(NamedTuple): | ||
| name: str | ||
| url: str | ||
| extras: List[str] | ||
| specifier: str | ||
| marker: Optional[MarkerList] | ||
| # -------------------------------------------------------------------------------------- | ||
| # Recursive descent parser for dependency specifier | ||
| # -------------------------------------------------------------------------------------- | ||
| def parse_requirement(source: str) -> ParsedRequirement: | ||
| return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES)) | ||
| def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement: | ||
| """ | ||
| requirement = WS? IDENTIFIER WS? extras WS? requirement_details | ||
| """ | ||
| tokenizer.consume("WS") | ||
| name_token = tokenizer.expect( | ||
| "IDENTIFIER", expected="package name at the start of dependency specifier" | ||
| ) | ||
| name = name_token.text | ||
| tokenizer.consume("WS") | ||
| extras = _parse_extras(tokenizer) | ||
| tokenizer.consume("WS") | ||
| url, specifier, marker = _parse_requirement_details(tokenizer) | ||
| tokenizer.expect("END", expected="end of dependency specifier") | ||
| return ParsedRequirement(name, url, extras, specifier, marker) | ||
| def _parse_requirement_details( | ||
| tokenizer: Tokenizer, | ||
| ) -> Tuple[str, str, Optional[MarkerList]]: | ||
| """ | ||
| requirement_details = AT URL (WS requirement_marker?)? | ||
| | specifier WS? (requirement_marker)? | ||
| """ | ||
| specifier = "" | ||
| url = "" | ||
| marker = None | ||
| if tokenizer.check("AT"): | ||
| tokenizer.read() | ||
| tokenizer.consume("WS") | ||
| url_start = tokenizer.position | ||
| url = tokenizer.expect("URL", expected="URL after @").text | ||
| if tokenizer.check("END", peek=True): | ||
| return (url, specifier, marker) | ||
| tokenizer.expect("WS", expected="whitespace after URL") | ||
| # The input might end after whitespace. | ||
| if tokenizer.check("END", peek=True): | ||
| return (url, specifier, marker) | ||
| marker = _parse_requirement_marker( | ||
| tokenizer, span_start=url_start, after="URL and whitespace" | ||
| ) | ||
| else: | ||
| specifier_start = tokenizer.position | ||
| specifier = _parse_specifier(tokenizer) | ||
| tokenizer.consume("WS") | ||
| if tokenizer.check("END", peek=True): | ||
| return (url, specifier, marker) | ||
| marker = _parse_requirement_marker( | ||
| tokenizer, | ||
| span_start=specifier_start, | ||
| after=( | ||
| "version specifier" | ||
| if specifier | ||
| else "name and no valid version specifier" | ||
| ), | ||
| ) | ||
| return (url, specifier, marker) | ||
| def _parse_requirement_marker( | ||
| tokenizer: Tokenizer, *, span_start: int, after: str | ||
| ) -> MarkerList: | ||
| """ | ||
| requirement_marker = SEMICOLON marker WS? | ||
| """ | ||
| if not tokenizer.check("SEMICOLON"): | ||
| tokenizer.raise_syntax_error( | ||
| f"Expected end or semicolon (after {after})", | ||
| span_start=span_start, | ||
| ) | ||
| tokenizer.read() | ||
| marker = _parse_marker(tokenizer) | ||
| tokenizer.consume("WS") | ||
| return marker | ||
| def _parse_extras(tokenizer: Tokenizer) -> List[str]: | ||
| """ | ||
| extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)? | ||
| """ | ||
| if not tokenizer.check("LEFT_BRACKET", peek=True): | ||
| return [] | ||
| with tokenizer.enclosing_tokens( | ||
| "LEFT_BRACKET", | ||
| "RIGHT_BRACKET", | ||
| around="extras", | ||
| ): | ||
| tokenizer.consume("WS") | ||
| extras = _parse_extras_list(tokenizer) | ||
| tokenizer.consume("WS") | ||
| return extras | ||
| def _parse_extras_list(tokenizer: Tokenizer) -> List[str]: | ||
| """ | ||
| extras_list = identifier (wsp* ',' wsp* identifier)* | ||
| """ | ||
| extras: List[str] = [] | ||
| if not tokenizer.check("IDENTIFIER"): | ||
| return extras | ||
| extras.append(tokenizer.read().text) | ||
| while True: | ||
| tokenizer.consume("WS") | ||
| if tokenizer.check("IDENTIFIER", peek=True): | ||
| tokenizer.raise_syntax_error("Expected comma between extra names") | ||
| elif not tokenizer.check("COMMA"): | ||
| break | ||
| tokenizer.read() | ||
| tokenizer.consume("WS") | ||
| extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma") | ||
| extras.append(extra_token.text) | ||
| return extras | ||
| def _parse_specifier(tokenizer: Tokenizer) -> str: | ||
| """ | ||
| specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS | ||
| | WS? version_many WS? | ||
| """ | ||
| with tokenizer.enclosing_tokens( | ||
| "LEFT_PARENTHESIS", | ||
| "RIGHT_PARENTHESIS", | ||
| around="version specifier", | ||
| ): | ||
| tokenizer.consume("WS") | ||
| parsed_specifiers = _parse_version_many(tokenizer) | ||
| tokenizer.consume("WS") | ||
| return parsed_specifiers | ||
| def _parse_version_many(tokenizer: Tokenizer) -> str: | ||
| """ | ||
| version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)? | ||
| """ | ||
| parsed_specifiers = "" | ||
| while tokenizer.check("SPECIFIER"): | ||
| span_start = tokenizer.position | ||
| parsed_specifiers += tokenizer.read().text | ||
| if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True): | ||
| tokenizer.raise_syntax_error( | ||
| ".* suffix can only be used with `==` or `!=` operators", | ||
| span_start=span_start, | ||
| span_end=tokenizer.position + 1, | ||
| ) | ||
| if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True): | ||
| tokenizer.raise_syntax_error( | ||
| "Local version label can only be used with `==` or `!=` operators", | ||
| span_start=span_start, | ||
| span_end=tokenizer.position, | ||
| ) | ||
| tokenizer.consume("WS") | ||
| if not tokenizer.check("COMMA"): | ||
| break | ||
| parsed_specifiers += tokenizer.read().text | ||
| tokenizer.consume("WS") | ||
| return parsed_specifiers | ||
| # -------------------------------------------------------------------------------------- | ||
| # Recursive descent parser for marker expression | ||
| # -------------------------------------------------------------------------------------- | ||
| def parse_marker(source: str) -> MarkerList: | ||
| return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES)) | ||
| def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList: | ||
| retval = _parse_marker(tokenizer) | ||
| tokenizer.expect("END", expected="end of marker expression") | ||
| return retval | ||
| def _parse_marker(tokenizer: Tokenizer) -> MarkerList: | ||
| """ | ||
| marker = marker_atom (BOOLOP marker_atom)+ | ||
| """ | ||
| expression = [_parse_marker_atom(tokenizer)] | ||
| while tokenizer.check("BOOLOP"): | ||
| token = tokenizer.read() | ||
| expr_right = _parse_marker_atom(tokenizer) | ||
| expression.extend((token.text, expr_right)) | ||
| return expression | ||
| def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom: | ||
| """ | ||
| marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS? | ||
| | WS? marker_item WS? | ||
| """ | ||
| tokenizer.consume("WS") | ||
| if tokenizer.check("LEFT_PARENTHESIS", peek=True): | ||
| with tokenizer.enclosing_tokens( | ||
| "LEFT_PARENTHESIS", | ||
| "RIGHT_PARENTHESIS", | ||
| around="marker expression", | ||
| ): | ||
| tokenizer.consume("WS") | ||
| marker: MarkerAtom = _parse_marker(tokenizer) | ||
| tokenizer.consume("WS") | ||
| else: | ||
| marker = _parse_marker_item(tokenizer) | ||
| tokenizer.consume("WS") | ||
| return marker | ||
| def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem: | ||
| """ | ||
| marker_item = WS? marker_var WS? marker_op WS? marker_var WS? | ||
| """ | ||
| tokenizer.consume("WS") | ||
| marker_var_left = _parse_marker_var(tokenizer) | ||
| tokenizer.consume("WS") | ||
| marker_op = _parse_marker_op(tokenizer) | ||
| tokenizer.consume("WS") | ||
| marker_var_right = _parse_marker_var(tokenizer) | ||
| tokenizer.consume("WS") | ||
| return (marker_var_left, marker_op, marker_var_right) | ||
| def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: | ||
| """ | ||
| marker_var = VARIABLE | QUOTED_STRING | ||
| """ | ||
| if tokenizer.check("VARIABLE"): | ||
| return process_env_var(tokenizer.read().text.replace(".", "_")) | ||
| elif tokenizer.check("QUOTED_STRING"): | ||
| return process_python_str(tokenizer.read().text) | ||
| else: | ||
| tokenizer.raise_syntax_error( | ||
| message="Expected a marker variable or quoted string" | ||
| ) | ||
| def process_env_var(env_var: str) -> Variable: | ||
| if env_var in ("platform_python_implementation", "python_implementation"): | ||
| return Variable("platform_python_implementation") | ||
| else: | ||
| return Variable(env_var) | ||
| def process_python_str(python_str: str) -> Value: | ||
| value = ast.literal_eval(python_str) | ||
| return Value(str(value)) | ||
| def _parse_marker_op(tokenizer: Tokenizer) -> Op: | ||
| """ | ||
| marker_op = IN | NOT IN | OP | ||
| """ | ||
| if tokenizer.check("IN"): | ||
| tokenizer.read() | ||
| return Op("in") | ||
| elif tokenizer.check("NOT"): | ||
| tokenizer.read() | ||
| tokenizer.expect("WS", expected="whitespace after 'not'") | ||
| tokenizer.expect("IN", expected="'in' after 'not'") | ||
| return Op("not in") | ||
| elif tokenizer.check("OP"): | ||
| return Op(tokenizer.read().text) | ||
| else: | ||
| return tokenizer.raise_syntax_error( | ||
| "Expected marker operator, one of " | ||
| "<=, <, !=, ==, >=, >, ~=, ===, in, not in" | ||
| ) |
| # 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. | ||
| class InfinityType: | ||
| def __repr__(self) -> str: | ||
| return "Infinity" | ||
| def __hash__(self) -> int: | ||
| return hash(repr(self)) | ||
| def __lt__(self, other: object) -> bool: | ||
| return False | ||
| def __le__(self, other: object) -> bool: | ||
| return False | ||
| def __eq__(self, other: object) -> bool: | ||
| return isinstance(other, self.__class__) | ||
| def __gt__(self, other: object) -> bool: | ||
| return True | ||
| def __ge__(self, other: object) -> bool: | ||
| return True | ||
| def __neg__(self: object) -> "NegativeInfinityType": | ||
| return NegativeInfinity | ||
| Infinity = InfinityType() | ||
| class NegativeInfinityType: | ||
| def __repr__(self) -> str: | ||
| return "-Infinity" | ||
| def __hash__(self) -> int: | ||
| return hash(repr(self)) | ||
| def __lt__(self, other: object) -> bool: | ||
| return True | ||
| def __le__(self, other: object) -> bool: | ||
| return True | ||
| def __eq__(self, other: object) -> bool: | ||
| return isinstance(other, self.__class__) | ||
| def __gt__(self, other: object) -> bool: | ||
| return False | ||
| def __ge__(self, other: object) -> bool: | ||
| return False | ||
| def __neg__(self: object) -> InfinityType: | ||
| return Infinity | ||
| NegativeInfinity = NegativeInfinityType() |
| import contextlib | ||
| import re | ||
| from dataclasses import dataclass | ||
| from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union | ||
| from .specifiers import Specifier | ||
| @dataclass | ||
| class Token: | ||
| name: str | ||
| text: str | ||
| position: int | ||
| class ParserSyntaxError(Exception): | ||
| """The provided source text could not be parsed correctly.""" | ||
| def __init__( | ||
| self, | ||
| message: str, | ||
| *, | ||
| source: str, | ||
| span: Tuple[int, int], | ||
| ) -> None: | ||
| self.span = span | ||
| self.message = message | ||
| self.source = source | ||
| super().__init__() | ||
| def __str__(self) -> str: | ||
| marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" | ||
| return "\n ".join([self.message, self.source, marker]) | ||
| DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = { | ||
| "LEFT_PARENTHESIS": r"\(", | ||
| "RIGHT_PARENTHESIS": r"\)", | ||
| "LEFT_BRACKET": r"\[", | ||
| "RIGHT_BRACKET": r"\]", | ||
| "SEMICOLON": r";", | ||
| "COMMA": r",", | ||
| "QUOTED_STRING": re.compile( | ||
| r""" | ||
| ( | ||
| ('[^']*') | ||
| | | ||
| ("[^"]*") | ||
| ) | ||
| """, | ||
| re.VERBOSE, | ||
| ), | ||
| "OP": r"(===|==|~=|!=|<=|>=|<|>)", | ||
| "BOOLOP": r"\b(or|and)\b", | ||
| "IN": r"\bin\b", | ||
| "NOT": r"\bnot\b", | ||
| "VARIABLE": re.compile( | ||
| r""" | ||
| \b( | ||
| python_version | ||
| |python_full_version | ||
| |os[._]name | ||
| |sys[._]platform | ||
| |platform_(release|system) | ||
| |platform[._](version|machine|python_implementation) | ||
| |python_implementation | ||
| |implementation_(name|version) | ||
| |extra | ||
| )\b | ||
| """, | ||
| re.VERBOSE, | ||
| ), | ||
| "SPECIFIER": re.compile( | ||
| Specifier._operator_regex_str + Specifier._version_regex_str, | ||
| re.VERBOSE | re.IGNORECASE, | ||
| ), | ||
| "AT": r"\@", | ||
| "URL": r"[^ \t]+", | ||
| "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", | ||
| "VERSION_PREFIX_TRAIL": r"\.\*", | ||
| "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", | ||
| "WS": r"[ \t]+", | ||
| "END": r"$", | ||
| } | ||
| class Tokenizer: | ||
| """Context-sensitive token parsing. | ||
| Provides methods to examine the input stream to check whether the next token | ||
| matches. | ||
| """ | ||
| def __init__( | ||
| self, | ||
| source: str, | ||
| *, | ||
| rules: "Dict[str, Union[str, re.Pattern[str]]]", | ||
| ) -> None: | ||
| self.source = source | ||
| self.rules: Dict[str, re.Pattern[str]] = { | ||
| name: re.compile(pattern) for name, pattern in rules.items() | ||
| } | ||
| self.next_token: Optional[Token] = None | ||
| self.position = 0 | ||
| def consume(self, name: str) -> None: | ||
| """Move beyond provided token name, if at current position.""" | ||
| if self.check(name): | ||
| self.read() | ||
| def check(self, name: str, *, peek: bool = False) -> bool: | ||
| """Check whether the next token has the provided name. | ||
| By default, if the check succeeds, the token *must* be read before | ||
| another check. If `peek` is set to `True`, the token is not loaded and | ||
| would need to be checked again. | ||
| """ | ||
| assert ( | ||
| self.next_token is None | ||
| ), f"Cannot check for {name!r}, already have {self.next_token!r}" | ||
| assert name in self.rules, f"Unknown token name: {name!r}" | ||
| expression = self.rules[name] | ||
| match = expression.match(self.source, self.position) | ||
| if match is None: | ||
| return False | ||
| if not peek: | ||
| self.next_token = Token(name, match[0], self.position) | ||
| return True | ||
| def expect(self, name: str, *, expected: str) -> Token: | ||
| """Expect a certain token name next, failing with a syntax error otherwise. | ||
| The token is *not* read. | ||
| """ | ||
| if not self.check(name): | ||
| raise self.raise_syntax_error(f"Expected {expected}") | ||
| return self.read() | ||
| def read(self) -> Token: | ||
| """Consume the next token and return it.""" | ||
| token = self.next_token | ||
| assert token is not None | ||
| self.position += len(token.text) | ||
| self.next_token = None | ||
| return token | ||
| def raise_syntax_error( | ||
| self, | ||
| message: str, | ||
| *, | ||
| span_start: Optional[int] = None, | ||
| span_end: Optional[int] = None, | ||
| ) -> NoReturn: | ||
| """Raise ParserSyntaxError at the given position.""" | ||
| span = ( | ||
| self.position if span_start is None else span_start, | ||
| self.position if span_end is None else span_end, | ||
| ) | ||
| raise ParserSyntaxError( | ||
| message, | ||
| source=self.source, | ||
| span=span, | ||
| ) | ||
| @contextlib.contextmanager | ||
| def enclosing_tokens( | ||
| self, open_token: str, close_token: str, *, around: str | ||
| ) -> Iterator[None]: | ||
| if self.check(open_token): | ||
| open_position = self.position | ||
| self.read() | ||
| else: | ||
| open_position = None | ||
| yield | ||
| if open_position is None: | ||
| return | ||
| if not self.check(close_token): | ||
| self.raise_syntax_error( | ||
| f"Expected matching {close_token} for {open_token}, after {around}", | ||
| span_start=open_position, | ||
| ) | ||
| self.read() |
| This software is made available under the terms of *either* of the licenses | ||
| found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made | ||
| under the terms of *both* these licenses. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| # 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. | ||
| 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 ._tokenizer import ParserSyntaxError | ||
| from .specifiers import InvalidSpecifier, Specifier | ||
| from .utils import canonicalize_name | ||
| __all__ = [ | ||
| "InvalidMarker", | ||
| "UndefinedComparison", | ||
| "UndefinedEnvironmentName", | ||
| "Marker", | ||
| "default_environment", | ||
| ] | ||
| Operator = Callable[[str, str], bool] | ||
| class InvalidMarker(ValueError): | ||
| """ | ||
| An invalid marker was found, users should refer to PEP 508. | ||
| """ | ||
| class UndefinedComparison(ValueError): | ||
| """ | ||
| An invalid operation was attempted on a value that doesn't support it. | ||
| """ | ||
| class UndefinedEnvironmentName(ValueError): | ||
| """ | ||
| A name was attempted to be used that does not exist inside of the | ||
| environment. | ||
| """ | ||
| def _normalize_extra_values(results: Any) -> Any: | ||
| """ | ||
| Normalize extra values. | ||
| """ | ||
| if isinstance(results[0], tuple): | ||
| lhs, op, rhs = results[0] | ||
| if isinstance(lhs, Variable) and lhs.value == "extra": | ||
| normalized_extra = canonicalize_name(rhs.value) | ||
| rhs = Value(normalized_extra) | ||
| elif isinstance(rhs, Variable) and rhs.value == "extra": | ||
| normalized_extra = canonicalize_name(lhs.value) | ||
| lhs = Value(normalized_extra) | ||
| results[0] = lhs, op, rhs | ||
| return results | ||
| def _format_marker( | ||
| marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True | ||
| ) -> str: | ||
| assert isinstance(marker, (list, tuple, str)) | ||
| # Sometimes we have a structure like [[...]] which is a single item list | ||
| # where the single item is itself it's own list. In that case we want skip | ||
| # the rest of this function so that we don't get extraneous () on the | ||
| # outside. | ||
| if ( | ||
| isinstance(marker, list) | ||
| and len(marker) == 1 | ||
| and isinstance(marker[0], (list, tuple)) | ||
| ): | ||
| return _format_marker(marker[0]) | ||
| if isinstance(marker, list): | ||
| inner = (_format_marker(m, first=False) for m in marker) | ||
| if first: | ||
| return " ".join(inner) | ||
| else: | ||
| return "(" + " ".join(inner) + ")" | ||
| elif isinstance(marker, tuple): | ||
| return " ".join([m.serialize() for m in marker]) | ||
| else: | ||
| return marker | ||
| _operators: Dict[str, Operator] = { | ||
| "in": lambda lhs, rhs: lhs in rhs, | ||
| "not in": lambda lhs, rhs: lhs not in rhs, | ||
| "<": operator.lt, | ||
| "<=": operator.le, | ||
| "==": operator.eq, | ||
| "!=": operator.ne, | ||
| ">=": operator.ge, | ||
| ">": operator.gt, | ||
| } | ||
| def _eval_op(lhs: str, op: Op, rhs: str) -> bool: | ||
| try: | ||
| spec = Specifier("".join([op.serialize(), rhs])) | ||
| except InvalidSpecifier: | ||
| pass | ||
| else: | ||
| return spec.contains(lhs, prereleases=True) | ||
| oper: Optional[Operator] = _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, ...]: | ||
| # PEP 685 – Comparison of extra names for optional distribution dependencies | ||
| # https://peps.python.org/pep-0685/ | ||
| # > When comparing extra names, tools MUST normalize the names being | ||
| # > compared using the semantics outlined in PEP 503 for names | ||
| if key == "extra": | ||
| return tuple(canonicalize_name(v) for v in values) | ||
| # other environment markers don't have such standards | ||
| return values | ||
| def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: | ||
| groups: List[List[bool]] = [[]] | ||
| for marker in markers: | ||
| assert isinstance(marker, (list, tuple, str)) | ||
| if isinstance(marker, list): | ||
| groups[-1].append(_evaluate_markers(marker, environment)) | ||
| elif isinstance(marker, tuple): | ||
| lhs, op, rhs = marker | ||
| if isinstance(lhs, Variable): | ||
| environment_key = lhs.value | ||
| lhs_value = environment[environment_key] | ||
| rhs_value = rhs.value | ||
| else: | ||
| lhs_value = lhs.value | ||
| environment_key = rhs.value | ||
| rhs_value = environment[environment_key] | ||
| lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) | ||
| groups[-1].append(_eval_op(lhs_value, op, rhs_value)) | ||
| else: | ||
| assert marker in ["and", "or"] | ||
| if marker == "or": | ||
| groups.append([]) | ||
| return any(all(item) for item in groups) | ||
| def format_full_version(info: "sys._version_info") -> str: | ||
| version = "{0.major}.{0.minor}.{0.micro}".format(info) | ||
| kind = info.releaselevel | ||
| if kind != "final": | ||
| version += kind[0] + str(info.serial) | ||
| return version | ||
| def default_environment() -> Dict[str, str]: | ||
| iver = format_full_version(sys.implementation.version) | ||
| implementation_name = sys.implementation.name | ||
| return { | ||
| "implementation_name": implementation_name, | ||
| "implementation_version": iver, | ||
| "os_name": os.name, | ||
| "platform_machine": platform.machine(), | ||
| "platform_release": platform.release(), | ||
| "platform_system": platform.system(), | ||
| "platform_version": platform.version(), | ||
| "python_full_version": platform.python_version(), | ||
| "platform_python_implementation": platform.python_implementation(), | ||
| "python_version": ".".join(platform.python_version_tuple()[:2]), | ||
| "sys_platform": sys.platform, | ||
| } | ||
| class Marker: | ||
| def __init__(self, marker: str) -> None: | ||
| # Note: We create a Marker object without calling this constructor in | ||
| # packaging.requirements.Requirement. If any additional logic is | ||
| # added here, make sure to mirror/adapt Requirement. | ||
| try: | ||
| self._markers = _normalize_extra_values(_parse_marker(marker)) | ||
| # The attribute `_markers` can be described in terms of a recursive type: | ||
| # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] | ||
| # | ||
| # For example, the following expression: | ||
| # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") | ||
| # | ||
| # is parsed into: | ||
| # [ | ||
| # (<Variable('python_version')>, <Op('>')>, <Value('3.6')>), | ||
| # 'and', | ||
| # [ | ||
| # (<Variable('python_version')>, <Op('==')>, <Value('3.6')>), | ||
| # 'or', | ||
| # (<Variable('os_name')>, <Op('==')>, <Value('unix')>) | ||
| # ] | ||
| # ] | ||
| except ParserSyntaxError as e: | ||
| raise InvalidMarker(str(e)) from e | ||
| def __str__(self) -> str: | ||
| return _format_marker(self._markers) | ||
| def __repr__(self) -> str: | ||
| return f"<Marker('{self}')>" | ||
| def __hash__(self) -> int: | ||
| return hash((self.__class__.__name__, str(self))) | ||
| def __eq__(self, other: Any) -> bool: | ||
| if not isinstance(other, Marker): | ||
| return NotImplemented | ||
| return str(self) == str(other) | ||
| def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: | ||
| """Evaluate a marker. | ||
| Return the boolean from evaluating the given marker against the | ||
| environment. environment is an optional argument to override all or | ||
| part of the determined environment. | ||
| The environment is determined from the current Python process. | ||
| """ | ||
| current_environment = default_environment() | ||
| current_environment["extra"] = "" | ||
| if environment is not None: | ||
| current_environment.update(environment) | ||
| # The API used to allow setting extra to None. We need to handle this | ||
| # case for backwards compatibility. | ||
| if current_environment["extra"] is None: | ||
| current_environment["extra"] = "" | ||
| return _evaluate_markers(self._markers, current_environment) |
| # 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 typing import Any, Iterator, Optional, Set | ||
| from ._parser import parse_requirement as _parse_requirement | ||
| from ._tokenizer import ParserSyntaxError | ||
| from .markers import Marker, _normalize_extra_values | ||
| from .specifiers import SpecifierSet | ||
| from .utils import canonicalize_name | ||
| class InvalidRequirement(ValueError): | ||
| """ | ||
| An invalid requirement was found, users should refer to PEP 508. | ||
| """ | ||
| class Requirement: | ||
| """Parse a requirement. | ||
| Parse a given requirement string into its parts, such as name, specifier, | ||
| URL, and extras. Raises InvalidRequirement on a badly-formed requirement | ||
| string. | ||
| """ | ||
| # TODO: Can we test whether something is contained within a requirement? | ||
| # If so how do we do that? Do we need to test against the _name_ of | ||
| # the thing as well as the version? What about the markers? | ||
| # TODO: Can we normalize the name and extra name? | ||
| def __init__(self, requirement_string: str) -> None: | ||
| try: | ||
| parsed = _parse_requirement(requirement_string) | ||
| except ParserSyntaxError as e: | ||
| 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.specifier: SpecifierSet = SpecifierSet(parsed.specifier) | ||
| self.marker: Optional[Marker] = None | ||
| if parsed.marker is not None: | ||
| self.marker = Marker.__new__(Marker) | ||
| self.marker._markers = _normalize_extra_values(parsed.marker) | ||
| def _iter_parts(self, name: str) -> Iterator[str]: | ||
| yield name | ||
| if self.extras: | ||
| formatted_extras = ",".join(sorted(self.extras)) | ||
| yield f"[{formatted_extras}]" | ||
| if self.specifier: | ||
| yield str(self.specifier) | ||
| if self.url: | ||
| yield f"@ {self.url}" | ||
| if self.marker: | ||
| yield " " | ||
| if self.marker: | ||
| yield f"; {self.marker}" | ||
| def __str__(self) -> str: | ||
| return "".join(self._iter_parts(self.name)) | ||
| def __repr__(self) -> str: | ||
| return f"<Requirement('{self}')>" | ||
| def __hash__(self) -> int: | ||
| return hash( | ||
| ( | ||
| self.__class__.__name__, | ||
| *self._iter_parts(canonicalize_name(self.name)), | ||
| ) | ||
| ) | ||
| def __eq__(self, other: Any) -> bool: | ||
| if not isinstance(other, Requirement): | ||
| return NotImplemented | ||
| return ( | ||
| canonicalize_name(self.name) == canonicalize_name(other.name) | ||
| and self.extras == other.extras | ||
| and self.specifier == other.specifier | ||
| and self.url == other.url | ||
| and self.marker == other.marker | ||
| ) |
| # 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. | ||
| """ | ||
| .. testsetup:: | ||
| from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier | ||
| from packaging.version import Version | ||
| """ | ||
| import abc | ||
| import itertools | ||
| import re | ||
| from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union | ||
| from .utils import canonicalize_version | ||
| from .version import Version | ||
| UnparsedVersion = Union[Version, str] | ||
| UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) | ||
| CallableOperator = Callable[[Version, str], bool] | ||
| def _coerce_version(version: UnparsedVersion) -> Version: | ||
| if not isinstance(version, Version): | ||
| version = Version(version) | ||
| return version | ||
| class InvalidSpecifier(ValueError): | ||
| """ | ||
| Raised when attempting to create a :class:`Specifier` with a specifier | ||
| string that is invalid. | ||
| >>> Specifier("lolwat") | ||
| Traceback (most recent call last): | ||
| ... | ||
| packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat' | ||
| """ | ||
| class BaseSpecifier(metaclass=abc.ABCMeta): | ||
| @abc.abstractmethod | ||
| def __str__(self) -> str: | ||
| """ | ||
| Returns the str representation of this Specifier-like object. This | ||
| should be representative of the Specifier itself. | ||
| """ | ||
| @abc.abstractmethod | ||
| def __hash__(self) -> int: | ||
| """ | ||
| Returns a hash value for this Specifier-like object. | ||
| """ | ||
| @abc.abstractmethod | ||
| def __eq__(self, other: object) -> bool: | ||
| """ | ||
| Returns a boolean representing whether or not the two Specifier-like | ||
| objects are equal. | ||
| :param other: The other object to check against. | ||
| """ | ||
| @property | ||
| @abc.abstractmethod | ||
| def prereleases(self) -> Optional[bool]: | ||
| """Whether or not pre-releases as a whole are allowed. | ||
| This can be set to either ``True`` or ``False`` to explicitly enable or disable | ||
| prereleases or it can be set to ``None`` (the default) to use default semantics. | ||
| """ | ||
| @prereleases.setter | ||
| def prereleases(self, value: bool) -> None: | ||
| """Setter for :attr:`prereleases`. | ||
| :param value: The value to set. | ||
| """ | ||
| @abc.abstractmethod | ||
| def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: | ||
| """ | ||
| Determines if the given item is contained within this specifier. | ||
| """ | ||
| @abc.abstractmethod | ||
| def filter( | ||
| self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None | ||
| ) -> Iterator[UnparsedVersionVar]: | ||
| """ | ||
| Takes an iterable of items and filters them so that only items which | ||
| are contained within this specifier are allowed in it. | ||
| """ | ||
| class Specifier(BaseSpecifier): | ||
| """This class abstracts handling of version specifiers. | ||
| .. tip:: | ||
| It is generally not required to instantiate this manually. You should instead | ||
| prefer to work with :class:`SpecifierSet` instead, which can parse | ||
| comma-separated version specifiers (which is what package metadata contains). | ||
| """ | ||
| _operator_regex_str = r""" | ||
| (?P<operator>(~=|==|!=|<=|>=|<|>|===)) | ||
| """ | ||
| _version_regex_str = r""" | ||
| (?P<version> | ||
| (?: | ||
| # The identity operators allow for an escape hatch that will | ||
| # do an exact string match of the version you wish to install. | ||
| # This will not be parsed by PEP 440 and we cannot determine | ||
| # any semantic meaning from it. This operator is discouraged | ||
| # but included entirely as an escape hatch. | ||
| (?<====) # Only match for the identity operator | ||
| \s* | ||
| [^\s;)]* # The arbitrary version can be just about anything, | ||
| # we match everything except for whitespace, a | ||
| # semi-colon for marker support, and a closing paren | ||
| # since versions can be enclosed in them. | ||
| ) | ||
| | | ||
| (?: | ||
| # The (non)equality operators allow for wild card and local | ||
| # versions to be specified so we have to define these two | ||
| # operators separately to enable that. | ||
| (?<===|!=) # Only match for equals and not equals | ||
| \s* | ||
| v? | ||
| (?:[0-9]+!)? # epoch | ||
| [0-9]+(?:\.[0-9]+)* # release | ||
| # You cannot use a wild card and a pre-release, post-release, a dev or | ||
| # local version together so group them with a | and make them optional. | ||
| (?: | ||
| \.\* # Wild card syntax of .* | ||
| | | ||
| (?: # pre release | ||
| [-_\.]? | ||
| (alpha|beta|preview|pre|a|b|c|rc) | ||
| [-_\.]? | ||
| [0-9]* | ||
| )? | ||
| (?: # post release | ||
| (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) | ||
| )? | ||
| (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release | ||
| (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local | ||
| )? | ||
| ) | ||
| | | ||
| (?: | ||
| # The compatible operator requires at least two digits in the | ||
| # release segment. | ||
| (?<=~=) # Only match for the compatible operator | ||
| \s* | ||
| v? | ||
| (?:[0-9]+!)? # epoch | ||
| [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) | ||
| (?: # pre release | ||
| [-_\.]? | ||
| (alpha|beta|preview|pre|a|b|c|rc) | ||
| [-_\.]? | ||
| [0-9]* | ||
| )? | ||
| (?: # post release | ||
| (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) | ||
| )? | ||
| (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release | ||
| ) | ||
| | | ||
| (?: | ||
| # All other operators only allow a sub set of what the | ||
| # (non)equality operators do. Specifically they do not allow | ||
| # local versions to be specified nor do they allow the prefix | ||
| # matching wild cards. | ||
| (?<!==|!=|~=) # We have special cases for these | ||
| # operators so we want to make sure they | ||
| # don't match here. | ||
| \s* | ||
| v? | ||
| (?:[0-9]+!)? # epoch | ||
| [0-9]+(?:\.[0-9]+)* # release | ||
| (?: # pre release | ||
| [-_\.]? | ||
| (alpha|beta|preview|pre|a|b|c|rc) | ||
| [-_\.]? | ||
| [0-9]* | ||
| )? | ||
| (?: # post release | ||
| (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) | ||
| )? | ||
| (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release | ||
| ) | ||
| ) | ||
| """ | ||
| _regex = re.compile( | ||
| r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$", | ||
| re.VERBOSE | re.IGNORECASE, | ||
| ) | ||
| _operators = { | ||
| "~=": "compatible", | ||
| "==": "equal", | ||
| "!=": "not_equal", | ||
| "<=": "less_than_equal", | ||
| ">=": "greater_than_equal", | ||
| "<": "less_than", | ||
| ">": "greater_than", | ||
| "===": "arbitrary", | ||
| } | ||
| def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: | ||
| """Initialize a Specifier instance. | ||
| :param spec: | ||
| The string representation of a specifier which will be parsed and | ||
| normalized before use. | ||
| :param prereleases: | ||
| This tells the specifier if it should accept prerelease versions if | ||
| applicable or not. The default of ``None`` will autodetect it from the | ||
| given specifiers. | ||
| :raises InvalidSpecifier: | ||
| If the given specifier is invalid (i.e. bad syntax). | ||
| """ | ||
| match = self._regex.search(spec) | ||
| if not match: | ||
| raise InvalidSpecifier(f"Invalid specifier: '{spec}'") | ||
| self._spec: Tuple[str, str] = ( | ||
| match.group("operator").strip(), | ||
| match.group("version").strip(), | ||
| ) | ||
| # Store whether or not this Specifier should accept prereleases | ||
| self._prereleases = prereleases | ||
| # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 | ||
| @property # type: ignore[override] | ||
| def prereleases(self) -> bool: | ||
| # If there is an explicit prereleases set for this, then we'll just | ||
| # blindly use that. | ||
| if self._prereleases is not None: | ||
| return self._prereleases | ||
| # Look at all of our specifiers and determine if they are inclusive | ||
| # operators, and if they are if they are including an explicit | ||
| # prerelease. | ||
| operator, version = self._spec | ||
| if operator in ["==", ">=", "<=", "~=", "==="]: | ||
| # The == specifier can include a trailing .*, if it does we | ||
| # want to remove before parsing. | ||
| if operator == "==" and version.endswith(".*"): | ||
| version = version[:-2] | ||
| # Parse the version, and if it is a pre-release than this | ||
| # specifier allows pre-releases. | ||
| if Version(version).is_prerelease: | ||
| return True | ||
| return False | ||
| @prereleases.setter | ||
| def prereleases(self, value: bool) -> None: | ||
| self._prereleases = value | ||
| @property | ||
| def operator(self) -> str: | ||
| """The operator of this specifier. | ||
| >>> Specifier("==1.2.3").operator | ||
| '==' | ||
| """ | ||
| return self._spec[0] | ||
| @property | ||
| def version(self) -> str: | ||
| """The version of this specifier. | ||
| >>> Specifier("==1.2.3").version | ||
| '1.2.3' | ||
| """ | ||
| return self._spec[1] | ||
| def __repr__(self) -> str: | ||
| """A representation of the Specifier that shows all internal state. | ||
| >>> Specifier('>=1.0.0') | ||
| <Specifier('>=1.0.0')> | ||
| >>> Specifier('>=1.0.0', prereleases=False) | ||
| <Specifier('>=1.0.0', prereleases=False)> | ||
| >>> Specifier('>=1.0.0', prereleases=True) | ||
| <Specifier('>=1.0.0', prereleases=True)> | ||
| """ | ||
| pre = ( | ||
| f", prereleases={self.prereleases!r}" | ||
| if self._prereleases is not None | ||
| else "" | ||
| ) | ||
| return f"<{self.__class__.__name__}({str(self)!r}{pre})>" | ||
| def __str__(self) -> str: | ||
| """A string representation of the Specifier that can be round-tripped. | ||
| >>> str(Specifier('>=1.0.0')) | ||
| '>=1.0.0' | ||
| >>> str(Specifier('>=1.0.0', prereleases=False)) | ||
| '>=1.0.0' | ||
| """ | ||
| return "{}{}".format(*self._spec) | ||
| @property | ||
| def _canonical_spec(self) -> Tuple[str, str]: | ||
| canonical_version = canonicalize_version( | ||
| self._spec[1], | ||
| strip_trailing_zero=(self._spec[0] != "~="), | ||
| ) | ||
| return self._spec[0], canonical_version | ||
| def __hash__(self) -> int: | ||
| return hash(self._canonical_spec) | ||
| def __eq__(self, other: object) -> bool: | ||
| """Whether or not the two Specifier-like objects are equal. | ||
| :param other: The other object to check against. | ||
| The value of :attr:`prereleases` is ignored. | ||
| >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0") | ||
| True | ||
| >>> (Specifier("==1.2.3", prereleases=False) == | ||
| ... Specifier("==1.2.3", prereleases=True)) | ||
| True | ||
| >>> Specifier("==1.2.3") == "==1.2.3" | ||
| True | ||
| >>> Specifier("==1.2.3") == Specifier("==1.2.4") | ||
| False | ||
| >>> Specifier("==1.2.3") == Specifier("~=1.2.3") | ||
| False | ||
| """ | ||
| if isinstance(other, str): | ||
| try: | ||
| other = self.__class__(str(other)) | ||
| except InvalidSpecifier: | ||
| return NotImplemented | ||
| elif not isinstance(other, self.__class__): | ||
| return NotImplemented | ||
| return self._canonical_spec == other._canonical_spec | ||
| def _get_operator(self, op: str) -> CallableOperator: | ||
| operator_callable: CallableOperator = getattr( | ||
| self, f"_compare_{self._operators[op]}" | ||
| ) | ||
| 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 | ||
| # implementing it ourselves. The only thing we need to do is construct | ||
| # the other specifiers. | ||
| # We want everything but the last item in the version, but we want to | ||
| # ignore suffix segments. | ||
| prefix = _version_join( | ||
| list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] | ||
| ) | ||
| # Add the prefix notation to the end of our string | ||
| prefix += ".*" | ||
| return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( | ||
| prospective, prefix | ||
| ) | ||
| 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. | ||
| normalized_prospective = canonicalize_version( | ||
| prospective.public, strip_trailing_zero=False | ||
| ) | ||
| # Get the normalized version string ignoring the trailing .* | ||
| normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) | ||
| # Split the spec out by bangs and dots, and pretend that there is | ||
| # an implicit dot in between a release segment and a pre-release segment. | ||
| split_spec = _version_split(normalized_spec) | ||
| # Split the prospective version out by bangs and dots, and pretend | ||
| # that there is an implicit dot in between a release segment and | ||
| # a pre-release segment. | ||
| split_prospective = _version_split(normalized_prospective) | ||
| # 0-pad the prospective version before shortening it to get the correct | ||
| # shortened version. | ||
| padded_prospective, _ = _pad_version(split_prospective, split_spec) | ||
| # Shorten the prospective version to be the same length as the spec | ||
| # so that we can determine if the specifier is a prefix of the | ||
| # prospective version or not. | ||
| shortened_prospective = padded_prospective[: len(split_spec)] | ||
| return shortened_prospective == split_spec | ||
| else: | ||
| # Convert our spec string into a Version | ||
| spec_version = Version(spec) | ||
| # If the specifier does not have a local segment, then we want to | ||
| # act as if the prospective version also does not have a local | ||
| # segment. | ||
| if not spec_version.local: | ||
| prospective = Version(prospective.public) | ||
| return prospective == spec_version | ||
| def _compare_not_equal(self, prospective: Version, spec: str) -> bool: | ||
| 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) | ||
| # Check to see if the prospective version is less than the spec | ||
| # version. If it's not we can short circuit and just return False now | ||
| # instead of doing extra unneeded work. | ||
| if not prospective < spec: | ||
| return False | ||
| # This special case is here so that, unless the specifier itself | ||
| # includes is a pre-release version, that we do not accept pre-release | ||
| # versions for the version mentioned in the specifier (e.g. <3.1 should | ||
| # not match 3.1.dev0, but should match 3.0.dev0). | ||
| if not spec.is_prerelease and prospective.is_prerelease: | ||
| if Version(prospective.base_version) == Version(spec.base_version): | ||
| return False | ||
| # If we've gotten to here, it means that prospective version is both | ||
| # less than the spec version *and* it's not a pre-release of the same | ||
| # version in the spec. | ||
| 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) | ||
| # Check to see if the prospective version is greater than the spec | ||
| # version. If it's not we can short circuit and just return False now | ||
| # instead of doing extra unneeded work. | ||
| if not prospective > spec: | ||
| return False | ||
| # This special case is here so that, unless the specifier itself | ||
| # includes is a post-release version, that we do not accept | ||
| # post-release versions for the version mentioned in the specifier | ||
| # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). | ||
| if not spec.is_postrelease and prospective.is_postrelease: | ||
| if Version(prospective.base_version) == Version(spec.base_version): | ||
| return False | ||
| # Ensure that we do not allow a local version of the version mentioned | ||
| # in the specifier, which is technically greater than, to match. | ||
| if prospective.local is not None: | ||
| if Version(prospective.base_version) == Version(spec.base_version): | ||
| return False | ||
| # If we've gotten to here, it means that prospective version is both | ||
| # greater than the spec version *and* it's not a pre-release of the | ||
| # same version in the spec. | ||
| return True | ||
| def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: | ||
| return str(prospective).lower() == str(spec).lower() | ||
| def __contains__(self, item: Union[str, Version]) -> bool: | ||
| """Return whether or not the item is contained in this specifier. | ||
| :param item: The item to check for. | ||
| This is used for the ``in`` operator and behaves the same as | ||
| :meth:`contains` with no ``prereleases`` argument passed. | ||
| >>> "1.2.3" in Specifier(">=1.2.3") | ||
| True | ||
| >>> Version("1.2.3") in Specifier(">=1.2.3") | ||
| True | ||
| >>> "1.0.0" in Specifier(">=1.2.3") | ||
| False | ||
| >>> "1.3.0a1" in Specifier(">=1.2.3") | ||
| False | ||
| >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) | ||
| True | ||
| """ | ||
| return self.contains(item) | ||
| def contains( | ||
| self, item: UnparsedVersion, prereleases: Optional[bool] = None | ||
| ) -> bool: | ||
| """Return whether or not the item is contained in this specifier. | ||
| :param item: | ||
| The item to check for, which can be a version string or a | ||
| :class:`Version` instance. | ||
| :param prereleases: | ||
| Whether or not to match prereleases with this Specifier. If set to | ||
| ``None`` (the default), it uses :attr:`prereleases` to determine | ||
| whether or not prereleases are allowed. | ||
| >>> Specifier(">=1.2.3").contains("1.2.3") | ||
| True | ||
| >>> Specifier(">=1.2.3").contains(Version("1.2.3")) | ||
| True | ||
| >>> Specifier(">=1.2.3").contains("1.0.0") | ||
| False | ||
| >>> Specifier(">=1.2.3").contains("1.3.0a1") | ||
| False | ||
| >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") | ||
| True | ||
| >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) | ||
| True | ||
| """ | ||
| # Determine if prereleases are to be allowed or not. | ||
| if prereleases is None: | ||
| prereleases = self.prereleases | ||
| # Normalize item to a Version, this allows us to have a shortcut for | ||
| # "2.0" in Specifier(">=2") | ||
| normalized_item = _coerce_version(item) | ||
| # Determine if we should be supporting prereleases in this specifier | ||
| # or not, if we do not support prereleases than we can short circuit | ||
| # logic if this version is a prereleases. | ||
| if normalized_item.is_prerelease and not prereleases: | ||
| return False | ||
| # Actually do the comparison to determine if this item is contained | ||
| # within this Specifier or not. | ||
| operator_callable: CallableOperator = self._get_operator(self.operator) | ||
| return operator_callable(normalized_item, self.version) | ||
| def filter( | ||
| self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None | ||
| ) -> Iterator[UnparsedVersionVar]: | ||
| """Filter items in the given iterable, that match the specifier. | ||
| :param iterable: | ||
| An iterable that can contain version strings and :class:`Version` instances. | ||
| The items in the iterable will be filtered according to the specifier. | ||
| :param prereleases: | ||
| Whether or not to allow prereleases in the returned iterator. If set to | ||
| ``None`` (the default), it will be intelligently decide whether to allow | ||
| prereleases or not (based on the :attr:`prereleases` attribute, and | ||
| whether the only versions matching are prereleases). | ||
| This method is smarter than just ``filter(Specifier().contains, [...])`` | ||
| because it implements the rule from :pep:`440` that a prerelease item | ||
| SHOULD be accepted if no other versions match the given specifier. | ||
| >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) | ||
| ['1.3'] | ||
| >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")])) | ||
| ['1.2.3', '1.3', <Version('1.4')>] | ||
| >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"])) | ||
| ['1.5a1'] | ||
| >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) | ||
| ['1.3', '1.5a1'] | ||
| >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) | ||
| ['1.3', '1.5a1'] | ||
| """ | ||
| yielded = False | ||
| found_prereleases = [] | ||
| kw = {"prereleases": prereleases if prereleases is not None else True} | ||
| # Attempt to iterate over all the values in the iterable and if any of | ||
| # them match, yield them. | ||
| for version in iterable: | ||
| parsed_version = _coerce_version(version) | ||
| if self.contains(parsed_version, **kw): | ||
| # If our version is a prerelease, and we were not set to allow | ||
| # prereleases, then we'll store it for later in case nothing | ||
| # else matches this specifier. | ||
| if parsed_version.is_prerelease and not ( | ||
| prereleases or self.prereleases | ||
| ): | ||
| found_prereleases.append(version) | ||
| # Either this is not a prerelease, or we should have been | ||
| # accepting prereleases from the beginning. | ||
| else: | ||
| yielded = True | ||
| yield version | ||
| # Now that we've iterated over everything, determine if we've yielded | ||
| # any values, and if we have not and we have any prereleases stored up | ||
| # then we will go ahead and yield the prereleases. | ||
| if not yielded and found_prereleases: | ||
| for version in found_prereleases: | ||
| yield version | ||
| _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") | ||
| def _version_split(version: str) -> List[str]: | ||
| """Split version into components. | ||
| The split components are intended for version comparison. The logic does | ||
| not attempt to retain the original version string, so joining the | ||
| components back with :func:`_version_join` may not produce the original | ||
| version string. | ||
| """ | ||
| result: List[str] = [] | ||
| epoch, _, rest = version.rpartition("!") | ||
| result.append(epoch or "0") | ||
| for item in rest.split("."): | ||
| match = _prefix_regex.search(item) | ||
| if match: | ||
| result.extend(match.groups()) | ||
| else: | ||
| result.append(item) | ||
| return result | ||
| 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 | ||
| first component must be the epoch (either empty or numeric), and all other | ||
| components numeric. | ||
| """ | ||
| epoch, *rest = components | ||
| return f"{epoch}!{'.'.join(rest)}" | ||
| def _is_not_suffix(segment: str) -> bool: | ||
| return not any( | ||
| segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") | ||
| ) | ||
| 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 | ||
| left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) | ||
| right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) | ||
| # Get the rest of our versions | ||
| left_split.append(left[len(left_split[0]) :]) | ||
| right_split.append(right[len(right_split[0]) :]) | ||
| # Insert our padding | ||
| left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) | ||
| right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) | ||
| return ( | ||
| list(itertools.chain.from_iterable(left_split)), | ||
| list(itertools.chain.from_iterable(right_split)), | ||
| ) | ||
| class SpecifierSet(BaseSpecifier): | ||
| """This class abstracts handling of a set of version specifiers. | ||
| It can be passed a single specifier (``>=3.0``), a comma-separated list of | ||
| specifiers (``>=3.0,!=3.1``), or no specifier at all. | ||
| """ | ||
| def __init__( | ||
| self, specifiers: str = "", prereleases: Optional[bool] = None | ||
| ) -> None: | ||
| """Initialize a SpecifierSet instance. | ||
| :param specifiers: | ||
| The string representation of a specifier or a comma-separated list of | ||
| specifiers which will be parsed and normalized before use. | ||
| :param prereleases: | ||
| This tells the SpecifierSet if it should accept prerelease versions if | ||
| applicable or not. The default of ``None`` will autodetect it from the | ||
| given specifiers. | ||
| :raises InvalidSpecifier: | ||
| If the given ``specifiers`` are not parseable than this exception will be | ||
| raised. | ||
| """ | ||
| # Split on `,` to break each individual specifier into it's own item, and | ||
| # strip each item to remove leading/trailing whitespace. | ||
| split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] | ||
| # Make each individual specifier a Specifier and save in a frozen set for later. | ||
| self._specs = frozenset(map(Specifier, split_specifiers)) | ||
| # Store our prereleases value so we can use it later to determine if | ||
| # we accept prereleases or not. | ||
| self._prereleases = prereleases | ||
| @property | ||
| def prereleases(self) -> Optional[bool]: | ||
| # If we have been given an explicit prerelease modifier, then we'll | ||
| # pass that through here. | ||
| if self._prereleases is not None: | ||
| return self._prereleases | ||
| # If we don't have any specifiers, and we don't have a forced value, | ||
| # then we'll just return None since we don't know if this should have | ||
| # pre-releases or not. | ||
| if not self._specs: | ||
| return None | ||
| # Otherwise we'll see if any of the given specifiers accept | ||
| # prereleases, if any of them do we'll return True, otherwise False. | ||
| return any(s.prereleases for s in self._specs) | ||
| @prereleases.setter | ||
| def prereleases(self, value: bool) -> None: | ||
| self._prereleases = value | ||
| def __repr__(self) -> str: | ||
| """A representation of the specifier set that shows all internal state. | ||
| Note that the ordering of the individual specifiers within the set may not | ||
| match the input string. | ||
| >>> SpecifierSet('>=1.0.0,!=2.0.0') | ||
| <SpecifierSet('!=2.0.0,>=1.0.0')> | ||
| >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False) | ||
| <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=False)> | ||
| >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True) | ||
| <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=True)> | ||
| """ | ||
| pre = ( | ||
| f", prereleases={self.prereleases!r}" | ||
| if self._prereleases is not None | ||
| else "" | ||
| ) | ||
| return f"<SpecifierSet({str(self)!r}{pre})>" | ||
| def __str__(self) -> str: | ||
| """A string representation of the specifier set that can be round-tripped. | ||
| Note that the ordering of the individual specifiers within the set may not | ||
| match the input string. | ||
| >>> str(SpecifierSet(">=1.0.0,!=1.0.1")) | ||
| '!=1.0.1,>=1.0.0' | ||
| >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False)) | ||
| '!=1.0.1,>=1.0.0' | ||
| """ | ||
| return ",".join(sorted(str(s) for s in self._specs)) | ||
| def __hash__(self) -> int: | ||
| return hash(self._specs) | ||
| def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": | ||
| """Return a SpecifierSet which is a combination of the two sets. | ||
| :param other: The other object to combine with. | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1' | ||
| <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')> | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1') | ||
| <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')> | ||
| """ | ||
| if isinstance(other, str): | ||
| other = SpecifierSet(other) | ||
| elif not isinstance(other, SpecifierSet): | ||
| return NotImplemented | ||
| specifier = SpecifierSet() | ||
| specifier._specs = frozenset(self._specs | other._specs) | ||
| if self._prereleases is None and other._prereleases is not None: | ||
| specifier._prereleases = other._prereleases | ||
| elif self._prereleases is not None and other._prereleases is None: | ||
| specifier._prereleases = self._prereleases | ||
| elif self._prereleases == other._prereleases: | ||
| specifier._prereleases = self._prereleases | ||
| else: | ||
| raise ValueError( | ||
| "Cannot combine SpecifierSets with True and False prerelease " | ||
| "overrides." | ||
| ) | ||
| return specifier | ||
| def __eq__(self, other: object) -> bool: | ||
| """Whether or not the two SpecifierSet-like objects are equal. | ||
| :param other: The other object to check against. | ||
| The value of :attr:`prereleases` is ignored. | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1") | ||
| True | ||
| >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) == | ||
| ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)) | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1" | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0") | ||
| False | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2") | ||
| False | ||
| """ | ||
| if isinstance(other, (str, Specifier)): | ||
| other = SpecifierSet(str(other)) | ||
| elif not isinstance(other, SpecifierSet): | ||
| return NotImplemented | ||
| return self._specs == other._specs | ||
| def __len__(self) -> int: | ||
| """Returns the number of specifiers in this specifier set.""" | ||
| return len(self._specs) | ||
| def __iter__(self) -> Iterator[Specifier]: | ||
| """ | ||
| Returns an iterator over all the underlying :class:`Specifier` instances | ||
| in this specifier set. | ||
| >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str) | ||
| [<Specifier('!=1.0.1')>, <Specifier('>=1.0.0')>] | ||
| """ | ||
| return iter(self._specs) | ||
| def __contains__(self, item: UnparsedVersion) -> bool: | ||
| """Return whether or not the item is contained in this specifier. | ||
| :param item: The item to check for. | ||
| This is used for the ``in`` operator and behaves the same as | ||
| :meth:`contains` with no ``prereleases`` argument passed. | ||
| >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1") | ||
| True | ||
| >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1") | ||
| True | ||
| >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") | ||
| False | ||
| >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") | ||
| False | ||
| >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) | ||
| True | ||
| """ | ||
| return self.contains(item) | ||
| def contains( | ||
| self, | ||
| item: UnparsedVersion, | ||
| prereleases: Optional[bool] = None, | ||
| installed: Optional[bool] = None, | ||
| ) -> bool: | ||
| """Return whether or not the item is contained in this SpecifierSet. | ||
| :param item: | ||
| The item to check for, which can be a version string or a | ||
| :class:`Version` instance. | ||
| :param prereleases: | ||
| Whether or not to match prereleases with this SpecifierSet. If set to | ||
| ``None`` (the default), it uses :attr:`prereleases` to determine | ||
| whether or not prereleases are allowed. | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3")) | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") | ||
| False | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") | ||
| False | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") | ||
| True | ||
| >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) | ||
| True | ||
| """ | ||
| # Ensure that our item is a Version instance. | ||
| if not isinstance(item, Version): | ||
| item = Version(item) | ||
| # Determine if we're forcing a prerelease or not, if we're not forcing | ||
| # one for this particular filter call, then we'll use whatever the | ||
| # SpecifierSet thinks for whether or not we should support prereleases. | ||
| if prereleases is None: | ||
| prereleases = self.prereleases | ||
| # We can determine if we're going to allow pre-releases by looking to | ||
| # see if any of the underlying items supports them. If none of them do | ||
| # and this item is a pre-release then we do not allow it and we can | ||
| # short circuit that here. | ||
| # Note: This means that 1.0.dev1 would not be contained in something | ||
| # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 | ||
| if not prereleases and item.is_prerelease: | ||
| return False | ||
| if installed and item.is_prerelease: | ||
| item = Version(item.base_version) | ||
| # We simply dispatch to the underlying specs here to make sure that the | ||
| # given version is contained within all of them. | ||
| # Note: This use of all() here means that an empty set of specifiers | ||
| # will always return True, this is an explicit design decision. | ||
| return all(s.contains(item, prereleases=prereleases) for s in self._specs) | ||
| def filter( | ||
| self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None | ||
| ) -> Iterator[UnparsedVersionVar]: | ||
| """Filter items in the given iterable, that match the specifiers in this set. | ||
| :param iterable: | ||
| An iterable that can contain version strings and :class:`Version` instances. | ||
| The items in the iterable will be filtered according to the specifier. | ||
| :param prereleases: | ||
| Whether or not to allow prereleases in the returned iterator. If set to | ||
| ``None`` (the default), it will be intelligently decide whether to allow | ||
| prereleases or not (based on the :attr:`prereleases` attribute, and | ||
| whether the only versions matching are prereleases). | ||
| This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` | ||
| because it implements the rule from :pep:`440` that a prerelease item | ||
| SHOULD be accepted if no other versions match the given specifier. | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) | ||
| ['1.3'] | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) | ||
| ['1.3', <Version('1.4')>] | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) | ||
| [] | ||
| >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) | ||
| ['1.3', '1.5a1'] | ||
| >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) | ||
| ['1.3', '1.5a1'] | ||
| An "empty" SpecifierSet will filter items based on the presence of prerelease | ||
| versions in the set. | ||
| >>> list(SpecifierSet("").filter(["1.3", "1.5a1"])) | ||
| ['1.3'] | ||
| >>> list(SpecifierSet("").filter(["1.5a1"])) | ||
| ['1.5a1'] | ||
| >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"])) | ||
| ['1.3', '1.5a1'] | ||
| >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True)) | ||
| ['1.3', '1.5a1'] | ||
| """ | ||
| # Determine if we're forcing a prerelease or not, if we're not forcing | ||
| # one for this particular filter call, then we'll use whatever the | ||
| # SpecifierSet thinks for whether or not we should support prereleases. | ||
| if prereleases is None: | ||
| prereleases = self.prereleases | ||
| # If we have any specifiers, then we want to wrap our iterable in the | ||
| # filter method for each one, this will act as a logical AND amongst | ||
| # each specifier. | ||
| if self._specs: | ||
| for spec in self._specs: | ||
| iterable = spec.filter(iterable, prereleases=bool(prereleases)) | ||
| return iter(iterable) | ||
| # If we do not have any specifiers, then we need to have a rough filter | ||
| # which will filter out any pre-releases, unless there are no final | ||
| # releases. | ||
| else: | ||
| filtered: List[UnparsedVersionVar] = [] | ||
| found_prereleases: List[UnparsedVersionVar] = [] | ||
| for item in iterable: | ||
| parsed_version = _coerce_version(item) | ||
| # Store any item which is a pre-release for later unless we've | ||
| # already found a final version or we are accepting prereleases | ||
| if parsed_version.is_prerelease and not prereleases: | ||
| if not filtered: | ||
| found_prereleases.append(item) | ||
| else: | ||
| filtered.append(item) | ||
| # If we've found no items except for pre-releases, then we'll go | ||
| # ahead and use the pre-releases | ||
| if not filtered and found_prereleases and prereleases is None: | ||
| return iter(found_prereleases) | ||
| return iter(filtered) |
| # 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. | ||
| import logging | ||
| import platform | ||
| import re | ||
| import struct | ||
| import subprocess | ||
| import sys | ||
| import sysconfig | ||
| from importlib.machinery import EXTENSION_SUFFIXES | ||
| from typing import ( | ||
| Dict, | ||
| FrozenSet, | ||
| Iterable, | ||
| Iterator, | ||
| List, | ||
| Optional, | ||
| Sequence, | ||
| Tuple, | ||
| Union, | ||
| cast, | ||
| ) | ||
| from . import _manylinux, _musllinux | ||
| logger = logging.getLogger(__name__) | ||
| PythonVersion = Sequence[int] | ||
| MacVersion = Tuple[int, int] | ||
| INTERPRETER_SHORT_NAMES: Dict[str, str] = { | ||
| "python": "py", # Generic. | ||
| "cpython": "cp", | ||
| "pypy": "pp", | ||
| "ironpython": "ip", | ||
| "jython": "jy", | ||
| } | ||
| _32_BIT_INTERPRETER = struct.calcsize("P") == 4 | ||
| class Tag: | ||
| """ | ||
| A representation of the tag triple for a wheel. | ||
| Instances are considered immutable and thus are hashable. Equality checking | ||
| is also supported. | ||
| """ | ||
| __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] | ||
| def __init__(self, interpreter: str, abi: str, platform: str) -> None: | ||
| self._interpreter = interpreter.lower() | ||
| self._abi = abi.lower() | ||
| self._platform = platform.lower() | ||
| # The __hash__ of every single element in a Set[Tag] will be evaluated each time | ||
| # that a set calls its `.disjoint()` method, which may be called hundreds of | ||
| # times when scanning a page of links for packages with tags matching that | ||
| # Set[Tag]. Pre-computing the value here produces significant speedups for | ||
| # downstream consumers. | ||
| self._hash = hash((self._interpreter, self._abi, self._platform)) | ||
| @property | ||
| def interpreter(self) -> str: | ||
| return self._interpreter | ||
| @property | ||
| def abi(self) -> str: | ||
| return self._abi | ||
| @property | ||
| def platform(self) -> str: | ||
| return self._platform | ||
| def __eq__(self, other: object) -> bool: | ||
| if not isinstance(other, Tag): | ||
| return NotImplemented | ||
| return ( | ||
| (self._hash == other._hash) # Short-circuit ASAP for perf reasons. | ||
| and (self._platform == other._platform) | ||
| and (self._abi == other._abi) | ||
| and (self._interpreter == other._interpreter) | ||
| ) | ||
| def __hash__(self) -> int: | ||
| return self._hash | ||
| def __str__(self) -> str: | ||
| return f"{self._interpreter}-{self._abi}-{self._platform}" | ||
| def __repr__(self) -> str: | ||
| return f"<{self} @ {id(self)}>" | ||
| def parse_tag(tag: str) -> FrozenSet[Tag]: | ||
| """ | ||
| Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. | ||
| Returning a set is required due to the possibility that the tag is a | ||
| compressed tag set. | ||
| """ | ||
| tags = set() | ||
| interpreters, abis, platforms = tag.split("-") | ||
| for interpreter in interpreters.split("."): | ||
| for abi in abis.split("."): | ||
| for platform_ in platforms.split("."): | ||
| tags.add(Tag(interpreter, abi, platform_)) | ||
| 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) | ||
| if value is None and warn: | ||
| logger.debug( | ||
| "Config variable '%s' is unset, Python ABI tag may be incorrect", name | ||
| ) | ||
| return value | ||
| def _normalize_string(string: str) -> str: | ||
| return string.replace(".", "_").replace("-", "_").replace(" ", "_") | ||
| def _is_threaded_cpython(abis: List[str]) -> bool: | ||
| """ | ||
| Determine if the ABI corresponds to a threaded (`--disable-gil`) build. | ||
| The threaded builds are indicated by a "t" in the abiflags. | ||
| """ | ||
| if len(abis) == 0: | ||
| return False | ||
| # expect e.g., cp313 | ||
| m = re.match(r"cp\d+(.*)", abis[0]) | ||
| if not m: | ||
| return False | ||
| abiflags = m.group(1) | ||
| return "t" in abiflags | ||
| def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: | ||
| """ | ||
| Determine if the Python version supports abi3. | ||
| PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) | ||
| builds do not support abi3. | ||
| """ | ||
| 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]: | ||
| py_version = tuple(py_version) # To allow for version comparison. | ||
| abis = [] | ||
| version = _version_nodot(py_version[:2]) | ||
| threading = debug = pymalloc = ucs4 = "" | ||
| with_debug = _get_config_var("Py_DEBUG", warn) | ||
| has_refcount = hasattr(sys, "gettotalrefcount") | ||
| # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled | ||
| # extension modules is the best option. | ||
| # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 | ||
| has_ext = "_d.pyd" in EXTENSION_SUFFIXES | ||
| if with_debug or (with_debug is None and (has_refcount or has_ext)): | ||
| debug = "d" | ||
| if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn): | ||
| threading = "t" | ||
| if py_version < (3, 8): | ||
| with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) | ||
| if with_pymalloc or with_pymalloc is None: | ||
| pymalloc = "m" | ||
| if py_version < (3, 3): | ||
| unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) | ||
| if unicode_size == 4 or ( | ||
| unicode_size is None and sys.maxunicode == 0x10FFFF | ||
| ): | ||
| ucs4 = "u" | ||
| elif debug: | ||
| # Debug builds can also load "normal" extension modules. | ||
| # We can also assume no UCS-4 or pymalloc requirement. | ||
| abis.append(f"cp{version}{threading}") | ||
| abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") | ||
| return abis | ||
| def cpython_tags( | ||
| python_version: Optional[PythonVersion] = None, | ||
| abis: Optional[Iterable[str]] = None, | ||
| platforms: Optional[Iterable[str]] = None, | ||
| *, | ||
| warn: bool = False, | ||
| ) -> Iterator[Tag]: | ||
| """ | ||
| Yields the tags for a CPython interpreter. | ||
| The tags consist of: | ||
| - cp<python_version>-<abi>-<platform> | ||
| - cp<python_version>-abi3-<platform> | ||
| - cp<python_version>-none-<platform> | ||
| - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2. | ||
| If python_version only specifies a major version then user-provided ABIs and | ||
| the 'none' ABItag will be used. | ||
| If 'abi3' or 'none' are specified in 'abis' then they will be yielded at | ||
| their normal position and not at the beginning. | ||
| """ | ||
| if not python_version: | ||
| python_version = sys.version_info[:2] | ||
| interpreter = f"cp{_version_nodot(python_version[:2])}" | ||
| if abis is None: | ||
| if len(python_version) > 1: | ||
| abis = _cpython_abis(python_version, warn) | ||
| else: | ||
| abis = [] | ||
| abis = list(abis) | ||
| # 'abi3' and 'none' are explicitly handled later. | ||
| for explicit_abi in ("abi3", "none"): | ||
| try: | ||
| abis.remove(explicit_abi) | ||
| except ValueError: | ||
| pass | ||
| platforms = list(platforms or platform_tags()) | ||
| for abi in abis: | ||
| for platform_ in platforms: | ||
| yield Tag(interpreter, abi, platform_) | ||
| threading = _is_threaded_cpython(abis) | ||
| use_abi3 = _abi3_applies(python_version, threading) | ||
| if use_abi3: | ||
| yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) | ||
| yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) | ||
| if use_abi3: | ||
| for minor_version in range(python_version[1] - 1, 1, -1): | ||
| for platform_ in platforms: | ||
| interpreter = "cp{version}".format( | ||
| version=_version_nodot((python_version[0], minor_version)) | ||
| ) | ||
| yield Tag(interpreter, "abi3", platform_) | ||
| def _generic_abi() -> List[str]: | ||
| """ | ||
| Return the ABI tag based on EXT_SUFFIX. | ||
| """ | ||
| # The following are examples of `EXT_SUFFIX`. | ||
| # We want to keep the parts which are related to the ABI and remove the | ||
| # parts which are related to the platform: | ||
| # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 | ||
| # - mac: '.cpython-310-darwin.so' => cp310 | ||
| # - win: '.cp310-win_amd64.pyd' => cp310 | ||
| # - win: '.pyd' => cp37 (uses _cpython_abis()) | ||
| # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 | ||
| # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' | ||
| # => graalpy_38_native | ||
| ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) | ||
| if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": | ||
| raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") | ||
| parts = ext_suffix.split(".") | ||
| if len(parts) < 3: | ||
| # CPython3.7 and earlier uses ".pyd" on Windows. | ||
| return _cpython_abis(sys.version_info[:2]) | ||
| soabi = parts[1] | ||
| if soabi.startswith("cpython"): | ||
| # non-windows | ||
| abi = "cp" + soabi.split("-")[1] | ||
| elif soabi.startswith("cp"): | ||
| # windows | ||
| abi = soabi.split("-")[0] | ||
| elif soabi.startswith("pypy"): | ||
| abi = "-".join(soabi.split("-")[:2]) | ||
| elif soabi.startswith("graalpy"): | ||
| abi = "-".join(soabi.split("-")[:3]) | ||
| elif soabi: | ||
| # pyston, ironpython, others? | ||
| abi = soabi | ||
| else: | ||
| return [] | ||
| return [_normalize_string(abi)] | ||
| def generic_tags( | ||
| interpreter: Optional[str] = None, | ||
| abis: Optional[Iterable[str]] = None, | ||
| platforms: Optional[Iterable[str]] = None, | ||
| *, | ||
| warn: bool = False, | ||
| ) -> Iterator[Tag]: | ||
| """ | ||
| Yields the tags for a generic interpreter. | ||
| The tags consist of: | ||
| - <interpreter>-<abi>-<platform> | ||
| The "none" ABI will be added if it was not explicitly provided. | ||
| """ | ||
| if not interpreter: | ||
| interp_name = interpreter_name() | ||
| interp_version = interpreter_version(warn=warn) | ||
| interpreter = "".join([interp_name, interp_version]) | ||
| if abis is None: | ||
| abis = _generic_abi() | ||
| else: | ||
| abis = list(abis) | ||
| platforms = list(platforms or platform_tags()) | ||
| if "none" not in abis: | ||
| abis.append("none") | ||
| for abi in abis: | ||
| for platform_ in platforms: | ||
| yield Tag(interpreter, abi, platform_) | ||
| def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: | ||
| """ | ||
| Yields Python versions in descending order. | ||
| After the latest version, the major-only version will be yielded, and then | ||
| all previous versions of that major version. | ||
| """ | ||
| if len(py_version) > 1: | ||
| yield f"py{_version_nodot(py_version[:2])}" | ||
| yield f"py{py_version[0]}" | ||
| if len(py_version) > 1: | ||
| for minor in range(py_version[1] - 1, -1, -1): | ||
| yield f"py{_version_nodot((py_version[0], minor))}" | ||
| def compatible_tags( | ||
| python_version: Optional[PythonVersion] = None, | ||
| interpreter: Optional[str] = None, | ||
| platforms: Optional[Iterable[str]] = None, | ||
| ) -> Iterator[Tag]: | ||
| """ | ||
| Yields the sequence of tags that are compatible with a specific version of Python. | ||
| The tags consist of: | ||
| - py*-none-<platform> | ||
| - <interpreter>-none-any # ... if `interpreter` is provided. | ||
| - py*-none-any | ||
| """ | ||
| if not python_version: | ||
| python_version = sys.version_info[:2] | ||
| platforms = list(platforms or platform_tags()) | ||
| for version in _py_interpreter_range(python_version): | ||
| for platform_ in platforms: | ||
| yield Tag(version, "none", platform_) | ||
| if interpreter: | ||
| yield Tag(interpreter, "none", "any") | ||
| for version in _py_interpreter_range(python_version): | ||
| yield Tag(version, "none", "any") | ||
| def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: | ||
| if not is_32bit: | ||
| return arch | ||
| if arch.startswith("ppc"): | ||
| return "ppc" | ||
| return "i386" | ||
| def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: | ||
| formats = [cpu_arch] | ||
| if cpu_arch == "x86_64": | ||
| if version < (10, 4): | ||
| return [] | ||
| formats.extend(["intel", "fat64", "fat32"]) | ||
| elif cpu_arch == "i386": | ||
| if version < (10, 4): | ||
| return [] | ||
| formats.extend(["intel", "fat32", "fat"]) | ||
| elif cpu_arch == "ppc64": | ||
| # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? | ||
| if version > (10, 5) or version < (10, 4): | ||
| return [] | ||
| formats.append("fat64") | ||
| elif cpu_arch == "ppc": | ||
| if version > (10, 6): | ||
| return [] | ||
| formats.extend(["fat32", "fat"]) | ||
| if cpu_arch in {"arm64", "x86_64"}: | ||
| formats.append("universal2") | ||
| if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: | ||
| formats.append("universal") | ||
| return formats | ||
| def mac_platforms( | ||
| version: Optional[MacVersion] = None, arch: Optional[str] = None | ||
| ) -> Iterator[str]: | ||
| """ | ||
| Yields the platform tags for a macOS system. | ||
| The `version` parameter is a two-item tuple specifying the macOS version to | ||
| generate platform tags for. The `arch` parameter is the CPU architecture to | ||
| generate platform tags for. Both parameters default to the appropriate value | ||
| for the current system. | ||
| """ | ||
| version_str, _, cpu_arch = platform.mac_ver() | ||
| if version is None: | ||
| version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) | ||
| if version == (10, 16): | ||
| # When built against an older macOS SDK, Python will report macOS 10.16 | ||
| # instead of the real version. | ||
| version_str = subprocess.run( | ||
| [ | ||
| sys.executable, | ||
| "-sS", | ||
| "-c", | ||
| "import platform; print(platform.mac_ver()[0])", | ||
| ], | ||
| check=True, | ||
| env={"SYSTEM_VERSION_COMPAT": "0"}, | ||
| stdout=subprocess.PIPE, | ||
| text=True, | ||
| ).stdout | ||
| version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) | ||
| else: | ||
| version = version | ||
| if arch is None: | ||
| arch = _mac_arch(cpu_arch) | ||
| else: | ||
| arch = arch | ||
| if (10, 0) <= version and version < (11, 0): | ||
| # Prior to Mac OS 11, each yearly release of Mac OS bumped the | ||
| # "minor" version number. The major version was always 10. | ||
| for minor_version in range(version[1], -1, -1): | ||
| compat_version = 10, minor_version | ||
| binary_formats = _mac_binary_formats(compat_version, arch) | ||
| for binary_format in binary_formats: | ||
| yield "macosx_{major}_{minor}_{binary_format}".format( | ||
| major=10, minor=minor_version, binary_format=binary_format | ||
| ) | ||
| if version >= (11, 0): | ||
| # Starting with Mac OS 11, each yearly release bumps the major version | ||
| # number. The minor versions are now the midyear updates. | ||
| for major_version in range(version[0], 10, -1): | ||
| compat_version = major_version, 0 | ||
| binary_formats = _mac_binary_formats(compat_version, arch) | ||
| for binary_format in binary_formats: | ||
| yield "macosx_{major}_{minor}_{binary_format}".format( | ||
| major=major_version, minor=0, binary_format=binary_format | ||
| ) | ||
| if version >= (11, 0): | ||
| # Mac OS 11 on x86_64 is compatible with binaries from previous releases. | ||
| # Arm64 support was introduced in 11.0, so no Arm binaries from previous | ||
| # releases exist. | ||
| # | ||
| # However, the "universal2" binary format can have a | ||
| # macOS version earlier than 11.0 when the x86_64 part of the binary supports | ||
| # that version of macOS. | ||
| if arch == "x86_64": | ||
| for minor_version in range(16, 3, -1): | ||
| compat_version = 10, minor_version | ||
| binary_formats = _mac_binary_formats(compat_version, arch) | ||
| for binary_format in binary_formats: | ||
| yield "macosx_{major}_{minor}_{binary_format}".format( | ||
| major=compat_version[0], | ||
| minor=compat_version[1], | ||
| binary_format=binary_format, | ||
| ) | ||
| else: | ||
| for minor_version in range(16, 3, -1): | ||
| compat_version = 10, minor_version | ||
| binary_format = "universal2" | ||
| yield "macosx_{major}_{minor}_{binary_format}".format( | ||
| major=compat_version[0], | ||
| minor=compat_version[1], | ||
| binary_format=binary_format, | ||
| ) | ||
| def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: | ||
| linux = _normalize_string(sysconfig.get_platform()) | ||
| if not linux.startswith("linux_"): | ||
| # we should never be here, just yield the sysconfig one and return | ||
| yield linux | ||
| return | ||
| if is_32bit: | ||
| if linux == "linux_x86_64": | ||
| linux = "linux_i686" | ||
| elif linux == "linux_aarch64": | ||
| linux = "linux_armv8l" | ||
| _, arch = linux.split("_", 1) | ||
| archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) | ||
| yield from _manylinux.platform_tags(archs) | ||
| yield from _musllinux.platform_tags(archs) | ||
| for arch in archs: | ||
| yield f"linux_{arch}" | ||
| def _generic_platforms() -> Iterator[str]: | ||
| yield _normalize_string(sysconfig.get_platform()) | ||
| def platform_tags() -> Iterator[str]: | ||
| """ | ||
| Provides the platform tags for this installation. | ||
| """ | ||
| if platform.system() == "Darwin": | ||
| return mac_platforms() | ||
| elif platform.system() == "Linux": | ||
| return _linux_platforms() | ||
| else: | ||
| return _generic_platforms() | ||
| def interpreter_name() -> str: | ||
| """ | ||
| Returns the name of the running interpreter. | ||
| Some implementations have a reserved, two-letter abbreviation which will | ||
| be returned when appropriate. | ||
| """ | ||
| name = sys.implementation.name | ||
| return INTERPRETER_SHORT_NAMES.get(name) or name | ||
| def interpreter_version(*, warn: bool = False) -> str: | ||
| """ | ||
| Returns the version of the running interpreter. | ||
| """ | ||
| version = _get_config_var("py_version_nodot", warn=warn) | ||
| if version: | ||
| version = str(version) | ||
| else: | ||
| version = _version_nodot(sys.version_info[:2]) | ||
| return version | ||
| def _version_nodot(version: PythonVersion) -> str: | ||
| return "".join(map(str, version)) | ||
| def sys_tags(*, warn: bool = False) -> Iterator[Tag]: | ||
| """ | ||
| Returns the sequence of tag triples for the running interpreter. | ||
| The order of the sequence corresponds to priority order for the | ||
| interpreter, from most to least important. | ||
| """ | ||
| interp_name = interpreter_name() | ||
| if interp_name == "cp": | ||
| yield from cpython_tags(warn=warn) | ||
| else: | ||
| yield from generic_tags() | ||
| if interp_name == "pp": | ||
| interp = "pp3" | ||
| elif interp_name == "cp": | ||
| interp = "cp" + interpreter_version(warn=warn) | ||
| else: | ||
| interp = None | ||
| yield from compatible_tags(interpreter=interp) |
| # 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. | ||
| import re | ||
| from typing import FrozenSet, NewType, Tuple, Union, cast | ||
| from .tags import Tag, parse_tag | ||
| from .version import InvalidVersion, Version | ||
| BuildTag = Union[Tuple[()], Tuple[int, str]] | ||
| NormalizedName = NewType("NormalizedName", str) | ||
| class InvalidName(ValueError): | ||
| """ | ||
| An invalid distribution name; users should refer to the packaging user guide. | ||
| """ | ||
| class InvalidWheelFilename(ValueError): | ||
| """ | ||
| An invalid wheel filename was found, users should refer to PEP 427. | ||
| """ | ||
| class InvalidSdistFilename(ValueError): | ||
| """ | ||
| An invalid sdist filename was found, users should refer to the packaging user guide. | ||
| """ | ||
| # Core metadata spec for `Name` | ||
| _validate_regex = re.compile( | ||
| r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE | ||
| ) | ||
| _canonicalize_regex = re.compile(r"[-_.]+") | ||
| _normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") | ||
| # PEP 427: The build number must start with a digit. | ||
| _build_tag_regex = re.compile(r"(\d+)(.*)") | ||
| def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: | ||
| if validate and not _validate_regex.match(name): | ||
| raise InvalidName(f"name is invalid: {name!r}") | ||
| # This is taken from PEP 503. | ||
| value = _canonicalize_regex.sub("-", name).lower() | ||
| return cast(NormalizedName, value) | ||
| def is_normalized_name(name: str) -> bool: | ||
| return _normalized_regex.match(name) is not None | ||
| def canonicalize_version( | ||
| version: Union[Version, str], *, strip_trailing_zero: bool = True | ||
| ) -> str: | ||
| """ | ||
| This is very similar to Version.__str__, but has one subtle difference | ||
| with the way it handles the release segment. | ||
| """ | ||
| if isinstance(version, str): | ||
| try: | ||
| parsed = Version(version) | ||
| except InvalidVersion: | ||
| # Legacy versions cannot be normalized | ||
| return version | ||
| else: | ||
| parsed = version | ||
| parts = [] | ||
| # Epoch | ||
| if parsed.epoch != 0: | ||
| parts.append(f"{parsed.epoch}!") | ||
| # Release segment | ||
| release_segment = ".".join(str(x) for x in parsed.release) | ||
| if strip_trailing_zero: | ||
| # NB: This strips trailing '.0's to normalize | ||
| release_segment = re.sub(r"(\.0)+$", "", release_segment) | ||
| parts.append(release_segment) | ||
| # Pre-release | ||
| if parsed.pre is not None: | ||
| parts.append("".join(str(x) for x in parsed.pre)) | ||
| # Post-release | ||
| if parsed.post is not None: | ||
| parts.append(f".post{parsed.post}") | ||
| # Development release | ||
| if parsed.dev is not None: | ||
| parts.append(f".dev{parsed.dev}") | ||
| # Local version segment | ||
| if parsed.local is not None: | ||
| parts.append(f"+{parsed.local}") | ||
| return "".join(parts) | ||
| def parse_wheel_filename( | ||
| filename: str, | ||
| ) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: | ||
| if not filename.endswith(".whl"): | ||
| raise InvalidWheelFilename( | ||
| f"Invalid wheel filename (extension must be '.whl'): {filename}" | ||
| ) | ||
| filename = filename[:-4] | ||
| dashes = filename.count("-") | ||
| if dashes not in (4, 5): | ||
| raise InvalidWheelFilename( | ||
| f"Invalid wheel filename (wrong number of parts): {filename}" | ||
| ) | ||
| parts = filename.split("-", dashes - 2) | ||
| name_part = parts[0] | ||
| # See PEP 427 for the rules on escaping the project name. | ||
| if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: | ||
| raise InvalidWheelFilename(f"Invalid project name: {filename}") | ||
| name = canonicalize_name(name_part) | ||
| try: | ||
| version = Version(parts[1]) | ||
| except InvalidVersion as e: | ||
| raise InvalidWheelFilename( | ||
| f"Invalid wheel filename (invalid version): {filename}" | ||
| ) from e | ||
| if dashes == 5: | ||
| build_part = parts[2] | ||
| build_match = _build_tag_regex.match(build_part) | ||
| if build_match is None: | ||
| raise InvalidWheelFilename( | ||
| f"Invalid build number: {build_part} in '{filename}'" | ||
| ) | ||
| build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) | ||
| else: | ||
| build = () | ||
| tags = parse_tag(parts[-1]) | ||
| return (name, version, build, tags) | ||
| def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: | ||
| if filename.endswith(".tar.gz"): | ||
| file_stem = filename[: -len(".tar.gz")] | ||
| elif filename.endswith(".zip"): | ||
| file_stem = filename[: -len(".zip")] | ||
| else: | ||
| raise InvalidSdistFilename( | ||
| f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" | ||
| f" {filename}" | ||
| ) | ||
| # We are requiring a PEP 440 version, which cannot contain dashes, | ||
| # so we split on the last dash. | ||
| name_part, sep, version_part = file_stem.rpartition("-") | ||
| if not sep: | ||
| raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") | ||
| name = canonicalize_name(name_part) | ||
| try: | ||
| version = Version(version_part) | ||
| except InvalidVersion as e: | ||
| raise InvalidSdistFilename( | ||
| f"Invalid sdist filename (invalid version): {filename}" | ||
| ) from e | ||
| return (name, version) |
| # 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. | ||
| """ | ||
| .. testsetup:: | ||
| from packaging.version import parse, Version | ||
| """ | ||
| import itertools | ||
| import re | ||
| from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union | ||
| from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType | ||
| __all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] | ||
| LocalType = Tuple[Union[int, str], ...] | ||
| CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] | ||
| CmpLocalType = Union[ | ||
| NegativeInfinityType, | ||
| Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], | ||
| ] | ||
| CmpKey = Tuple[ | ||
| int, | ||
| Tuple[int, ...], | ||
| CmpPrePostDevType, | ||
| CmpPrePostDevType, | ||
| CmpPrePostDevType, | ||
| CmpLocalType, | ||
| ] | ||
| 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] | ||
| def parse(version: str) -> "Version": | ||
| """Parse the given version string. | ||
| >>> parse('1.0.dev1') | ||
| <Version('1.0.dev1')> | ||
| :param version: The version string to parse. | ||
| :raises InvalidVersion: When the version string is not a valid version. | ||
| """ | ||
| return Version(version) | ||
| class InvalidVersion(ValueError): | ||
| """Raised when a version string is not a valid version. | ||
| >>> Version("invalid") | ||
| Traceback (most recent call last): | ||
| ... | ||
| packaging.version.InvalidVersion: Invalid version: 'invalid' | ||
| """ | ||
| class _BaseVersion: | ||
| _key: Tuple[Any, ...] | ||
| def __hash__(self) -> int: | ||
| return hash(self._key) | ||
| # 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: | ||
| if not isinstance(other, _BaseVersion): | ||
| return NotImplemented | ||
| return self._key < other._key | ||
| def __le__(self, other: "_BaseVersion") -> bool: | ||
| if not isinstance(other, _BaseVersion): | ||
| return NotImplemented | ||
| return self._key <= other._key | ||
| def __eq__(self, other: object) -> bool: | ||
| if not isinstance(other, _BaseVersion): | ||
| return NotImplemented | ||
| return self._key == other._key | ||
| def __ge__(self, other: "_BaseVersion") -> bool: | ||
| if not isinstance(other, _BaseVersion): | ||
| return NotImplemented | ||
| return self._key >= other._key | ||
| def __gt__(self, other: "_BaseVersion") -> bool: | ||
| if not isinstance(other, _BaseVersion): | ||
| return NotImplemented | ||
| return self._key > other._key | ||
| def __ne__(self, other: object) -> bool: | ||
| if not isinstance(other, _BaseVersion): | ||
| return NotImplemented | ||
| return self._key != other._key | ||
| # Deliberately not anchored to the start and end of the string, to make it | ||
| # easier for 3rd party code to reuse | ||
| _VERSION_PATTERN = r""" | ||
| v? | ||
| (?: | ||
| (?:(?P<epoch>[0-9]+)!)? # epoch | ||
| (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment | ||
| (?P<pre> # pre-release | ||
| [-_\.]? | ||
| (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) | ||
| [-_\.]? | ||
| (?P<pre_n>[0-9]+)? | ||
| )? | ||
| (?P<post> # post release | ||
| (?:-(?P<post_n1>[0-9]+)) | ||
| | | ||
| (?: | ||
| [-_\.]? | ||
| (?P<post_l>post|rev|r) | ||
| [-_\.]? | ||
| (?P<post_n2>[0-9]+)? | ||
| ) | ||
| )? | ||
| (?P<dev> # dev release | ||
| [-_\.]? | ||
| (?P<dev_l>dev) | ||
| [-_\.]? | ||
| (?P<dev_n>[0-9]+)? | ||
| )? | ||
| ) | ||
| (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version | ||
| """ | ||
| VERSION_PATTERN = _VERSION_PATTERN | ||
| """ | ||
| A string containing the regular expression used to match a valid version. | ||
| The pattern is not anchored at either end, and is intended for embedding in larger | ||
| expressions (for example, matching a version number as part of a file name). The | ||
| regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` | ||
| flags set. | ||
| :meta hide-value: | ||
| """ | ||
| class Version(_BaseVersion): | ||
| """This class abstracts handling of a project's versions. | ||
| A :class:`Version` instance is comparison aware and can be compared and | ||
| sorted using the standard Python interfaces. | ||
| >>> v1 = Version("1.0a5") | ||
| >>> v2 = Version("1.0") | ||
| >>> v1 | ||
| <Version('1.0a5')> | ||
| >>> v2 | ||
| <Version('1.0')> | ||
| >>> v1 < v2 | ||
| True | ||
| >>> v1 == v2 | ||
| False | ||
| >>> v1 > v2 | ||
| False | ||
| >>> v1 >= v2 | ||
| False | ||
| >>> v1 <= v2 | ||
| True | ||
| """ | ||
| _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) | ||
| _key: CmpKey | ||
| def __init__(self, version: str) -> None: | ||
| """Initialize a Version object. | ||
| :param version: | ||
| The string representation of a version which will be parsed and normalized | ||
| before use. | ||
| :raises InvalidVersion: | ||
| If the ``version`` does not conform to PEP 440 in any way then this | ||
| exception will be raised. | ||
| """ | ||
| # Validate the version and parse it into pieces | ||
| match = self._regex.search(version) | ||
| if not match: | ||
| raise InvalidVersion(f"Invalid version: '{version}'") | ||
| # Store the parsed out pieces of the version | ||
| self._version = _Version( | ||
| epoch=int(match.group("epoch")) if match.group("epoch") else 0, | ||
| release=tuple(int(i) for i in match.group("release").split(".")), | ||
| pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), | ||
| post=_parse_letter_version( | ||
| match.group("post_l"), match.group("post_n1") or match.group("post_n2") | ||
| ), | ||
| dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), | ||
| local=_parse_local_version(match.group("local")), | ||
| ) | ||
| # Generate a key which will be used for sorting | ||
| self._key = _cmpkey( | ||
| self._version.epoch, | ||
| self._version.release, | ||
| self._version.pre, | ||
| self._version.post, | ||
| self._version.dev, | ||
| self._version.local, | ||
| ) | ||
| def __repr__(self) -> str: | ||
| """A representation of the Version that shows all internal state. | ||
| >>> Version('1.0.0') | ||
| <Version('1.0.0')> | ||
| """ | ||
| return f"<Version('{self}')>" | ||
| def __str__(self) -> str: | ||
| """A string representation of the version that can be rounded-tripped. | ||
| >>> str(Version("1.0a5")) | ||
| '1.0a5' | ||
| """ | ||
| parts = [] | ||
| # Epoch | ||
| if self.epoch != 0: | ||
| parts.append(f"{self.epoch}!") | ||
| # Release segment | ||
| parts.append(".".join(str(x) for x in self.release)) | ||
| # Pre-release | ||
| if self.pre is not None: | ||
| parts.append("".join(str(x) for x in self.pre)) | ||
| # Post-release | ||
| if self.post is not None: | ||
| parts.append(f".post{self.post}") | ||
| # Development release | ||
| if self.dev is not None: | ||
| parts.append(f".dev{self.dev}") | ||
| # Local version segment | ||
| if self.local is not None: | ||
| parts.append(f"+{self.local}") | ||
| return "".join(parts) | ||
| @property | ||
| def epoch(self) -> int: | ||
| """The epoch of the version. | ||
| >>> Version("2.0.0").epoch | ||
| 0 | ||
| >>> Version("1!2.0.0").epoch | ||
| 1 | ||
| """ | ||
| return self._version.epoch | ||
| @property | ||
| def release(self) -> Tuple[int, ...]: | ||
| """The components of the "release" segment of the version. | ||
| >>> Version("1.2.3").release | ||
| (1, 2, 3) | ||
| >>> Version("2.0.0").release | ||
| (2, 0, 0) | ||
| >>> Version("1!2.0.0.post0").release | ||
| (2, 0, 0) | ||
| Includes trailing zeroes but not the epoch or any pre-release / development / | ||
| post-release suffixes. | ||
| """ | ||
| return self._version.release | ||
| @property | ||
| def pre(self) -> Optional[Tuple[str, int]]: | ||
| """The pre-release segment of the version. | ||
| >>> print(Version("1.2.3").pre) | ||
| None | ||
| >>> Version("1.2.3a1").pre | ||
| ('a', 1) | ||
| >>> Version("1.2.3b1").pre | ||
| ('b', 1) | ||
| >>> Version("1.2.3rc1").pre | ||
| ('rc', 1) | ||
| """ | ||
| return self._version.pre | ||
| @property | ||
| def post(self) -> Optional[int]: | ||
| """The post-release number of the version. | ||
| >>> print(Version("1.2.3").post) | ||
| None | ||
| >>> Version("1.2.3.post1").post | ||
| 1 | ||
| """ | ||
| return self._version.post[1] if self._version.post else None | ||
| @property | ||
| def dev(self) -> Optional[int]: | ||
| """The development number of the version. | ||
| >>> print(Version("1.2.3").dev) | ||
| None | ||
| >>> Version("1.2.3.dev1").dev | ||
| 1 | ||
| """ | ||
| return self._version.dev[1] if self._version.dev else None | ||
| @property | ||
| def local(self) -> Optional[str]: | ||
| """The local version segment of the version. | ||
| >>> print(Version("1.2.3").local) | ||
| None | ||
| >>> Version("1.2.3+abc").local | ||
| 'abc' | ||
| """ | ||
| if self._version.local: | ||
| return ".".join(str(x) for x in self._version.local) | ||
| else: | ||
| return None | ||
| @property | ||
| def public(self) -> str: | ||
| """The public portion of the version. | ||
| >>> Version("1.2.3").public | ||
| '1.2.3' | ||
| >>> Version("1.2.3+abc").public | ||
| '1.2.3' | ||
| >>> Version("1.2.3+abc.dev1").public | ||
| '1.2.3' | ||
| """ | ||
| return str(self).split("+", 1)[0] | ||
| @property | ||
| def base_version(self) -> str: | ||
| """The "base version" of the version. | ||
| >>> Version("1.2.3").base_version | ||
| '1.2.3' | ||
| >>> Version("1.2.3+abc").base_version | ||
| '1.2.3' | ||
| >>> Version("1!1.2.3+abc.dev1").base_version | ||
| '1!1.2.3' | ||
| The "base version" is the public version of the project without any pre or post | ||
| release markers. | ||
| """ | ||
| parts = [] | ||
| # Epoch | ||
| if self.epoch != 0: | ||
| parts.append(f"{self.epoch}!") | ||
| # Release segment | ||
| parts.append(".".join(str(x) for x in self.release)) | ||
| return "".join(parts) | ||
| @property | ||
| def is_prerelease(self) -> bool: | ||
| """Whether this version is a pre-release. | ||
| >>> Version("1.2.3").is_prerelease | ||
| False | ||
| >>> Version("1.2.3a1").is_prerelease | ||
| True | ||
| >>> Version("1.2.3b1").is_prerelease | ||
| True | ||
| >>> Version("1.2.3rc1").is_prerelease | ||
| True | ||
| >>> Version("1.2.3dev1").is_prerelease | ||
| True | ||
| """ | ||
| return self.dev is not None or self.pre is not None | ||
| @property | ||
| def is_postrelease(self) -> bool: | ||
| """Whether this version is a post-release. | ||
| >>> Version("1.2.3").is_postrelease | ||
| False | ||
| >>> Version("1.2.3.post1").is_postrelease | ||
| True | ||
| """ | ||
| return self.post is not None | ||
| @property | ||
| def is_devrelease(self) -> bool: | ||
| """Whether this version is a development release. | ||
| >>> Version("1.2.3").is_devrelease | ||
| False | ||
| >>> Version("1.2.3.dev1").is_devrelease | ||
| True | ||
| """ | ||
| return self.dev is not None | ||
| @property | ||
| def major(self) -> int: | ||
| """The first item of :attr:`release` or ``0`` if unavailable. | ||
| >>> Version("1.2.3").major | ||
| 1 | ||
| """ | ||
| return self.release[0] if len(self.release) >= 1 else 0 | ||
| @property | ||
| def minor(self) -> int: | ||
| """The second item of :attr:`release` or ``0`` if unavailable. | ||
| >>> Version("1.2.3").minor | ||
| 2 | ||
| >>> Version("1").minor | ||
| 0 | ||
| """ | ||
| return self.release[1] if len(self.release) >= 2 else 0 | ||
| @property | ||
| def micro(self) -> int: | ||
| """The third item of :attr:`release` or ``0`` if unavailable. | ||
| >>> Version("1.2.3").micro | ||
| 3 | ||
| >>> Version("1").micro | ||
| 0 | ||
| """ | ||
| return self.release[2] if len(self.release) >= 3 else 0 | ||
| def _parse_letter_version( | ||
| letter: Optional[str], number: Union[str, bytes, SupportsInt, None] | ||
| ) -> Optional[Tuple[str, int]]: | ||
| if letter: | ||
| # We consider there to be an implicit 0 in a pre-release if there is | ||
| # not a numeral associated with it. | ||
| if number is None: | ||
| number = 0 | ||
| # We normalize any letters to their lower case form | ||
| letter = letter.lower() | ||
| # We consider some words to be alternate spellings of other words and | ||
| # in those cases we want to normalize the spellings to our preferred | ||
| # spelling. | ||
| if letter == "alpha": | ||
| letter = "a" | ||
| elif letter == "beta": | ||
| letter = "b" | ||
| elif letter in ["c", "pre", "preview"]: | ||
| letter = "rc" | ||
| elif letter in ["rev", "r"]: | ||
| letter = "post" | ||
| return letter, int(number) | ||
| if not letter and number: | ||
| # We assume if we are given a number, but we are not given a letter | ||
| # then this is using the implicit post release syntax (e.g. 1.0-1) | ||
| letter = "post" | ||
| return letter, int(number) | ||
| return None | ||
| _local_version_separators = re.compile(r"[\._-]") | ||
| def _parse_local_version(local: Optional[str]) -> Optional[LocalType]: | ||
| """ | ||
| Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). | ||
| """ | ||
| if local is not None: | ||
| return tuple( | ||
| part.lower() if not part.isdigit() else int(part) | ||
| for part in _local_version_separators.split(local) | ||
| ) | ||
| return None | ||
| 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], | ||
| ) -> CmpKey: | ||
| # When we compare a release version, we want to compare it with all of the | ||
| # trailing zeros removed. So we'll use a reverse the list, drop all the now | ||
| # leading zeros until we come to something non zero, then take the rest | ||
| # re-reverse it back into the correct order and make it a tuple and use | ||
| # that for our sorting key. | ||
| _release = tuple( | ||
| reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) | ||
| ) | ||
| # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. | ||
| # We'll do this by abusing the pre segment, but we _only_ want to do this | ||
| # if there is not a pre or a post segment. If we have one of those then | ||
| # the normal sorting rules will handle this case correctly. | ||
| if pre is None and post is None and dev is not None: | ||
| _pre: CmpPrePostDevType = NegativeInfinity | ||
| # Versions without a pre-release (except as noted above) should sort after | ||
| # those with one. | ||
| elif pre is None: | ||
| _pre = Infinity | ||
| else: | ||
| _pre = pre | ||
| # Versions without a post segment should sort before those with one. | ||
| if post is None: | ||
| _post: CmpPrePostDevType = NegativeInfinity | ||
| else: | ||
| _post = post | ||
| # Versions without a development segment should sort after those with one. | ||
| if dev is None: | ||
| _dev: CmpPrePostDevType = Infinity | ||
| else: | ||
| _dev = dev | ||
| if local is None: | ||
| # Versions without a local segment should sort before those with one. | ||
| _local: CmpLocalType = NegativeInfinity | ||
| else: | ||
| # Versions with a local segment need that segment parsed to implement | ||
| # the sorting rules in PEP440. | ||
| # - Alpha numeric segments sort before numeric segments | ||
| # - Alpha numeric segments sort lexicographically | ||
| # - Numeric segments sort numerically | ||
| # - Shorter versions sort before longer versions when the prefixes | ||
| # match exactly | ||
| _local = tuple( | ||
| (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local | ||
| ) | ||
| return epoch, _release, _pre, _post, _dev, _local |
| packaging==24.0 |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
4620322
-2.1%528
-3.3%74913
-2.71%