diff --git a/docs/version.rst b/docs/version.rst index 73a2a01a..2adf336e 100644 --- a/docs/version.rst +++ b/docs/version.rst @@ -1,11 +1,13 @@ Version Handling ================ -.. currentmodule:: packaging.version - A core requirement of dealing with packages is the ability to work with -versions. `PEP 440`_ defines the standard version scheme for Python packages -which has been implemented by this module. +versions. + +See `Version Specifiers Specification`_ for more details on the exact +format implemented in this module, for use in Python Packaging tooling. + +.. _Version Specifiers Specification: https://packaging.python.org/en/latest/specifications/version-specifiers/ Usage ----- @@ -47,112 +49,6 @@ Usage Reference --------- -.. function:: parse(version) - - This function takes a version string and will parse it as a - :class:`Version` if the version is a valid PEP 440 version. - Otherwise, raises :class:`InvalidVersion`. - - -.. class:: Version(version) - - This class abstracts handling of a project's versions. It implements the - scheme defined in `PEP 440`_. A :class:`Version` instance is comparison - aware and can be compared and sorted using the standard Python interfaces. - - :param str 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. - - .. attribute:: public - - A string representing the public version portion of this ``Version()``. - - .. attribute:: base_version - - A string representing the base version of this :class:`Version` - instance. The base version is the public version of the project without - any pre or post release markers. - - .. attribute:: epoch - - An integer giving the version epoch of this :class:`Version` instance - - .. attribute:: release - - A tuple of integers giving the components of the release segment of - this :class:`Version` instance; that is, the ``1.2.3`` part of the - version number, including trailing zeroes but not including the epoch - or any prerelease/development/postrelease suffixes - - .. attribute:: major - - An integer representing the first item of :attr:`release` or ``0`` if unavailable. - - .. attribute:: minor - - An integer representing the second item of :attr:`release` or ``0`` if unavailable. - - .. attribute:: micro - - An integer representing the third item of :attr:`release` or ``0`` if unavailable. - - .. attribute:: local - - A string representing the local version portion of this ``Version()`` - if it has one, or ``None`` otherwise. - - .. attribute:: pre - - If this :class:`Version` instance represents a prerelease, this - attribute will be a pair of the prerelease phase (the string ``"a"``, - ``"b"``, or ``"rc"``) and the prerelease number (an integer). If this - instance is not a prerelease, the attribute will be `None`. - - .. attribute:: is_prerelease - - A boolean value indicating whether this :class:`Version` instance - represents a prerelease and/or development release. - - .. attribute:: dev - - If this :class:`Version` instance represents a development release, - this attribute will be the development release number (an integer); - otherwise, it will be `None`. - - .. attribute:: is_devrelease - - A boolean value indicating whether this :class:`Version` instance - represents a development release. - - .. attribute:: post - - If this :class:`Version` instance represents a postrelease, this - attribute will be the postrelease number (an integer); otherwise, it - will be `None`. - - .. attribute:: is_postrelease - - A boolean value indicating whether this :class:`Version` instance - represents a post-release. - - -.. exception:: InvalidVersion - - Raised when attempting to create a :class:`Version` with a version string - that does not conform to `PEP 440`_. - - -.. data:: 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. - - -.. _PEP 440: https://www.python.org/dev/peps/pep-0440/ -.. _Pre-release spelling : https://www.python.org/dev/peps/pep-0440/#pre-release-spelling -.. _Post-release spelling : https://www.python.org/dev/peps/pep-0440/#post-release-spelling +.. automodule:: packaging.version + :members: + :special-members: diff --git a/packaging/version.py b/packaging/version.py index 9a23b8e2..e5c738cf 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -1,6 +1,11 @@ # 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 collections import itertools @@ -9,7 +14,7 @@ from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType -__all__ = ["parse", "Version", "InvalidVersion", "VERSION_PATTERN"] +__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] InfiniteTypes = Union[InfinityType, NegativeInfinityType] PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] @@ -36,19 +41,24 @@ def parse(version: str) -> "Version": - """ - Parse the given version string. + """Parse the given version string. - Returns a :class:`Version` object, if the given version is a valid PEP 440 version. + >>> parse('1.0.dev1') + - Raises :class:`InvalidVersion` otherwise. + :param version: The version string to parse. + :raises InvalidVersion: When the version string is not a valid version. """ return Version(version) class InvalidVersion(ValueError): - """ - An invalid version was found, users should refer to PEP 440. + """Raised when a version string is not a valid version. + + >>> Version("invalid") + Traceback (most recent call last): + ... + packaging.version.InvalidVersion: Invalid version: 'invalid' """ @@ -100,7 +110,7 @@ def __ne__(self, other: object) -> bool: # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse -VERSION_PATTERN = r""" +_VERSION_PATTERN = r""" v? (?: (?:(?P[0-9]+)!)? # epoch @@ -131,12 +141,55 @@ def __ne__(self, other: object) -> bool: (?:\+(?P[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 + + >>> v2 + + >>> 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) 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) @@ -166,9 +219,19 @@ def __init__(self, version: str) -> None: ) def __repr__(self) -> str: + """A representation of the Version that shows all internal state. + + >>> Version('1.0.0') + + """ return f"" def __str__(self) -> str: + """A string representation of the version that can be rounded-tripped. + + >>> str(Version("1.0a5")) + '1.0a5' + """ parts = [] # Epoch @@ -198,29 +261,80 @@ def __str__(self) -> str: @property def epoch(self) -> int: + """The epoch of the version. + + >>> Version("2.0.0").epoch + 0 + >>> Version("1!2.0.0").epoch + 1 + """ _epoch: int = self._version.epoch return _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. + """ _release: Tuple[int, ...] = self._version.release return _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) + """ _pre: Optional[Tuple[str, int]] = self._version.pre return _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: @@ -228,10 +342,31 @@ def local(self) -> Optional[str]: @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 @@ -245,26 +380,72 @@ def base_version(self) -> str: @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