1# This file is dual licensed under the terms of the Apache License, Version 
    2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 
    3# for complete details. 
    4""" 
    5.. testsetup:: 
    6 
    7    from packaging.version import parse, Version 
    8""" 
    9 
    10from __future__ import annotations 
    11 
    12import itertools 
    13import re 
    14from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union 
    15 
    16from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType 
    17 
    18__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] 
    19 
    20LocalType = Tuple[Union[int, str], ...] 
    21 
    22CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] 
    23CmpLocalType = Union[ 
    24    NegativeInfinityType, 
    25    Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], 
    26] 
    27CmpKey = Tuple[ 
    28    int, 
    29    Tuple[int, ...], 
    30    CmpPrePostDevType, 
    31    CmpPrePostDevType, 
    32    CmpPrePostDevType, 
    33    CmpLocalType, 
    34] 
    35VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] 
    36 
    37 
    38class _Version(NamedTuple): 
    39    epoch: int 
    40    release: tuple[int, ...] 
    41    dev: tuple[str, int] | None 
    42    pre: tuple[str, int] | None 
    43    post: tuple[str, int] | None 
    44    local: LocalType | None 
    45 
    46 
    47def parse(version: str) -> Version: 
    48    """Parse the given version string. 
    49 
    50    >>> parse('1.0.dev1') 
    51    <Version('1.0.dev1')> 
    52 
    53    :param version: The version string to parse. 
    54    :raises InvalidVersion: When the version string is not a valid version. 
    55    """ 
    56    return Version(version) 
    57 
    58 
    59class InvalidVersion(ValueError): 
    60    """Raised when a version string is not a valid version. 
    61 
    62    >>> Version("invalid") 
    63    Traceback (most recent call last): 
    64        ... 
    65    packaging.version.InvalidVersion: Invalid version: 'invalid' 
    66    """ 
    67 
    68 
    69class _BaseVersion: 
    70    _key: tuple[Any, ...] 
    71 
    72    def __hash__(self) -> int: 
    73        return hash(self._key) 
    74 
    75    # Please keep the duplicated `isinstance` check 
    76    # in the six comparisons hereunder 
    77    # unless you find a way to avoid adding overhead function calls. 
    78    def __lt__(self, other: _BaseVersion) -> bool: 
    79        if not isinstance(other, _BaseVersion): 
    80            return NotImplemented 
    81 
    82        return self._key < other._key 
    83 
    84    def __le__(self, other: _BaseVersion) -> bool: 
    85        if not isinstance(other, _BaseVersion): 
    86            return NotImplemented 
    87 
    88        return self._key <= other._key 
    89 
    90    def __eq__(self, other: object) -> bool: 
    91        if not isinstance(other, _BaseVersion): 
    92            return NotImplemented 
    93 
    94        return self._key == other._key 
    95 
    96    def __ge__(self, other: _BaseVersion) -> bool: 
    97        if not isinstance(other, _BaseVersion): 
    98            return NotImplemented 
    99 
    100        return self._key >= other._key 
    101 
    102    def __gt__(self, other: _BaseVersion) -> bool: 
    103        if not isinstance(other, _BaseVersion): 
    104            return NotImplemented 
    105 
    106        return self._key > other._key 
    107 
    108    def __ne__(self, other: object) -> bool: 
    109        if not isinstance(other, _BaseVersion): 
    110            return NotImplemented 
    111 
    112        return self._key != other._key 
    113 
    114 
    115# Deliberately not anchored to the start and end of the string, to make it 
    116# easier for 3rd party code to reuse 
    117_VERSION_PATTERN = r""" 
    118    v? 
    119    (?: 
    120        (?:(?P<epoch>[0-9]+)!)?                           # epoch 
    121        (?P<release>[0-9]+(?:\.[0-9]+)*)                  # release segment 
    122        (?P<pre>                                          # pre-release 
    123            [-_\.]? 
    124            (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) 
    125            [-_\.]? 
    126            (?P<pre_n>[0-9]+)? 
    127        )? 
    128        (?P<post>                                         # post release 
    129            (?:-(?P<post_n1>[0-9]+)) 
    130            | 
    131            (?: 
    132                [-_\.]? 
    133                (?P<post_l>post|rev|r) 
    134                [-_\.]? 
    135                (?P<post_n2>[0-9]+)? 
    136            ) 
    137        )? 
    138        (?P<dev>                                          # dev release 
    139            [-_\.]? 
    140            (?P<dev_l>dev) 
    141            [-_\.]? 
    142            (?P<dev_n>[0-9]+)? 
    143        )? 
    144    ) 
    145    (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version 
    146""" 
    147 
    148VERSION_PATTERN = _VERSION_PATTERN 
    149""" 
    150A string containing the regular expression used to match a valid version. 
    151 
    152The pattern is not anchored at either end, and is intended for embedding in larger 
    153expressions (for example, matching a version number as part of a file name). The 
    154regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` 
    155flags set. 
    156 
    157:meta hide-value: 
    158""" 
    159 
    160 
    161class Version(_BaseVersion): 
    162    """This class abstracts handling of a project's versions. 
    163 
    164    A :class:`Version` instance is comparison aware and can be compared and 
    165    sorted using the standard Python interfaces. 
    166 
    167    >>> v1 = Version("1.0a5") 
    168    >>> v2 = Version("1.0") 
    169    >>> v1 
    170    <Version('1.0a5')> 
    171    >>> v2 
    172    <Version('1.0')> 
    173    >>> v1 < v2 
    174    True 
    175    >>> v1 == v2 
    176    False 
    177    >>> v1 > v2 
    178    False 
    179    >>> v1 >= v2 
    180    False 
    181    >>> v1 <= v2 
    182    True 
    183    """ 
    184 
    185    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) 
    186    _key: CmpKey 
    187 
    188    def __init__(self, version: str) -> None: 
    189        """Initialize a Version object. 
    190 
    191        :param version: 
    192            The string representation of a version which will be parsed and normalized 
    193            before use. 
    194        :raises InvalidVersion: 
    195            If the ``version`` does not conform to PEP 440 in any way then this 
    196            exception will be raised. 
    197        """ 
    198 
    199        # Validate the version and parse it into pieces 
    200        match = self._regex.search(version) 
    201        if not match: 
    202            raise InvalidVersion(f"Invalid version: '{version}'") 
    203 
    204        # Store the parsed out pieces of the version 
    205        self._version = _Version( 
    206            epoch=int(match.group("epoch")) if match.group("epoch") else 0, 
    207            release=tuple(int(i) for i in match.group("release").split(".")), 
    208            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), 
    209            post=_parse_letter_version( 
    210                match.group("post_l"), match.group("post_n1") or match.group("post_n2") 
    211            ), 
    212            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), 
    213            local=_parse_local_version(match.group("local")), 
    214        ) 
    215 
    216        # Generate a key which will be used for sorting 
    217        self._key = _cmpkey( 
    218            self._version.epoch, 
    219            self._version.release, 
    220            self._version.pre, 
    221            self._version.post, 
    222            self._version.dev, 
    223            self._version.local, 
    224        ) 
    225 
    226    def __repr__(self) -> str: 
    227        """A representation of the Version that shows all internal state. 
    228 
    229        >>> Version('1.0.0') 
    230        <Version('1.0.0')> 
    231        """ 
    232        return f"<Version('{self}')>" 
    233 
    234    def __str__(self) -> str: 
    235        """A string representation of the version that can be rounded-tripped. 
    236 
    237        >>> str(Version("1.0a5")) 
    238        '1.0a5' 
    239        """ 
    240        parts = [] 
    241 
    242        # Epoch 
    243        if self.epoch != 0: 
    244            parts.append(f"{self.epoch}!") 
    245 
    246        # Release segment 
    247        parts.append(".".join(str(x) for x in self.release)) 
    248 
    249        # Pre-release 
    250        if self.pre is not None: 
    251            parts.append("".join(str(x) for x in self.pre)) 
    252 
    253        # Post-release 
    254        if self.post is not None: 
    255            parts.append(f".post{self.post}") 
    256 
    257        # Development release 
    258        if self.dev is not None: 
    259            parts.append(f".dev{self.dev}") 
    260 
    261        # Local version segment 
    262        if self.local is not None: 
    263            parts.append(f"+{self.local}") 
    264 
    265        return "".join(parts) 
    266 
    267    @property 
    268    def epoch(self) -> int: 
    269        """The epoch of the version. 
    270 
    271        >>> Version("2.0.0").epoch 
    272        0 
    273        >>> Version("1!2.0.0").epoch 
    274        1 
    275        """ 
    276        return self._version.epoch 
    277 
    278    @property 
    279    def release(self) -> tuple[int, ...]: 
    280        """The components of the "release" segment of the version. 
    281 
    282        >>> Version("1.2.3").release 
    283        (1, 2, 3) 
    284        >>> Version("2.0.0").release 
    285        (2, 0, 0) 
    286        >>> Version("1!2.0.0.post0").release 
    287        (2, 0, 0) 
    288 
    289        Includes trailing zeroes but not the epoch or any pre-release / development / 
    290        post-release suffixes. 
    291        """ 
    292        return self._version.release 
    293 
    294    @property 
    295    def pre(self) -> tuple[str, int] | None: 
    296        """The pre-release segment of the version. 
    297 
    298        >>> print(Version("1.2.3").pre) 
    299        None 
    300        >>> Version("1.2.3a1").pre 
    301        ('a', 1) 
    302        >>> Version("1.2.3b1").pre 
    303        ('b', 1) 
    304        >>> Version("1.2.3rc1").pre 
    305        ('rc', 1) 
    306        """ 
    307        return self._version.pre 
    308 
    309    @property 
    310    def post(self) -> int | None: 
    311        """The post-release number of the version. 
    312 
    313        >>> print(Version("1.2.3").post) 
    314        None 
    315        >>> Version("1.2.3.post1").post 
    316        1 
    317        """ 
    318        return self._version.post[1] if self._version.post else None 
    319 
    320    @property 
    321    def dev(self) -> int | None: 
    322        """The development number of the version. 
    323 
    324        >>> print(Version("1.2.3").dev) 
    325        None 
    326        >>> Version("1.2.3.dev1").dev 
    327        1 
    328        """ 
    329        return self._version.dev[1] if self._version.dev else None 
    330 
    331    @property 
    332    def local(self) -> str | None: 
    333        """The local version segment of the version. 
    334 
    335        >>> print(Version("1.2.3").local) 
    336        None 
    337        >>> Version("1.2.3+abc").local 
    338        'abc' 
    339        """ 
    340        if self._version.local: 
    341            return ".".join(str(x) for x in self._version.local) 
    342        else: 
    343            return None 
    344 
    345    @property 
    346    def public(self) -> str: 
    347        """The public portion of the version. 
    348 
    349        >>> Version("1.2.3").public 
    350        '1.2.3' 
    351        >>> Version("1.2.3+abc").public 
    352        '1.2.3' 
    353        >>> Version("1.2.3+abc.dev1").public 
    354        '1.2.3' 
    355        """ 
    356        return str(self).split("+", 1)[0] 
    357 
    358    @property 
    359    def base_version(self) -> str: 
    360        """The "base version" of the version. 
    361 
    362        >>> Version("1.2.3").base_version 
    363        '1.2.3' 
    364        >>> Version("1.2.3+abc").base_version 
    365        '1.2.3' 
    366        >>> Version("1!1.2.3+abc.dev1").base_version 
    367        '1!1.2.3' 
    368 
    369        The "base version" is the public version of the project without any pre or post 
    370        release markers. 
    371        """ 
    372        parts = [] 
    373 
    374        # Epoch 
    375        if self.epoch != 0: 
    376            parts.append(f"{self.epoch}!") 
    377 
    378        # Release segment 
    379        parts.append(".".join(str(x) for x in self.release)) 
    380 
    381        return "".join(parts) 
    382 
    383    @property 
    384    def is_prerelease(self) -> bool: 
    385        """Whether this version is a pre-release. 
    386 
    387        >>> Version("1.2.3").is_prerelease 
    388        False 
    389        >>> Version("1.2.3a1").is_prerelease 
    390        True 
    391        >>> Version("1.2.3b1").is_prerelease 
    392        True 
    393        >>> Version("1.2.3rc1").is_prerelease 
    394        True 
    395        >>> Version("1.2.3dev1").is_prerelease 
    396        True 
    397        """ 
    398        return self.dev is not None or self.pre is not None 
    399 
    400    @property 
    401    def is_postrelease(self) -> bool: 
    402        """Whether this version is a post-release. 
    403 
    404        >>> Version("1.2.3").is_postrelease 
    405        False 
    406        >>> Version("1.2.3.post1").is_postrelease 
    407        True 
    408        """ 
    409        return self.post is not None 
    410 
    411    @property 
    412    def is_devrelease(self) -> bool: 
    413        """Whether this version is a development release. 
    414 
    415        >>> Version("1.2.3").is_devrelease 
    416        False 
    417        >>> Version("1.2.3.dev1").is_devrelease 
    418        True 
    419        """ 
    420        return self.dev is not None 
    421 
    422    @property 
    423    def major(self) -> int: 
    424        """The first item of :attr:`release` or ``0`` if unavailable. 
    425 
    426        >>> Version("1.2.3").major 
    427        1 
    428        """ 
    429        return self.release[0] if len(self.release) >= 1 else 0 
    430 
    431    @property 
    432    def minor(self) -> int: 
    433        """The second item of :attr:`release` or ``0`` if unavailable. 
    434 
    435        >>> Version("1.2.3").minor 
    436        2 
    437        >>> Version("1").minor 
    438        0 
    439        """ 
    440        return self.release[1] if len(self.release) >= 2 else 0 
    441 
    442    @property 
    443    def micro(self) -> int: 
    444        """The third item of :attr:`release` or ``0`` if unavailable. 
    445 
    446        >>> Version("1.2.3").micro 
    447        3 
    448        >>> Version("1").micro 
    449        0 
    450        """ 
    451        return self.release[2] if len(self.release) >= 3 else 0 
    452 
    453 
    454def _parse_letter_version( 
    455    letter: str | None, number: str | bytes | SupportsInt | None 
    456) -> tuple[str, int] | None: 
    457    if letter: 
    458        # We consider there to be an implicit 0 in a pre-release if there is 
    459        # not a numeral associated with it. 
    460        if number is None: 
    461            number = 0 
    462 
    463        # We normalize any letters to their lower case form 
    464        letter = letter.lower() 
    465 
    466        # We consider some words to be alternate spellings of other words and 
    467        # in those cases we want to normalize the spellings to our preferred 
    468        # spelling. 
    469        if letter == "alpha": 
    470            letter = "a" 
    471        elif letter == "beta": 
    472            letter = "b" 
    473        elif letter in ["c", "pre", "preview"]: 
    474            letter = "rc" 
    475        elif letter in ["rev", "r"]: 
    476            letter = "post" 
    477 
    478        return letter, int(number) 
    479    if not letter and number: 
    480        # We assume if we are given a number, but we are not given a letter 
    481        # then this is using the implicit post release syntax (e.g. 1.0-1) 
    482        letter = "post" 
    483 
    484        return letter, int(number) 
    485 
    486    return None 
    487 
    488 
    489_local_version_separators = re.compile(r"[\._-]") 
    490 
    491 
    492def _parse_local_version(local: str | None) -> LocalType | None: 
    493    """ 
    494    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). 
    495    """ 
    496    if local is not None: 
    497        return tuple( 
    498            part.lower() if not part.isdigit() else int(part) 
    499            for part in _local_version_separators.split(local) 
    500        ) 
    501    return None 
    502 
    503 
    504def _cmpkey( 
    505    epoch: int, 
    506    release: tuple[int, ...], 
    507    pre: tuple[str, int] | None, 
    508    post: tuple[str, int] | None, 
    509    dev: tuple[str, int] | None, 
    510    local: LocalType | None, 
    511) -> CmpKey: 
    512    # When we compare a release version, we want to compare it with all of the 
    513    # trailing zeros removed. So we'll use a reverse the list, drop all the now 
    514    # leading zeros until we come to something non zero, then take the rest 
    515    # re-reverse it back into the correct order and make it a tuple and use 
    516    # that for our sorting key. 
    517    _release = tuple( 
    518        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) 
    519    ) 
    520 
    521    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. 
    522    # We'll do this by abusing the pre segment, but we _only_ want to do this 
    523    # if there is not a pre or a post segment. If we have one of those then 
    524    # the normal sorting rules will handle this case correctly. 
    525    if pre is None and post is None and dev is not None: 
    526        _pre: CmpPrePostDevType = NegativeInfinity 
    527    # Versions without a pre-release (except as noted above) should sort after 
    528    # those with one. 
    529    elif pre is None: 
    530        _pre = Infinity 
    531    else: 
    532        _pre = pre 
    533 
    534    # Versions without a post segment should sort before those with one. 
    535    if post is None: 
    536        _post: CmpPrePostDevType = NegativeInfinity 
    537 
    538    else: 
    539        _post = post 
    540 
    541    # Versions without a development segment should sort after those with one. 
    542    if dev is None: 
    543        _dev: CmpPrePostDevType = Infinity 
    544 
    545    else: 
    546        _dev = dev 
    547 
    548    if local is None: 
    549        # Versions without a local segment should sort before those with one. 
    550        _local: CmpLocalType = NegativeInfinity 
    551    else: 
    552        # Versions with a local segment need that segment parsed to implement 
    553        # the sorting rules in PEP440. 
    554        # - Alpha numeric segments sort before numeric segments 
    555        # - Alpha numeric segments sort lexicographically 
    556        # - Numeric segments sort numerically 
    557        # - Shorter versions sort before longer versions when the prefixes 
    558        #   match exactly 
    559        _local = tuple( 
    560            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local 
    561        ) 
    562 
    563    return epoch, _release, _pre, _post, _dev, _local