/
niceSOFT
/
packaging
Обзор
Документация
Войти
/
niceSOFT
/
packaging
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/test_version.py
1 449 строк
50 KB
dependabot[bot]
chore(deps): bump the pre-commit group with 5 updates (#1357)
03 авг 2026, 22:54
Не верифицирован
03 авг 2026, 22:54
f300ebf
Код
Авторство
О чём код?
# This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. from __future__ import annotations import itertools import operator import pickle import sys import typing import pretend import pytest from packaging._structures import Infinity, NegativeInfinity from packaging.version import ( InvalidVersion, Version, _BaseVersion, _TrimmedRelease, _VersionReplace, parse, ) if typing.TYPE_CHECKING: from collections.abc import Callable from typing_extensions import Self, Unpack if sys.version_info >= (3, 13): from copy import replace else: T = typing.TypeVar("T") class SupportsReplace(typing.Protocol): def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self: ... S = typing.TypeVar("S", bound="SupportsReplace") def replace(item: S, **kwargs: Unpack[_VersionReplace]) -> S: return item.__replace__(**kwargs) def test_parse() -> None: assert isinstance(parse("1.0"), Version) def test_parse_raises() -> None: with pytest.raises(InvalidVersion): parse("lolwat") # This list must be in the correct sorting order VERSIONS = [ # Implicit epoch of 0 "1.0.dev0", "1.0.dev456", "1.0.dev456+local", "1.0a0", "1.0a0.post0.dev0", "1.0a0.post0", "1.0a1.dev1", "1.0a1.dev1+local", "1.0a1", "1.0a1+local", "1.0b0", "1.0b1.dev456", "1.0b2", "1.0b2.post345.dev456", "1.0b2.post345", "1.0b2-346", "1.0rc0", "1.0rc1.dev1", "1.0c1", "1.0rc2", "1.0", "1.0.post0.dev0", "1.0.post0", "1.0.post456.dev34", "1.0.post456", "1.0.post456+local", "1.0.1.dev1", "1.0.1a1", "1.0.1", "1.0.1+local", "1.0.1.post1", "1.1.dev1", "1.2+a", "1.2+abc", "1.2+abcdef", "1.2+def", "1.2+0", "1.2+1", "1.2+1.abc", "1.2+1.1", "1.2+1.1.0", "1.2+2", "1.2+123", "1.2+123456", "1.2.r32+123456", "1.2.rev33+123456", # Explicit epoch of 1 "1!1.0.dev0", "1!1.0.dev456", "1!1.0.dev456+local", "1!1.0a0", "1!1.0a0.post0.dev0", "1!1.0a0.post0", "1!1.0a1.dev1", "1!1.0a1.dev1+local", "1!1.0a1", "1!1.0a1+local", "1!1.0b0", "1!1.0b1.dev456", "1!1.0b2", "1!1.0b2.post345.dev456", "1!1.0b2.post345", "1!1.0b2-346", "1!1.0rc0", "1!1.0rc1.dev1", "1!1.0c1", "1!1.0rc2", "1!1.0", "1!1.0.post0.dev0", "1!1.0.post0", "1!1.0.post456.dev34", "1!1.0.post456", "1!1.0.post456+local", "1!1.0.1.dev1", "1!1.0.1a1", "1!1.0.1", "1!1.0.1+local", "1!1.0.1.post1", "1!1.1.dev1", "1!1.2+a", "1!1.2+abc", "1!1.2+abcdef", "1!1.2+def", "1!1.2+0", "1!1.2+1", "1!1.2+1.abc", "1!1.2+1.1", "1!1.2+1.1.0", "1!1.2+2", "1!1.2+123", "1!1.2+123456", "1!1.2.r32+123456", "1!1.2.rev33+123456", ] # Simple _BaseVersion subclass for testing comparison with non-Version types class SimpleVersion(_BaseVersion): """A simple _BaseVersion subclass for testing cross-type comparisons.""" def __init__(self, key: typing.Any) -> None: # noqa: ANN401 # If key is a string, parse it as a version to create a compatible key if isinstance(key, str): parsed = Version(key) self._key_tuple = parsed._key else: self._key_tuple = key @property def _key(self) -> typing.Any: # noqa: ANN401 return self._key_tuple @_key.setter def _key(self, value: typing.Any) -> None: # noqa: ANN401 self._key_tuple = value class TestVersion: @pytest.mark.parametrize("version", VERSIONS) def test_valid_versions(self, version: str) -> None: Version(version) def test_match_args(self) -> None: assert Version.__match_args__ == ("_str",) assert Version("1.2")._str == "1.2" @pytest.mark.parametrize( "version", [ # Non sensical versions should be invalid "french toast", # Versions with invalid local versions "1.0+a+", "1.0++", "1.0+_foobar", "1.0+foo&asd", "1.0+1+1", # Spaces in versions are also invalid "1. 0", "1 .0", "1. 0a1", "1 .0a1", "1.0 a1", "1.0a 1", # Versions do need to be standard numbers "٠١٢.٣٤٥.٦٧٨٩", # Invalid versions that trigger the fast path (digits/dots only) ".", "..", "1..0", "1.0.", ".1.0", "1..2.3", # Local version which includes a non-ASCII letter that matches # regex '[a-z]' when re.IGNORECASE is in force and re.ASCII is not "1.0+\u0130", ], ) def test_invalid_versions(self, version: str) -> None: with pytest.raises(InvalidVersion): Version(version) @pytest.mark.parametrize("version", [None, 1, ["1", ".", "0"], ("1",), b"1.0"]) def test_non_string_versions_raise_invalid_version(self, version: object) -> None: with pytest.raises(InvalidVersion): Version(version) # type: ignore[arg-type] @pytest.mark.skipif( not hasattr(sys, "get_int_max_str_digits"), reason="requires int max str digits limit", ) @pytest.mark.parametrize( "version", [ # Simple path (digits only) "1" * 5000, # Regex path (has pre-release) "1.0a" + "1" * 5000, ], ) def test_oversized_version_raises_valueerror(self, version: str) -> None: old = sys.get_int_max_str_digits() sys.set_int_max_str_digits(4300) try: with pytest.raises(ValueError, match="Exceeds the limit"): Version(version) finally: sys.set_int_max_str_digits(old) @pytest.mark.parametrize( ("version", "normalized"), [ # Various development release incarnations ("1.0dev", "1.0.dev0"), ("1.0.dev", "1.0.dev0"), ("1.0dev1", "1.0.dev1"), ("1.0-dev", "1.0.dev0"), ("1.0-dev1", "1.0.dev1"), ("1.0DEV", "1.0.dev0"), ("1.0.DEV", "1.0.dev0"), ("1.0DEV1", "1.0.dev1"), ("1.0.DEV1", "1.0.dev1"), ("1.0-DEV", "1.0.dev0"), ("1.0-DEV1", "1.0.dev1"), # Various alpha incarnations ("1.0a", "1.0a0"), ("1.0.a", "1.0a0"), ("1.0.a1", "1.0a1"), ("1.0-a", "1.0a0"), ("1.0-a1", "1.0a1"), ("1.0alpha", "1.0a0"), ("1.0.alpha", "1.0a0"), ("1.0.alpha1", "1.0a1"), ("1.0-alpha", "1.0a0"), ("1.0-alpha1", "1.0a1"), ("1.0A", "1.0a0"), ("1.0.A", "1.0a0"), ("1.0.A1", "1.0a1"), ("1.0-A", "1.0a0"), ("1.0-A1", "1.0a1"), ("1.0ALPHA", "1.0a0"), ("1.0.ALPHA", "1.0a0"), ("1.0.ALPHA1", "1.0a1"), ("1.0-ALPHA", "1.0a0"), ("1.0-ALPHA1", "1.0a1"), # Various beta incarnations ("1.0b", "1.0b0"), ("1.0.b", "1.0b0"), ("1.0.b1", "1.0b1"), ("1.0-b", "1.0b0"), ("1.0-b1", "1.0b1"), ("1.0beta", "1.0b0"), ("1.0.beta", "1.0b0"), ("1.0.beta1", "1.0b1"), ("1.0-beta", "1.0b0"), ("1.0-beta1", "1.0b1"), ("1.0B", "1.0b0"), ("1.0.B", "1.0b0"), ("1.0.B1", "1.0b1"), ("1.0-B", "1.0b0"), ("1.0-B1", "1.0b1"), ("1.0BETA", "1.0b0"), ("1.0.BETA", "1.0b0"), ("1.0.BETA1", "1.0b1"), ("1.0-BETA", "1.0b0"), ("1.0-BETA1", "1.0b1"), # Various release candidate incarnations ("1.0c", "1.0rc0"), ("1.0.c", "1.0rc0"), ("1.0.c1", "1.0rc1"), ("1.0-c", "1.0rc0"), ("1.0-c1", "1.0rc1"), ("1.0rc", "1.0rc0"), ("1.0.rc", "1.0rc0"), ("1.0.rc1", "1.0rc1"), ("1.0-rc", "1.0rc0"), ("1.0-rc1", "1.0rc1"), ("1.0C", "1.0rc0"), ("1.0.C", "1.0rc0"), ("1.0.C1", "1.0rc1"), ("1.0-C", "1.0rc0"), ("1.0-C1", "1.0rc1"), ("1.0RC", "1.0rc0"), ("1.0.RC", "1.0rc0"), ("1.0.RC1", "1.0rc1"), ("1.0-RC", "1.0rc0"), ("1.0-RC1", "1.0rc1"), # Various post release incarnations ("1.0post", "1.0.post0"), ("1.0.post", "1.0.post0"), ("1.0post1", "1.0.post1"), ("1.0-post", "1.0.post0"), ("1.0-post1", "1.0.post1"), ("1.0POST", "1.0.post0"), ("1.0.POST", "1.0.post0"), ("1.0POST1", "1.0.post1"), ("1.0r", "1.0.post0"), ("1.0rev", "1.0.post0"), ("1.0.POST1", "1.0.post1"), ("1.0.r1", "1.0.post1"), ("1.0.rev1", "1.0.post1"), ("1.0-POST", "1.0.post0"), ("1.0-POST1", "1.0.post1"), ("1.0-5", "1.0.post5"), ("1.0-r5", "1.0.post5"), ("1.0-rev5", "1.0.post5"), # Local version case insensitivity ("1.0+AbC", "1.0+abc"), # Integer Normalization ("1.01", "1.1"), ("1.0a05", "1.0a5"), ("1.0b07", "1.0b7"), ("1.0c056", "1.0rc56"), ("1.0rc09", "1.0rc9"), ("1.0.post000", "1.0.post0"), ("1.1.dev09000", "1.1.dev9000"), ("00!1.2", "1.2"), ("0100!0.0", "100!0.0"), # Various other normalizations ("v1.0", "1.0"), (" v1.0\t\n", "1.0"), # Non-ASCII whitespace ("\N{NARROW NO-BREAK SPACE}1.0\t\N{PARAGRAPH SEPARATOR}\n ", "1.0"), ], ) def test_normalized_versions(self, version: str, normalized: str) -> None: assert str(Version(version)) == normalized @pytest.mark.parametrize( ("version", "expected"), [ ("1.0.dev456", "1.0.dev456"), ("1.0a1", "1.0a1"), ("1.0a2.dev456", "1.0a2.dev456"), ("1.0a12.dev456", "1.0a12.dev456"), ("1.0a12", "1.0a12"), ("1.0b1.dev456", "1.0b1.dev456"), ("1.0b2", "1.0b2"), ("1.0b2.post345.dev456", "1.0b2.post345.dev456"), ("1.0b2.post345", "1.0b2.post345"), ("1.0rc1.dev456", "1.0rc1.dev456"), ("1.0rc1", "1.0rc1"), ("1.0", "1.0"), ("1.0.post456.dev34", "1.0.post456.dev34"), ("1.0.post456", "1.0.post456"), ("1.0.1", "1.0.1"), ("0!1.0.2", "1.0.2"), ("1.0.3+7", "1.0.3+7"), ("0!1.0.4+8.0", "1.0.4+8.0"), ("1.0.5+9.5", "1.0.5+9.5"), ("1.2+1234.abc", "1.2+1234.abc"), ("1.2+123456", "1.2+123456"), ("1.2+123abc", "1.2+123abc"), ("1.2+123abc456", "1.2+123abc456"), ("1.2+abc", "1.2+abc"), ("1.2+abc123", "1.2+abc123"), ("1.2+abc123def", "1.2+abc123def"), ("1.1.dev1", "1.1.dev1"), ("7!1.0.dev456", "7!1.0.dev456"), ("7!1.0a1", "7!1.0a1"), ("7!1.0a2.dev456", "7!1.0a2.dev456"), ("7!1.0a12.dev456", "7!1.0a12.dev456"), ("7!1.0a12", "7!1.0a12"), ("7!1.0b1.dev456", "7!1.0b1.dev456"), ("7!1.0b2", "7!1.0b2"), ("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"), ("7!1.0b2.post345", "7!1.0b2.post345"), ("7!1.0rc1.dev456", "7!1.0rc1.dev456"), ("7!1.0rc1", "7!1.0rc1"), ("7!1.0", "7!1.0"), ("7!1.0.post456.dev34", "7!1.0.post456.dev34"), ("7!1.0.post456", "7!1.0.post456"), ("7!1.0.1", "7!1.0.1"), ("7!1.0.2", "7!1.0.2"), ("7!1.0.3+7", "7!1.0.3+7"), ("7!1.0.4+8.0", "7!1.0.4+8.0"), ("7!1.0.5+9.5", "7!1.0.5+9.5"), ("7!1.1.dev1", "7!1.1.dev1"), ], ) def test_version_str_repr(self, version: str, expected: str) -> None: assert str(Version(version)) == expected assert repr(Version(version)) == f"<Version({expected!r})>" def test_version_rc_and_c_equals(self) -> None: assert Version("1.0rc1") == Version("1.0c1") @pytest.mark.parametrize("version", VERSIONS) def test_version_hash(self, version: str) -> None: assert hash(Version(version)) == hash(Version(version)) def test_version_hash_with_warm_key_cache(self) -> None: v = Version("1.0") # Populate _key_cache via comparison before hashing assert v > Version("0.9") assert hash(v) == hash(Version("1.0")) @pytest.mark.parametrize( ("version", "public"), [ ("1.0", "1.0"), ("1.0.dev0", "1.0.dev0"), ("1.0.dev6", "1.0.dev6"), ("1.0a1", "1.0a1"), ("1.0a1.post5", "1.0a1.post5"), ("1.0a1.post5.dev6", "1.0a1.post5.dev6"), ("1.0rc4", "1.0rc4"), ("1.0.post5", "1.0.post5"), ("1!1.0", "1!1.0"), ("1!1.0.dev6", "1!1.0.dev6"), ("1!1.0a1", "1!1.0a1"), ("1!1.0a1.post5", "1!1.0a1.post5"), ("1!1.0a1.post5.dev6", "1!1.0a1.post5.dev6"), ("1!1.0rc4", "1!1.0rc4"), ("1!1.0.post5", "1!1.0.post5"), ("1.0+deadbeef", "1.0"), ("1.0.dev6+deadbeef", "1.0.dev6"), ("1.0a1+deadbeef", "1.0a1"), ("1.0a1.post5+deadbeef", "1.0a1.post5"), ("1.0a1.post5.dev6+deadbeef", "1.0a1.post5.dev6"), ("1.0rc4+deadbeef", "1.0rc4"), ("1.0.post5+deadbeef", "1.0.post5"), ("1!1.0+deadbeef", "1!1.0"), ("1!1.0.dev6+deadbeef", "1!1.0.dev6"), ("1!1.0a1+deadbeef", "1!1.0a1"), ("1!1.0a1.post5+deadbeef", "1!1.0a1.post5"), ("1!1.0a1.post5.dev6+deadbeef", "1!1.0a1.post5.dev6"), ("1!1.0rc4+deadbeef", "1!1.0rc4"), ("1!1.0.post5+deadbeef", "1!1.0.post5"), ], ) def test_version_public(self, version: str, public: str) -> None: assert Version(version).public == public @pytest.mark.parametrize( ("version", "base_version"), [ ("1.0", "1.0"), ("1.0.dev0", "1.0"), ("1.0.dev6", "1.0"), ("1.0a1", "1.0"), ("1.0a1.post5", "1.0"), ("1.0a1.post5.dev6", "1.0"), ("1.0rc4", "1.0"), ("1.0.post5", "1.0"), ("1!1.0", "1!1.0"), ("1!1.0.dev6", "1!1.0"), ("1!1.0a1", "1!1.0"), ("1!1.0a1.post5", "1!1.0"), ("1!1.0a1.post5.dev6", "1!1.0"), ("1!1.0rc4", "1!1.0"), ("1!1.0.post5", "1!1.0"), ("1.0+deadbeef", "1.0"), ("1.0.dev6+deadbeef", "1.0"), ("1.0a1+deadbeef", "1.0"), ("1.0a1.post5+deadbeef", "1.0"), ("1.0a1.post5.dev6+deadbeef", "1.0"), ("1.0rc4+deadbeef", "1.0"), ("1.0.post5+deadbeef", "1.0"), ("1!1.0+deadbeef", "1!1.0"), ("1!1.0.dev6+deadbeef", "1!1.0"), ("1!1.0a1+deadbeef", "1!1.0"), ("1!1.0a1.post5+deadbeef", "1!1.0"), ("1!1.0a1.post5.dev6+deadbeef", "1!1.0"), ("1!1.0rc4+deadbeef", "1!1.0"), ("1!1.0.post5+deadbeef", "1!1.0"), ], ) def test_version_base_version(self, version: str, base_version: str) -> None: assert Version(version).base_version == base_version @pytest.mark.parametrize( ("version", "epoch"), [ ("1.0", 0), ("1.0.dev0", 0), ("1.0.dev6", 0), ("1.0a1", 0), ("1.0a1.post5", 0), ("1.0a1.post5.dev6", 0), ("1.0rc4", 0), ("1.0.post5", 0), ("1!1.0", 1), ("1!1.0.dev6", 1), ("1!1.0a1", 1), ("1!1.0a1.post5", 1), ("1!1.0a1.post5.dev6", 1), ("1!1.0rc4", 1), ("1!1.0.post5", 1), ("1.0+deadbeef", 0), ("1.0.dev6+deadbeef", 0), ("1.0a1+deadbeef", 0), ("1.0a1.post5+deadbeef", 0), ("1.0a1.post5.dev6+deadbeef", 0), ("1.0rc4+deadbeef", 0), ("1.0.post5+deadbeef", 0), ("1!1.0+deadbeef", 1), ("1!1.0.dev6+deadbeef", 1), ("1!1.0a1+deadbeef", 1), ("1!1.0a1.post5+deadbeef", 1), ("1!1.0a1.post5.dev6+deadbeef", 1), ("1!1.0rc4+deadbeef", 1), ("1!1.0.post5+deadbeef", 1), ], ) def test_version_epoch(self, version: str, epoch: int) -> None: assert Version(version).epoch == epoch @pytest.mark.parametrize( ("version", "release"), [ ("1.0", (1, 0)), ("1.0.dev0", (1, 0)), ("1.0.dev6", (1, 0)), ("1.0a1", (1, 0)), ("1.0a1.post5", (1, 0)), ("1.0a1.post5.dev6", (1, 0)), ("1.0rc4", (1, 0)), ("1.0.post5", (1, 0)), ("1!1.0", (1, 0)), ("1!1.0.dev6", (1, 0)), ("1!1.0a1", (1, 0)), ("1!1.0a1.post5", (1, 0)), ("1!1.0a1.post5.dev6", (1, 0)), ("1!1.0rc4", (1, 0)), ("1!1.0.post5", (1, 0)), ("1.0+deadbeef", (1, 0)), ("1.0.dev6+deadbeef", (1, 0)), ("1.0a1+deadbeef", (1, 0)), ("1.0a1.post5+deadbeef", (1, 0)), ("1.0a1.post5.dev6+deadbeef", (1, 0)), ("1.0rc4+deadbeef", (1, 0)), ("1.0.post5+deadbeef", (1, 0)), ("1!1.0+deadbeef", (1, 0)), ("1!1.0.dev6+deadbeef", (1, 0)), ("1!1.0a1+deadbeef", (1, 0)), ("1!1.0a1.post5+deadbeef", (1, 0)), ("1!1.0a1.post5.dev6+deadbeef", (1, 0)), ("1!1.0rc4+deadbeef", (1, 0)), ("1!1.0.post5+deadbeef", (1, 0)), ], ) def test_version_release(self, version: str, release: tuple[int, int]) -> None: assert Version(version).release == release @pytest.mark.parametrize( ("version", "local"), [ ("1.0", None), ("1.0.dev0", None), ("1.0.dev6", None), ("1.0a1", None), ("1.0a1.post5", None), ("1.0a1.post5.dev6", None), ("1.0rc4", None), ("1.0.post5", None), ("1!1.0", None), ("1!1.0.dev6", None), ("1!1.0a1", None), ("1!1.0a1.post5", None), ("1!1.0a1.post5.dev6", None), ("1!1.0rc4", None), ("1!1.0.post5", None), ("1.0+deadbeef", "deadbeef"), ("1.0.dev6+deadbeef", "deadbeef"), ("1.0a1+deadbeef", "deadbeef"), ("1.0a1.post5+deadbeef", "deadbeef"), ("1.0a1.post5.dev6+deadbeef", "deadbeef"), ("1.0rc4+deadbeef", "deadbeef"), ("1.0.post5+deadbeef", "deadbeef"), ("1!1.0+deadbeef", "deadbeef"), ("1!1.0.dev6+deadbeef", "deadbeef"), ("1!1.0a1+deadbeef", "deadbeef"), ("1!1.0a1.post5+deadbeef", "deadbeef"), ("1!1.0a1.post5.dev6+deadbeef", "deadbeef"), ("1!1.0rc4+deadbeef", "deadbeef"), ("1!1.0.post5+deadbeef", "deadbeef"), ], ) def test_version_local(self, version: str, local: str | None) -> None: assert Version(version).local == local @pytest.mark.parametrize( ("version", "pre"), [ ("1.0", None), ("1.0.dev0", None), ("1.0.dev6", None), ("1.0a1", ("a", 1)), ("1.0a1.post5", ("a", 1)), ("1.0a1.post5.dev6", ("a", 1)), ("1.0rc4", ("rc", 4)), ("1.0.post5", None), ("1!1.0", None), ("1!1.0.dev6", None), ("1!1.0a1", ("a", 1)), ("1!1.0a1.post5", ("a", 1)), ("1!1.0a1.post5.dev6", ("a", 1)), ("1!1.0rc4", ("rc", 4)), ("1!1.0.post5", None), ("1.0+deadbeef", None), ("1.0.dev6+deadbeef", None), ("1.0a1+deadbeef", ("a", 1)), ("1.0a1.post5+deadbeef", ("a", 1)), ("1.0a1.post5.dev6+deadbeef", ("a", 1)), ("1.0rc4+deadbeef", ("rc", 4)), ("1.0.post5+deadbeef", None), ("1!1.0+deadbeef", None), ("1!1.0.dev6+deadbeef", None), ("1!1.0a1+deadbeef", ("a", 1)), ("1!1.0a1.post5+deadbeef", ("a", 1)), ("1!1.0a1.post5.dev6+deadbeef", ("a", 1)), ("1!1.0rc4+deadbeef", ("rc", 4)), ("1!1.0.post5+deadbeef", None), ], ) def test_version_pre(self, version: str, pre: tuple[str, int] | None) -> None: assert Version(version).pre == pre @pytest.mark.parametrize( ("version", "expected"), [ ("1.0.dev0", True), ("1.0.dev1", True), ("1.0a1.dev1", True), ("1.0b1.dev1", True), ("1.0c1.dev1", True), ("1.0rc1.dev1", True), ("1.0a1", True), ("1.0b1", True), ("1.0c1", True), ("1.0rc1", True), ("1.0a1.post1.dev1", True), ("1.0b1.post1.dev1", True), ("1.0c1.post1.dev1", True), ("1.0rc1.post1.dev1", True), ("1.0a1.post1", True), ("1.0b1.post1", True), ("1.0c1.post1", True), ("1.0rc1.post1", True), ("1.0", False), ("1.0+dev", False), ("1.0.post1", False), ("1.0.post1+dev", False), ], ) def test_version_is_prerelease(self, version: str, expected: bool) -> None: assert Version(version).is_prerelease is expected @pytest.mark.parametrize( ("version", "dev"), [ ("1.0", None), ("1.0.dev0", 0), ("1.0.dev6", 6), ("1.0a1", None), ("1.0a1.post5", None), ("1.0a1.post5.dev6", 6), ("1.0rc4", None), ("1.0.post5", None), ("1!1.0", None), ("1!1.0.dev6", 6), ("1!1.0a1", None), ("1!1.0a1.post5", None), ("1!1.0a1.post5.dev6", 6), ("1!1.0rc4", None), ("1!1.0.post5", None), ("1.0+deadbeef", None), ("1.0.dev6+deadbeef", 6), ("1.0a1+deadbeef", None), ("1.0a1.post5+deadbeef", None), ("1.0a1.post5.dev6+deadbeef", 6), ("1.0rc4+deadbeef", None), ("1.0.post5+deadbeef", None), ("1!1.0+deadbeef", None), ("1!1.0.dev6+deadbeef", 6), ("1!1.0a1+deadbeef", None), ("1!1.0a1.post5+deadbeef", None), ("1!1.0a1.post5.dev6+deadbeef", 6), ("1!1.0rc4+deadbeef", None), ("1!1.0.post5+deadbeef", None), ], ) def test_version_dev(self, version: str, dev: int | None) -> None: assert Version(version).dev == dev @pytest.mark.parametrize( ("version", "expected"), [ ("1.0", False), ("1.0.dev0", True), ("1.0.dev6", True), ("1.0a1", False), ("1.0a1.post5", False), ("1.0a1.post5.dev6", True), ("1.0rc4", False), ("1.0.post5", False), ("1!1.0", False), ("1!1.0.dev6", True), ("1!1.0a1", False), ("1!1.0a1.post5", False), ("1!1.0a1.post5.dev6", True), ("1!1.0rc4", False), ("1!1.0.post5", False), ("1.0+deadbeef", False), ("1.0.dev6+deadbeef", True), ("1.0a1+deadbeef", False), ("1.0a1.post5+deadbeef", False), ("1.0a1.post5.dev6+deadbeef", True), ("1.0rc4+deadbeef", False), ("1.0.post5+deadbeef", False), ("1!1.0+deadbeef", False), ("1!1.0.dev6+deadbeef", True), ("1!1.0a1+deadbeef", False), ("1!1.0a1.post5+deadbeef", False), ("1!1.0a1.post5.dev6+deadbeef", True), ("1!1.0rc4+deadbeef", False), ("1!1.0.post5+deadbeef", False), ], ) def test_version_is_devrelease(self, version: str, expected: bool) -> None: assert Version(version).is_devrelease is expected @pytest.mark.parametrize( ("version", "post"), [ ("1.0", None), ("1.0.dev0", None), ("1.0.dev6", None), ("1.0a1", None), ("1.0a1.post5", 5), ("1.0a1.post5.dev6", 5), ("1.0rc4", None), ("1.0.post5", 5), ("1!1.0", None), ("1!1.0.dev6", None), ("1!1.0a1", None), ("1!1.0a1.post5", 5), ("1!1.0a1.post5.dev6", 5), ("1!1.0rc4", None), ("1!1.0.post5", 5), ("1.0+deadbeef", None), ("1.0.dev6+deadbeef", None), ("1.0a1+deadbeef", None), ("1.0a1.post5+deadbeef", 5), ("1.0a1.post5.dev6+deadbeef", 5), ("1.0rc4+deadbeef", None), ("1.0.post5+deadbeef", 5), ("1!1.0+deadbeef", None), ("1!1.0.dev6+deadbeef", None), ("1!1.0a1+deadbeef", None), ("1!1.0a1.post5+deadbeef", 5), ("1!1.0a1.post5.dev6+deadbeef", 5), ("1!1.0rc4+deadbeef", None), ("1!1.0.post5+deadbeef", 5), ], ) def test_version_post(self, version: str, post: int | None) -> None: assert Version(version).post == post @pytest.mark.parametrize( ("version", "expected"), [ ("1.0.dev1", False), ("1.0", False), ("1.0+foo", False), ("1.0.post1.dev1", True), ("1.0.post1", True), ], ) def test_version_is_postrelease(self, version: str, expected: bool) -> None: assert Version(version).is_postrelease is expected @pytest.mark.parametrize( ("left", "right", "op"), # Below we'll generate every possible combination of VERSIONS that # should be True for the given operator list( itertools.chain.from_iterable( # Verify that the less than (<) operator works correctly [ [(x, y, operator.lt) for y in VERSIONS[i + 1 :]] for i, x in enumerate(VERSIONS) ] + # Verify that the less than equal (<=) operator works correctly [ [(x, y, operator.le) for y in VERSIONS[i:]] for i, x in enumerate(VERSIONS) ] + # Verify that the equal (==) operator works correctly [[(x, x, operator.eq) for x in VERSIONS]] + # Verify that the not equal (!=) operator works correctly [ [(x, y, operator.ne) for j, y in enumerate(VERSIONS) if i != j] for i, x in enumerate(VERSIONS) ] + # Verify that the greater than equal (>=) operator works correctly [ [(x, y, operator.ge) for y in VERSIONS[: i + 1]] for i, x in enumerate(VERSIONS) ] + # Verify that the greater than (>) operator works correctly [ [(x, y, operator.gt) for y in VERSIONS[:i]] for i, x in enumerate(VERSIONS) ] ) ), ) def test_comparison_true( self, left: str, right: str, op: Callable[[Version, Version], bool] ) -> None: assert op(Version(left), Version(right)) @pytest.mark.parametrize( ("left", "right", "op"), # Below we'll generate every possible combination of VERSIONS that # should be False for the given operator list( itertools.chain.from_iterable( # Verify that the less than (<) operator works correctly [ [(x, y, operator.lt) for y in VERSIONS[: i + 1]] for i, x in enumerate(VERSIONS) ] + # Verify that the less than equal (<=) operator works correctly [ [(x, y, operator.le) for y in VERSIONS[:i]] for i, x in enumerate(VERSIONS) ] + # Verify that the equal (==) operator works correctly [ [(x, y, operator.eq) for j, y in enumerate(VERSIONS) if i != j] for i, x in enumerate(VERSIONS) ] + # Verify that the not equal (!=) operator works correctly [[(x, x, operator.ne) for x in VERSIONS]] + # Verify that the greater than equal (>=) operator works correctly [ [(x, y, operator.ge) for y in VERSIONS[i + 1 :]] for i, x in enumerate(VERSIONS) ] + # Verify that the greater than (>) operator works correctly [ [(x, y, operator.gt) for y in VERSIONS[i:]] for i, x in enumerate(VERSIONS) ] ) ), ) def test_comparison_false( self, left: str, right: str, op: Callable[[Version, Version], bool] ) -> None: assert not op(Version(left), Version(right)) @pytest.mark.parametrize("op", ["lt", "le", "eq", "ge", "gt", "ne"]) def test_dunder_op_returns_notimplemented(self, op: str) -> None: method = getattr(Version, f"__{op}__") assert method(Version("1"), 1) is NotImplemented @pytest.mark.parametrize(("op", "expected"), [("eq", False), ("ne", True)]) def test_compare_other(self, op: str, expected: bool) -> None: other = pretend.stub(**{f"__{op}__": lambda _: NotImplemented}) assert getattr(operator, op)(Version("1"), other) is expected @pytest.mark.parametrize( "op", ["__lt__", "__le__", "__eq__", "__ge__", "__gt__", "__ne__"] ) def test_base_version_notimplemented_with_non_base_version(self, op: str) -> None: """Test _BaseVersion returns NotImplemented with non-_BaseVersion.""" v = SimpleVersion("1.0") assert getattr(v, op)(1) is NotImplemented def test_base_version_hash(self) -> None: """Test that _BaseVersion hash works""" v = SimpleVersion("1.0") assert isinstance(hash(v), int) def test_base_version_ne_with_base_version(self) -> None: """Test _BaseVersion.__ne__ with another _BaseVersion.""" v1 = SimpleVersion("1.0") v2 = SimpleVersion("2.0") assert v1 != v2 def test_gt_with_cached_other(self) -> None: """__gt__ fast path when other already has a cached key.""" other = Version("1.0") # Warm other's key cache. _ = other < Version("2.0") # Fresh version on the left, cached one on the right. assert Version("2.0") > other def test_version_compare_with_base_version_subclass(self) -> None: """Test Version comparison with another _BaseVersion subclass""" v1 = Version("1.0") v2 = SimpleVersion("1.0") # All comparisons should work with compatible keys assert v1 == v2 assert v1 <= v2 assert v1 >= v2 assert v1 == v2 assert not (v1 < v2) assert not (v1 > v2) # Test with different versions to exercise != path v3 = Version("1.0") v4 = SimpleVersion("2.0") assert v3 != v4 def test_version_ne_with_uncached_keys(self) -> None: """Test Version.__ne__ populates cache when comparing with another Version""" v1 = Version("1.0") v2 = Version("2.0") # Test with both caches None result = v1 != v2 assert result is True # Test with v1 cached, v3 uncached v3 = Version("1.5") result = v1 != v3 assert result is True # Test with v3 cached, v4 uncached (the reverse case) v4 = Version("1.2") result = v4 != v3 assert result is True def test_version_le_with_uncached_keys(self) -> None: """Test Version.__le__ populates cache when comparing with another Version""" v1 = Version("1.0") v2 = Version("2.0") # Test <= with both caches None result = v1 <= v2 assert result is True # Test with v1 cached (from above), v3 uncached v3 = Version("1.5") result = v1 <= v3 assert result is True def test_major_version(self) -> None: assert Version("2.1.0").major == 2 def test_minor_version(self) -> None: assert Version("2.1.0").minor == 1 assert Version("2").minor == 0 def test_micro_version(self) -> None: assert Version("2.1.3").micro == 3 assert Version("2.1").micro == 0 assert Version("2").micro == 0 # Tests for replace() method def test_replace_no_args(self) -> None: """replace() with no arguments should return an equivalent version""" v = Version("1.2.3a1.post2.dev3+local") v_replaced = replace(v) assert v == v_replaced assert str(v) == str(v_replaced) def test_replace_epoch(self) -> None: v = Version("1.2.3") assert str(replace(v, epoch=2)) == "2!1.2.3" assert replace(v, epoch=0).epoch == 0 v_with_epoch = Version("1!1.2.3") assert str(replace(v_with_epoch, epoch=2)) == "2!1.2.3" assert str(replace(v_with_epoch, epoch=None)) == "1.2.3" def test_replace_release_tuple(self) -> None: v = Version("1.2.3") assert str(replace(v, release=(2, 0, 0))) == "2.0.0" assert str(replace(v, release=(1,))) == "1" assert str(replace(v, release=(1, 2, 3, 4, 5))) == "1.2.3.4.5" def test_replace_release_none(self) -> None: v = Version("1.2.3") assert str(replace(v, release=None)) == "0" def test_replace_pre_alpha(self) -> None: v = Version("1.2.3") assert str(replace(v, pre=("a", 1))) == "1.2.3a1" assert str(replace(v, pre=("A", 0))) == "1.2.3a0" assert str(replace(v, pre=("Alpha", 2))) == "1.2.3a2" def test_replace_pre_alpha_none(self) -> None: v = Version("1.2.3a1") assert str(replace(v, pre=None)) == "1.2.3" def test_replace_pre_beta(self) -> None: v = Version("1.2.3") assert str(replace(v, pre=("b", 1))) == "1.2.3b1" assert str(replace(v, pre=("b", 0))) == "1.2.3b0" assert str(replace(v, pre=("bEta", 2))) == "1.2.3b2" def test_replace_pre_beta_none(self) -> None: v = Version("1.2.3b1") assert str(replace(v, pre=None)) == "1.2.3" def test_replace_pre_rc(self) -> None: v = Version("1.2.3") assert str(replace(v, pre=("rc", 1))) == "1.2.3rc1" assert str(replace(v, pre=("rc", 0))) == "1.2.3rc0" def test_replace_pre_rc_none(self) -> None: v = Version("1.2.3rc1") assert str(replace(v, pre=None)) == "1.2.3" def test_replace_post(self) -> None: v = Version("1.2.3") assert str(replace(v, post=1)) == "1.2.3.post1" assert str(replace(v, post=0)) == "1.2.3.post0" def test_replace_post_none(self) -> None: v = Version("1.2.3.post1") assert str(replace(v, post=None)) == "1.2.3" def test_replace_dev(self) -> None: v = Version("1.2.3") assert str(replace(v, dev=1)) == "1.2.3.dev1" assert str(replace(v, dev=0)) == "1.2.3.dev0" def test_replace_dev_none(self) -> None: v = Version("1.2.3.dev1") assert str(replace(v, dev=None)) == "1.2.3" def test_replace_local_string(self) -> None: v = Version("1.2.3") assert str(replace(v, local="abc")) == "1.2.3+abc" assert str(replace(v, local="abc.123")) == "1.2.3+abc.123" assert str(replace(v, local="abc-123")) == "1.2.3+abc.123" def test_replace_local_none(self) -> None: v = Version("1.2.3+local") assert str(replace(v, local=None)) == "1.2.3" def test_replace_multiple_components(self) -> None: v = Version("1.2.3") assert str(replace(v, pre=("a", 1), post=1)) == "1.2.3a1.post1" assert str(replace(v, release=(2, 0, 0), pre=("b", 2), dev=1)) == "2.0.0b2.dev1" assert str(replace(v, epoch=1, release=(3, 0), local="abc")) == "1!3.0+abc" def test_replace_clear_all_optional(self) -> None: v = Version("1!1.2.3a1.post2.dev3+local") cleared = replace(v, epoch=None, pre=None, post=None, dev=None, local=None) assert str(cleared) == "1.2.3" def test_replace_preserves_comparison(self) -> None: v1 = Version("1.2.3") v2 = Version("1.2.4") v1_new = replace(v1, release=(1, 2, 4)) assert v1_new == v2 assert v1 < v2 assert v1_new >= v2 def test_replace_preserves_hash(self) -> None: v1 = Version("1.2.3") v2 = replace(v1, release=(1, 2, 3)) assert hash(v1) == hash(v2) v3 = replace(v1, release=(2, 0, 0)) assert hash(v1) != hash(v3) def test_replace_returns_same_instance_when_unchanged(self) -> None: """replace() returns the exact same object when no components change""" v = Version("1.2.3a1.post2.dev3+local") assert replace(v) is v assert replace(v, epoch=0) is v assert replace(v, release=(1, 2, 3)) is v assert replace(v, pre=("a", 1)) is v assert replace(v, post=2) is v assert replace(v, dev=3) is v assert replace(v, local="local") is v def test_replace_change_pre_type(self) -> None: """Can change from one pre-release type to another""" v = Version("1.2.3a1") assert str(replace(v, pre=("b", 2))) == "1.2.3b2" assert str(replace(v, pre=("rc", 1))) == "1.2.3rc1" v2 = Version("1.2.3rc5") assert str(replace(v2, pre=("a", 0))) == "1.2.3a0" def test_replace_invalid_epoch_type(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="epoch must be non-negative"): replace(v, epoch="1") # type: ignore[arg-type] def test_replace_invalid_post_type(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="post must be non-negative"): replace(v, post="1") # type: ignore[arg-type] def test_replace_invalid_dev_type(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="dev must be non-negative"): replace(v, dev="1") # type: ignore[arg-type] def test_replace_invalid_epoch_negative(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="epoch must be non-negative"): replace(v, epoch=-1) def test_replace_invalid_release_empty(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="release must be a non-empty tuple"): replace(v, release=()) def test_replace_invalid_release_tuple_content(self) -> None: v = Version("1.2.3") with pytest.raises( InvalidVersion, match="release must be a non-empty tuple of non-negative" ): replace(v, release=(1, -2, 3)) def test_replace_invalid_pre_negative(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="pre must be a tuple"): replace(v, pre=("a", -1)) def test_replace_invalid_pre_type(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="pre must be a tuple"): replace(v, pre=("x", 1)) def test_replace_invalid_pre_format(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="pre must be a tuple"): replace(v, pre="a1") # type: ignore[arg-type] with pytest.raises(InvalidVersion, match="pre must be a tuple"): replace(v, pre=("a",)) # type: ignore[arg-type] with pytest.raises(InvalidVersion, match="pre must be a tuple"): replace(v, pre=("a", 1, 2)) # type: ignore[arg-type] def test_replace_invalid_post_negative(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="post must be non-negative"): replace(v, post=-1) def test_replace_invalid_dev_negative(self) -> None: v = Version("1.2.3") with pytest.raises(InvalidVersion, match="dev must be non-negative"): replace(v, dev=-1) def test_replace_invalid_local_string(self) -> None: v = Version("1.2.3") with pytest.raises( InvalidVersion, match="local must be a valid version string" ): replace(v, local="abc+123") with pytest.raises( InvalidVersion, match="local must be a valid version string" ): replace(v, local="+abc") # Taken from hatchling 1.28 def reset_version_parts(version: Version, **kwargs: typing.Any) -> None: # noqa: ANN401 # https://github.com/pypa/packaging/blob/20.9/packaging/version.py#L301-L310 internal_version = version._version parts: dict[str, typing.Any] = {} ordered_part_names = ("epoch", "release", "pre", "post", "dev", "local") reset = False for part_name in ordered_part_names: if reset: parts[part_name] = kwargs.get(part_name) elif part_name in kwargs: parts[part_name] = kwargs[part_name] reset = True else: parts[part_name] = getattr(internal_version, part_name) version._version = type(internal_version)(**parts) # These will be deprecated in 26.1, and removed in the future def test_deprecated__version() -> None: v = Version("1.2.3") with pytest.warns(DeprecationWarning, match="is private"): assert v._version.release == (1, 2, 3) def test_hatchling_usage__version() -> None: v = Version("2.3.4") with pytest.warns(DeprecationWarning, match="is private"): reset_version_parts(v, post=("post", 1)) assert v == Version("2.3.4.post1") @pytest.mark.parametrize( ("args", "string"), [ ({"release": (1, 2, 3)}, "1.2.3"), ({"release": (1, 2, 3), "epoch": 2}, "2!1.2.3"), ({"release": (1, 2, 3), "pre": ("b", 1)}, "1.2.3b1"), ({"release": (1, 2, 3), "pre": ("B", 1)}, "1.2.3b1"), ({"release": (1, 2, 3), "pre": ("beta", 1)}, "1.2.3b1"), ({"release": (1, 2, 3), "post": 2}, "1.2.3post2"), ({"release": (1, 2, 3), "dev": 3}, "1.2.3.dev3"), ({"release": (1, 2, 3), "local": "abc"}, "1.2.3+abc"), ( { "release": (1, 2, 3), "epoch": None, "pre": None, "post": None, "dev": None, "local": None, }, "1.2.3", ), ( { "release": (2, 3, 4), "epoch": 1, "pre": ("a", 5), "post": 6, "dev": 7, "local": "zzz", }, "1!2.3.4a5.post6.dev7+zzz", ), ], ) def test_from_parts(args: dict[str, typing.Any], string: str) -> None: v = Version.from_parts(**args) assert v == Version(string) def test_from_parts_rejects_non_str_pre_letter() -> None: # A non-string pre letter must raise InvalidVersion rather than escaping # as an AttributeError from the normalization step. with pytest.raises(InvalidVersion, match="pre must be a tuple"): Version.from_parts(release=(1,), pre=(1, 1)) # type: ignore[arg-type] @pytest.mark.parametrize( "version", [ "1.2.3", "0.1.0", "2.0a1", "1.0b2", "3.0rc1", "1.0.post1", "1.0.dev3", "1!2.3.4a5.post6.dev7+zzz", ], ) def test_pickle_roundtrip(version: str) -> None: # Make sure equality and str() work between a pickle/unpickle round trip. v = Version(version) loaded = pickle.loads(pickle.dumps(v)) assert loaded == v assert str(loaded) == str(v) # Pickle bytes generated with packaging==25.0, Python 3.13.1, pickle protocol 2. # These contain references to packaging._structures.InfinityType and # NegativeInfinityType in the _key cache, which were removed in packaging 26.1. _PACKAGING_25_0_PICKLE_V1_2_3 = ( b"\x80\x02cpackaging.version\nVersion\nq\x00)\x81q\x01}q\x02" b"(X\x08\x00\x00\x00_versionq\x03cpackaging.version\n_Version\n" b"q\x04(K\x00K\x01K\x02K\x03\x87q\x05NNNNtq\x06\x81q\x07X\x04" b"\x00\x00\x00_keyq\x08(K\x00K\x01K\x02K\x03\x87q\tcpackaging._structures\n" b"InfinityType\nq\n)\x81q\x0bcpackaging._structures\nNegativeInfinityType\n" b"q\x0c)\x81q\rh\x0bh\rtq\x0eub." ) _PACKAGING_25_0_PICKLE_V2_0A1 = ( b"\x80\x02cpackaging.version\nVersion\nq\x00)\x81q\x01}q\x02" b"(X\x08\x00\x00\x00_versionq\x03cpackaging.version\n_Version\n" b"q\x04(K\x00K\x02K\x00\x86q\x05NX\x01\x00\x00\x00aq\x06K\x01" b"\x86q\x07NNtq\x08\x81q\tX\x04\x00\x00\x00_keyq\n(K\x00K\x02" b"\x85q\x0bh\x07cpackaging._structures\nNegativeInfinityType\n" b"q\x0c)\x81q\rcpackaging._structures\nInfinityType\nq\x0e)\x81" b"q\x0fh\rtq\x10ub." ) def test_pickle_old_format_loads() -> None: # Verify that pickles created with packaging <= 25.x can be loaded # and produce correct Version objects. v = pickle.loads(_PACKAGING_25_0_PICKLE_V1_2_3) assert isinstance(v, Version) assert str(v) == "1.2.3" assert v == Version("1.2.3") assert v < Version("2.0") assert v > Version("1.2.2") v2 = pickle.loads(_PACKAGING_25_0_PICKLE_V2_0A1) assert isinstance(v2, Version) assert str(v2) == "2.0a1" assert v2 == Version("2.0a1") assert v2 < Version("2.0") def test_pickle_old_format_re_pickled_is_clean() -> None: # Verify that loading an old pickle and re-pickling it produces # a clean payload that no longer references packaging._structures. v = pickle.loads(_PACKAGING_25_0_PICKLE_V1_2_3) new_data = pickle.dumps(v) assert b"_structures" not in new_data # And the re-pickled version still works. v2 = pickle.loads(new_data) assert v2 == Version("1.2.3") assert str(v2) == "1.2.3" # Pickle bytes generated with packaging==26.0, Python 3.13.1, pickle protocol 2. # 26.0 used __slots__ (no __dict__), so the pickle state is (None, {slot: value}). # The _key_cache slot still contains packaging._structures.InfinityType references. _PACKAGING_26_0_PICKLE_V1_2_3 = ( b"\x80\x02cpackaging.version\nVersion\nq\x00)\x81q\x01N}q\x02" b"(X\x04\x00\x00\x00_devq\x03NX\x06\x00\x00\x00_epochq\x04K\x00" b"X\n\x00\x00\x00_key_cacheq\x05(K\x00K\x01K\x02K\x03\x87q\x06" b"cpackaging._structures\nInfinityType\nq\x07)\x81q\x08cpackaging._structures\n" b"NegativeInfinityType\nq\t)\x81q\nh\x08h\ntq\x0bX\x06\x00\x00\x00" b"_localq\x0cNX\x05\x00\x00\x00_postq\rNX\x04\x00\x00\x00_preq\x0e" b"NX\x08\x00\x00\x00_releaseq\x0fh\x06u\x86q\x10b." ) def test_pickle_26_0_slots_format_loads() -> None: # Verify that pickles created with packaging 26.0 (__slots__, no __reduce__) # can be loaded and produce correct Version objects. v = pickle.loads(_PACKAGING_26_0_PICKLE_V1_2_3) assert isinstance(v, Version) assert str(v) == "1.2.3" assert v == Version("1.2.3") assert v < Version("2.0") assert v > Version("1.2.2") # Pickle bytes generated with packaging 26.2+ (6-tuple __getstate__ format), # Python 3.13.1, pickle protocol 2. _PACKAGING_26_2_TUPLE_PICKLE_V1E2_3_4A5_POST6_DEV7_ZZZ = ( b"\x80\x02cpackaging.version\nVersion\nq\x00)\x81q\x01(K\x01K\x02K\x03" b"K\x04\x87q\x02X\x01\x00\x00\x00aq\x03K\x05\x86q\x04X\x04\x00\x00" b"\x00postq\x05K\x06\x86q\x06X\x03\x00\x00\x00devq\x07K\x07\x86q\x08" b"X\x03\x00\x00\x00zzzq\t\x85q\ntq\x0bb." ) def test_pickle_26_2_tuple_getstate_loads() -> None: # Verify that pickles created with packaging 26.2+ (6-tuple __getstate__) # can be loaded and produce correct Version objects. v = pickle.loads(_PACKAGING_26_2_TUPLE_PICKLE_V1E2_3_4A5_POST6_DEV7_ZZZ) assert isinstance(v, Version) assert str(v) == "1!2.3.4a5.post6.dev7+zzz" assert v == Version("1!2.3.4a5.post6.dev7+zzz") assert v.epoch == 1 assert v.release == (2, 3, 4) assert v.pre == ("a", 5) assert v.post == 6 assert v.dev == 7 assert v.local == "zzz" def test_pickle_setstate_rejects_invalid_state() -> None: # Cover the TypeError branches in __setstate__ for invalid input. v = Version.__new__(Version) # dict without "_version" key with pytest.raises(TypeError, match="Cannot restore Version"): v.__setstate__({"bad_key": 123}) # tuple with non-dict second element with pytest.raises(TypeError, match="Cannot restore Version"): v.__setstate__((None, "not_a_dict")) # tuple with unexpected length (not 2 or 6) with pytest.raises(TypeError, match="Cannot restore Version"): v.__setstate__((1, 2, 3)) # completely wrong type with pytest.raises(TypeError, match="Cannot restore Version"): v.__setstate__(12345) def test_structures_shim_repr() -> None: # Cover the __repr__ methods on the backward-compatibility shim classes. assert repr(Infinity) == "Infinity" assert repr(NegativeInfinity) == "-Infinity" def test_trimmed_release_hash_from_unhashed_version() -> None: # Regression test for GH-1239: hashing a _TrimmedRelease built from a fresh # Version must not raise AttributeError from the uncopied _hash_cache slot. v = Version("1.0") tr = _TrimmedRelease(v) assert hash(tr) == hash(v) def test_trimmed_release_hash_from_prehashed_version() -> None: # Hashing a _TrimmedRelease built from a Version whose hash has already # been computed and cached must also work correctly. v = Version("1.0") _ = hash(v) # Populate the hash cache on the source Version. tr = _TrimmedRelease(v) assert hash(tr) == hash(v)