#!/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