python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
setuptools/
_vendor/
packaging/
version.py
       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  import collections
       6  import itertools
       7  import re
       8  import warnings
       9  from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
      10  
      11  from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
      12  
      13  __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
      14  
      15  InfiniteTypes = Union[InfinityType, NegativeInfinityType]
      16  PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
      17  SubLocalType = Union[InfiniteTypes, int, str]
      18  LocalType = Union[
      19      NegativeInfinityType,
      20      Tuple[
      21          Union[
      22              SubLocalType,
      23              Tuple[SubLocalType, str],
      24              Tuple[NegativeInfinityType, SubLocalType],
      25          ],
      26          ...,
      27      ],
      28  ]
      29  CmpKey = Tuple[
      30      int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
      31  ]
      32  LegacyCmpKey = Tuple[int, Tuple[str, ...]]
      33  VersionComparisonMethod = Callable[
      34      [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
      35  ]
      36  
      37  _Version = collections.namedtuple(
      38      "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
      39  )
      40  
      41  
      42  def parse(version: str) -> Union["LegacyVersion", "Version"]:
      43      """
      44      Parse the given version string and return either a :class:`Version` object
      45      or a :class:`LegacyVersion` object depending on if the given version is
      46      a valid PEP 440 version or a legacy version.
      47      """
      48      try:
      49          return Version(version)
      50      except InvalidVersion:
      51          return LegacyVersion(version)
      52  
      53  
      54  class ESC[4;38;5;81mInvalidVersion(ESC[4;38;5;149mValueError):
      55      """
      56      An invalid version was found, users should refer to PEP 440.
      57      """
      58  
      59  
      60  class ESC[4;38;5;81m_BaseVersion:
      61      _key: Union[CmpKey, LegacyCmpKey]
      62  
      63      def __hash__(self) -> int:
      64          return hash(self._key)
      65  
      66      # Please keep the duplicated `isinstance` check
      67      # in the six comparisons hereunder
      68      # unless you find a way to avoid adding overhead function calls.
      69      def __lt__(self, other: "_BaseVersion") -> bool:
      70          if not isinstance(other, _BaseVersion):
      71              return NotImplemented
      72  
      73          return self._key < other._key
      74  
      75      def __le__(self, other: "_BaseVersion") -> bool:
      76          if not isinstance(other, _BaseVersion):
      77              return NotImplemented
      78  
      79          return self._key <= other._key
      80  
      81      def __eq__(self, other: object) -> bool:
      82          if not isinstance(other, _BaseVersion):
      83              return NotImplemented
      84  
      85          return self._key == other._key
      86  
      87      def __ge__(self, other: "_BaseVersion") -> bool:
      88          if not isinstance(other, _BaseVersion):
      89              return NotImplemented
      90  
      91          return self._key >= other._key
      92  
      93      def __gt__(self, other: "_BaseVersion") -> bool:
      94          if not isinstance(other, _BaseVersion):
      95              return NotImplemented
      96  
      97          return self._key > other._key
      98  
      99      def __ne__(self, other: object) -> bool:
     100          if not isinstance(other, _BaseVersion):
     101              return NotImplemented
     102  
     103          return self._key != other._key
     104  
     105  
     106  class ESC[4;38;5;81mLegacyVersion(ESC[4;38;5;149m_BaseVersion):
     107      def __init__(self, version: str) -> None:
     108          self._version = str(version)
     109          self._key = _legacy_cmpkey(self._version)
     110  
     111          warnings.warn(
     112              "Creating a LegacyVersion has been deprecated and will be "
     113              "removed in the next major release",
     114              DeprecationWarning,
     115          )
     116  
     117      def __str__(self) -> str:
     118          return self._version
     119  
     120      def __repr__(self) -> str:
     121          return f"<LegacyVersion('{self}')>"
     122  
     123      @property
     124      def public(self) -> str:
     125          return self._version
     126  
     127      @property
     128      def base_version(self) -> str:
     129          return self._version
     130  
     131      @property
     132      def epoch(self) -> int:
     133          return -1
     134  
     135      @property
     136      def release(self) -> None:
     137          return None
     138  
     139      @property
     140      def pre(self) -> None:
     141          return None
     142  
     143      @property
     144      def post(self) -> None:
     145          return None
     146  
     147      @property
     148      def dev(self) -> None:
     149          return None
     150  
     151      @property
     152      def local(self) -> None:
     153          return None
     154  
     155      @property
     156      def is_prerelease(self) -> bool:
     157          return False
     158  
     159      @property
     160      def is_postrelease(self) -> bool:
     161          return False
     162  
     163      @property
     164      def is_devrelease(self) -> bool:
     165          return False
     166  
     167  
     168  _legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
     169  
     170  _legacy_version_replacement_map = {
     171      "pre": "c",
     172      "preview": "c",
     173      "-": "final-",
     174      "rc": "c",
     175      "dev": "@",
     176  }
     177  
     178  
     179  def _parse_version_parts(s: str) -> Iterator[str]:
     180      for part in _legacy_version_component_re.split(s):
     181          part = _legacy_version_replacement_map.get(part, part)
     182  
     183          if not part or part == ".":
     184              continue
     185  
     186          if part[:1] in "0123456789":
     187              # pad for numeric comparison
     188              yield part.zfill(8)
     189          else:
     190              yield "*" + part
     191  
     192      # ensure that alpha/beta/candidate are before final
     193      yield "*final"
     194  
     195  
     196  def _legacy_cmpkey(version: str) -> LegacyCmpKey:
     197  
     198      # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
     199      # greater than or equal to 0. This will effectively put the LegacyVersion,
     200      # which uses the defacto standard originally implemented by setuptools,
     201      # as before all PEP 440 versions.
     202      epoch = -1
     203  
     204      # This scheme is taken from pkg_resources.parse_version setuptools prior to
     205      # it's adoption of the packaging library.
     206      parts: List[str] = []
     207      for part in _parse_version_parts(version.lower()):
     208          if part.startswith("*"):
     209              # remove "-" before a prerelease tag
     210              if part < "*final":
     211                  while parts and parts[-1] == "*final-":
     212                      parts.pop()
     213  
     214              # remove trailing zeros from each series of numeric parts
     215              while parts and parts[-1] == "00000000":
     216                  parts.pop()
     217  
     218          parts.append(part)
     219  
     220      return epoch, tuple(parts)
     221  
     222  
     223  # Deliberately not anchored to the start and end of the string, to make it
     224  # easier for 3rd party code to reuse
     225  VERSION_PATTERN = r"""
     226      v?
     227      (?:
     228          (?:(?P<epoch>[0-9]+)!)?                           # epoch
     229          (?P<release>[0-9]+(?:\.[0-9]+)*)                  # release segment
     230          (?P<pre>                                          # pre-release
     231              [-_\.]?
     232              (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
     233              [-_\.]?
     234              (?P<pre_n>[0-9]+)?
     235          )?
     236          (?P<post>                                         # post release
     237              (?:-(?P<post_n1>[0-9]+))
     238              |
     239              (?:
     240                  [-_\.]?
     241                  (?P<post_l>post|rev|r)
     242                  [-_\.]?
     243                  (?P<post_n2>[0-9]+)?
     244              )
     245          )?
     246          (?P<dev>                                          # dev release
     247              [-_\.]?
     248              (?P<dev_l>dev)
     249              [-_\.]?
     250              (?P<dev_n>[0-9]+)?
     251          )?
     252      )
     253      (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
     254  """
     255  
     256  
     257  class ESC[4;38;5;81mVersion(ESC[4;38;5;149m_BaseVersion):
     258  
     259      _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
     260  
     261      def __init__(self, version: str) -> None:
     262  
     263          # Validate the version and parse it into pieces
     264          match = self._regex.search(version)
     265          if not match:
     266              raise InvalidVersion(f"Invalid version: '{version}'")
     267  
     268          # Store the parsed out pieces of the version
     269          self._version = _Version(
     270              epoch=int(match.group("epoch")) if match.group("epoch") else 0,
     271              release=tuple(int(i) for i in match.group("release").split(".")),
     272              pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
     273              post=_parse_letter_version(
     274                  match.group("post_l"), match.group("post_n1") or match.group("post_n2")
     275              ),
     276              dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
     277              local=_parse_local_version(match.group("local")),
     278          )
     279  
     280          # Generate a key which will be used for sorting
     281          self._key = _cmpkey(
     282              self._version.epoch,
     283              self._version.release,
     284              self._version.pre,
     285              self._version.post,
     286              self._version.dev,
     287              self._version.local,
     288          )
     289  
     290      def __repr__(self) -> str:
     291          return f"<Version('{self}')>"
     292  
     293      def __str__(self) -> str:
     294          parts = []
     295  
     296          # Epoch
     297          if self.epoch != 0:
     298              parts.append(f"{self.epoch}!")
     299  
     300          # Release segment
     301          parts.append(".".join(str(x) for x in self.release))
     302  
     303          # Pre-release
     304          if self.pre is not None:
     305              parts.append("".join(str(x) for x in self.pre))
     306  
     307          # Post-release
     308          if self.post is not None:
     309              parts.append(f".post{self.post}")
     310  
     311          # Development release
     312          if self.dev is not None:
     313              parts.append(f".dev{self.dev}")
     314  
     315          # Local version segment
     316          if self.local is not None:
     317              parts.append(f"+{self.local}")
     318  
     319          return "".join(parts)
     320  
     321      @property
     322      def epoch(self) -> int:
     323          _epoch: int = self._version.epoch
     324          return _epoch
     325  
     326      @property
     327      def release(self) -> Tuple[int, ...]:
     328          _release: Tuple[int, ...] = self._version.release
     329          return _release
     330  
     331      @property
     332      def pre(self) -> Optional[Tuple[str, int]]:
     333          _pre: Optional[Tuple[str, int]] = self._version.pre
     334          return _pre
     335  
     336      @property
     337      def post(self) -> Optional[int]:
     338          return self._version.post[1] if self._version.post else None
     339  
     340      @property
     341      def dev(self) -> Optional[int]:
     342          return self._version.dev[1] if self._version.dev else None
     343  
     344      @property
     345      def local(self) -> Optional[str]:
     346          if self._version.local:
     347              return ".".join(str(x) for x in self._version.local)
     348          else:
     349              return None
     350  
     351      @property
     352      def public(self) -> str:
     353          return str(self).split("+", 1)[0]
     354  
     355      @property
     356      def base_version(self) -> str:
     357          parts = []
     358  
     359          # Epoch
     360          if self.epoch != 0:
     361              parts.append(f"{self.epoch}!")
     362  
     363          # Release segment
     364          parts.append(".".join(str(x) for x in self.release))
     365  
     366          return "".join(parts)
     367  
     368      @property
     369      def is_prerelease(self) -> bool:
     370          return self.dev is not None or self.pre is not None
     371  
     372      @property
     373      def is_postrelease(self) -> bool:
     374          return self.post is not None
     375  
     376      @property
     377      def is_devrelease(self) -> bool:
     378          return self.dev is not None
     379  
     380      @property
     381      def major(self) -> int:
     382          return self.release[0] if len(self.release) >= 1 else 0
     383  
     384      @property
     385      def minor(self) -> int:
     386          return self.release[1] if len(self.release) >= 2 else 0
     387  
     388      @property
     389      def micro(self) -> int:
     390          return self.release[2] if len(self.release) >= 3 else 0
     391  
     392  
     393  def _parse_letter_version(
     394      letter: str, number: Union[str, bytes, SupportsInt]
     395  ) -> Optional[Tuple[str, int]]:
     396  
     397      if letter:
     398          # We consider there to be an implicit 0 in a pre-release if there is
     399          # not a numeral associated with it.
     400          if number is None:
     401              number = 0
     402  
     403          # We normalize any letters to their lower case form
     404          letter = letter.lower()
     405  
     406          # We consider some words to be alternate spellings of other words and
     407          # in those cases we want to normalize the spellings to our preferred
     408          # spelling.
     409          if letter == "alpha":
     410              letter = "a"
     411          elif letter == "beta":
     412              letter = "b"
     413          elif letter in ["c", "pre", "preview"]:
     414              letter = "rc"
     415          elif letter in ["rev", "r"]:
     416              letter = "post"
     417  
     418          return letter, int(number)
     419      if not letter and number:
     420          # We assume if we are given a number, but we are not given a letter
     421          # then this is using the implicit post release syntax (e.g. 1.0-1)
     422          letter = "post"
     423  
     424          return letter, int(number)
     425  
     426      return None
     427  
     428  
     429  _local_version_separators = re.compile(r"[\._-]")
     430  
     431  
     432  def _parse_local_version(local: str) -> Optional[LocalType]:
     433      """
     434      Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     435      """
     436      if local is not None:
     437          return tuple(
     438              part.lower() if not part.isdigit() else int(part)
     439              for part in _local_version_separators.split(local)
     440          )
     441      return None
     442  
     443  
     444  def _cmpkey(
     445      epoch: int,
     446      release: Tuple[int, ...],
     447      pre: Optional[Tuple[str, int]],
     448      post: Optional[Tuple[str, int]],
     449      dev: Optional[Tuple[str, int]],
     450      local: Optional[Tuple[SubLocalType]],
     451  ) -> CmpKey:
     452  
     453      # When we compare a release version, we want to compare it with all of the
     454      # trailing zeros removed. So we'll use a reverse the list, drop all the now
     455      # leading zeros until we come to something non zero, then take the rest
     456      # re-reverse it back into the correct order and make it a tuple and use
     457      # that for our sorting key.
     458      _release = tuple(
     459          reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
     460      )
     461  
     462      # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
     463      # We'll do this by abusing the pre segment, but we _only_ want to do this
     464      # if there is not a pre or a post segment. If we have one of those then
     465      # the normal sorting rules will handle this case correctly.
     466      if pre is None and post is None and dev is not None:
     467          _pre: PrePostDevType = NegativeInfinity
     468      # Versions without a pre-release (except as noted above) should sort after
     469      # those with one.
     470      elif pre is None:
     471          _pre = Infinity
     472      else:
     473          _pre = pre
     474  
     475      # Versions without a post segment should sort before those with one.
     476      if post is None:
     477          _post: PrePostDevType = NegativeInfinity
     478  
     479      else:
     480          _post = post
     481  
     482      # Versions without a development segment should sort after those with one.
     483      if dev is None:
     484          _dev: PrePostDevType = Infinity
     485  
     486      else:
     487          _dev = dev
     488  
     489      if local is None:
     490          # Versions without a local segment should sort before those with one.
     491          _local: LocalType = NegativeInfinity
     492      else:
     493          # Versions with a local segment need that segment parsed to implement
     494          # the sorting rules in PEP440.
     495          # - Alpha numeric segments sort before numeric segments
     496          # - Alpha numeric segments sort lexicographically
     497          # - Numeric segments sort numerically
     498          # - Shorter versions sort before longer versions when the prefixes
     499          #   match exactly
     500          _local = tuple(
     501              (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
     502          )
     503  
     504      return epoch, _release, _pre, _post, _dev, _local