import inspect
import re
import abc
from functools import total_ordering
from packaging.version import Version as PypaVersion
__version_register__ = {}
__version_hook_register__ = {}
[docs]def version(format, value):
"""Construct a valid |ProducerVersion| instance with the correct format.
Args:
format (str): A snake case registered format identifier.
value (str): The |ProducerVersion| string representation.
Returns:
|ProducerVersion|: A |ProducerVersion| instance with the correct format.
Raises:
ValueError: If ``format`` is not a registered version format.
"""
try:
format_cls = __version_register__[format]
except KeyError:
raise ValueError('Invalid version format provided: {} is unknown.'.format(format))
format_hook = __version_hook_register__.get(format, lambda instance: instance)
return format_hook(format_cls(value))
[docs]def register(cls, hook=None):
"""Register a |ProducerVersion| class to the |version| function with an optional initialisation hook.
Args:
cls (|ProducerVersion|): The |ProducerVersion| class to register.
hook (Callable): Optional. Default to the identity. An optional initialisation hook.
"""
__version_register__[cls.format] = cls
if hook is not None:
__version_hook_register__[cls.format] = hook
else:
if cls.format in __version_hook_register__:
del __version_hook_register__[cls.format]
_camel_to_snake_re1 = re.compile('(.)([A-Z][a-z]+)')
_camel_to_snake_re2 = re.compile('([a-z0-9])([A-Z])')
def _camel_to_snake(camel_cased_name):
s1 = _camel_to_snake_re1.sub(r'\1_\2', camel_cased_name)
return _camel_to_snake_re2.sub(r'\1_\2', s1).lower()
[docs]@total_ordering
class Version(object, metaclass=MetaVersion):
"""Abstract base class for all |Producer| version.
Subclasses are expected to override the :meth:`__init__`, :meth:`__str__`, :meth:`__eq__` and :meth:`__lt__` magic
method to make the version format 'registerable' to the |version| factory function.
Args:
version (str): A valid version *representation string* with regard to the |Version| format.
Attributes:
format (str): The |Version| corresponding *format*, it is computed from the class name as a snake case.
"""
@property
def version(self):
"""str: The |Version| *string representation*, it is computed with the :meth:`__str__` method."""
return self.__str__()
def __repr__(self):
"""Return a representation of the |Version|."""
return '{}({})'.format(self.__class__.__name__, str(self))
@abc.abstractmethod
def __str__(self):
"""Return the |Version| human-readable representation.
For example, for a standard *PyPA* package version this would return the *version string*, *e.g.* ``2.0.1.dev0``
Returns:
str: The |Version| string representation.
"""
raise NotImplementedError
@abc.abstractmethod
def __eq__(self, other):
"""Return whether two |Version| are equal.
Args:
other (|Version|): Another |Version| to compare to.
Returns:
bool: ``True`` if the two version may be considered equal.
Raises:
TypeError: If it not possible to compare to ``other``.
"""
return NotImplemented
def __ne__(self, other):
"""Return whether two |Version| are not equal.
Args:
other (|Version|): Another |Version| to compare to.
Returns:
bool: ``True`` if the two version may not be considered equal.
Raises:
TypeError: If it not possible to compare to ``other``.
"""
return not self == other
@abc.abstractmethod
def __lt__(self, other):
"""Return whether other |Version| is less than self.
Args:
other (|Version|): Another |Version| to compare to.
Returns:
bool: ``True`` if ``other`` may be considered less than self.
Raises:
TypeError: If it not possible to compare to ``other``.
"""
return NotImplemented
[docs]class PyPA(Version):
"""A |Producer| version following the standard **PyPA** versioning scheme.
It follows the rules defined in `PEP 440`_ which are grossly similar to `Semantic Versioning`_ with a few notable
differences and generalisation.
Args:
version (str): A valid `PEP 440` version *representation string*.
Attributes:
format (str): The |Version| corresponding *format*, it is computed from the class name as a snake case.
.. _PEP 440:
https://www.python.org/dev/peps/pep-0440/
.. _Semantic Versioning:
https://semver.org/
"""
def __init__(self, version):
self._version = PypaVersion(version)
def __str__(self):
"""Return the |Version| human-readable representation.
For example, for a standard *PyPA* package version this would return the *version string*, *e.g.* ``2.0.1.dev0``
Returns:
str: The |Version| string representation.
"""
return str(self._version)
def __eq__(self, other):
"""Return whether two |Version| are equal.
Args:
other (|Version|): Another |Version| to compare to.
Returns:
bool: ``True`` if the two version may be considered equal.
Raises:
TypeError: If it not possible to compare to ``other``.
"""
return self._version == other
def __lt__(self, other):
"""Return whether other |Version| is less than self.
Args:
other (|Version|): Another |Version| to compare to.
Returns:
bool: ``True`` if ``other`` may be considered less than self.
Raises:
TypeError: If it not possible to compare to ``other``.
"""
return self._version < other
register(PyPA)