Source code for domdf_python_tools.versions

#!/usr/bin/env python
#
#  versions.py
"""
NamedTuple-like class to represent a version number.

.. versionadded:: 0.4.4
"""
#
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import re
from typing import Dict, Generator, Iterable, Sequence, Tuple, Type, TypeVar, Union

# 3rd party
from typing_extensions import final

__all__ = ["Version"]

_V = TypeVar("_V", bound="Version")


[docs]@final class Version(Tuple[int, int, int]): """ NamedTuple-like class to represent a version number. :param major: .. versionchanged:: 1.4.0 Implemented the same interface as a :func:`collections.namedtuple`. """ __slots__ = () #: The major version number. major: int #: The minor version number. minor: int #: The patch version number. patch: int _fields: Tuple[str, str, str] = ("major", "minor", "patch") """ Tuple of strings listing the field names. Useful for introspection and for creating new named tuple types from existing named tuples. .. versionadded:: 1.4.0 """ _field_defaults: Dict[str, int] = {"major": 0, "minor": 0, "patch": 0} """ Dictionary mapping field names to default values. .. versionadded:: 1.4.0 """ @property # type: ignore def major(self): # noqa: D102 return self[0] @property # type: ignore def minor(self): # noqa: D102 return self[1] @property # type: ignore def patch(self): # noqa: D102 return self[2] def __new__(cls: Type[_V], major=0, minor=0, patch=0) -> _V: # noqa: D102 t: _V = super().__new__(cls, (int(major), int(minor), int(patch))) # type: ignore return t
[docs] def __repr__(self) -> str: """ Return the representation of the version. """ repr_fmt = '(' + ", ".join(f"{name}=%r" for name in self._fields) + ')' return self.__class__.__name__ + repr_fmt % self
[docs] def __str__(self) -> str: """ Return version as a string. """ return 'v' + '.'.join(str(x) for x in self) # pylint: disable=not-an-iterable
[docs] def __float__(self) -> float: """ Return the major and minor version number as a float. """ return float('.'.join(str(x) for x in self[:2]))
[docs] def __int__(self) -> int: """ Return the major version number as an integer. """ return self.major
def __getnewargs__(self): """ Return Version as a plain tuple. Used by copy and pickle. """ return tuple(self)
[docs] def __eq__(self, other) -> bool: """ Returns whether this version is equal to the other version. :type other: :class:`str`, :class:`float`, :class:`~.Version` """ other = _prep_for_eq(other) if other is NotImplemented: return NotImplemented # pragma: no cover else: shortest = min(len(self), (len(other))) return self[:shortest] == other[:shortest]
[docs] def __gt__(self, other) -> bool: """ Returns whether this version is greater than the other version. :type other: :class:`str`, :class:`float`, :class:`~.Version` """ other = _prep_for_eq(other) if other is NotImplemented: return NotImplemented # pragma: no cover else: return tuple(self) > other
[docs] def __lt__(self, other) -> bool: """ Returns whether this version is less than the other version. :type other: :class:`str`, :class:`float`, :class:`~.Version` """ other = _prep_for_eq(other) if other is NotImplemented: return NotImplemented # pragma: no cover else: return tuple(self) < other
[docs] def __ge__(self, other) -> bool: """ Returns whether this version is greater than or equal to the other version. :type other: :class:`str`, :class:`float`, :class:`~.Version` """ other = _prep_for_eq(other) if other is NotImplemented: return NotImplemented # pragma: no cover else: return tuple(self)[:len(other)] >= other
[docs] def __le__(self, other) -> bool: """ Returns whether this version is less than or equal to the other version. :type other: :class:`str`, :class:`float`, :class:`~.Version` """ other = _prep_for_eq(other) if other is NotImplemented: return NotImplemented # pragma: no cover else: return tuple(self)[:len(other)] <= other
[docs] @classmethod def from_str(cls: Type[_V], version_string: str) -> _V: """ Create a :class:`~.Version` from a :class:`str`. :param version_string: The version number. :return: The created :class:`~domdf_python_tools.versions.Version`. """ return cls(*_iter_string(version_string))
[docs] @classmethod def from_tuple(cls: Type[_V], version_tuple: Tuple[Union[str, int], ...]) -> _V: """ Create a :class:`~.Version` from a :class:`tuple`. :param version_tuple: The version number. :return: The created :class:`~domdf_python_tools.versions.Version`. .. versionchanged:: 0.9.0 Tuples with more than three elements are truncated. Previously a :exc:`TypeError` was raised. """ return cls(*(int(x) for x in version_tuple[:3]))
[docs] @classmethod def from_float(cls: Type[_V], version_float: float) -> _V: """ Create a :class:`~.Version` from a :class:`float`. :param version_float: The version number. :return: The created :class:`~domdf_python_tools.versions.Version`. """ return cls.from_str(str(version_float))
[docs] def _asdict(self) -> Dict[str, int]: """ Return a new dict which maps field names to their corresponding values. .. versionadded:: 1.4.0 """ return { "major": self.major, "minor": self.minor, "patch": self.patch, }
[docs] def _replace(self: _V, **kwargs) -> _V: """ Return a new instance of the named tuple replacing specified fields with new values. .. versionadded:: 1.4.0 :param kwargs: """ return self.__class__(**{**self._asdict(), **kwargs})
[docs] @classmethod def _make(cls: Type[_V], iterable: Iterable[Union[str, int]]) -> _V: """ Class method that makes a new instance from an existing sequence or iterable. .. versionadded:: 1.4.0 :param iterable: """ return cls(*(int(x) for x in tuple(iterable)[:3]))
def _iter_string(version_string: str) -> Generator[int, None, None]: """ Iterate over the version elements from a string. :param version_string: The version as a string. :return: Iterable elements of the version. """ return (int(x) for x in re.split("[.,]", version_string)) def _iter_float(version_float: float) -> Generator[int, None, None]: """ Iterate over the version elements from a float. :param version_float: The version as a float. :return: Iterable elements of the version. """ return _iter_string(str(version_float)) def _prep_for_eq(other: Union[str, float, Version], ) -> Tuple[int, ...]: """ Prepare 'other' for use in ``__eq__``, ``__le__``, ``__ge__``, ``__gt__``, and ``__lt__``. """ if isinstance(other, str): return tuple(_iter_string(other)) elif isinstance(other, (Version, Sequence)): return tuple(int(x) for x in other) elif isinstance(other, (int, float)): return tuple(_iter_float(other)) else: # pragma: no cover return NotImplemented