blob: 0d5edabcba72298232071908390cd04fb9bd78c5 [file] [log] [blame]
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
"""
Version parsing classes.
"""
from __future__ import absolute_import, unicode_literals
import functools
import six
__all__ = [
'InvalidVersionError',
'Version',
]
# -----------------------------------------------------------------------------
# Version Parsing
class _ComponentType(object):
"""Poor-man's enum representing all valid version character groups.
"""
def __init__(self, name):
self.name = name
def __eq__(self, other):
if not isinstance(other, _ComponentType):
return NotImplemented
return self.name == other.name
def __ne__(self, other):
return not self.__eq__(other)
@classmethod
def _register(cls, name):
setattr(cls, name, cls(name))
_ComponentType._register('ALPHA_LOWER')
_ComponentType._register('ALPHA_UPPER')
_ComponentType._register('DOT')
_ComponentType._register('NUMERIC')
_ComponentType._register('OTHER')
def _get_component_type(component):
"""Classifies a component into one of the registered component types.
"""
if len(component) <= 0:
raise ValueError('Empty component')
if component == '.':
return _ComponentType.DOT
if component.isdigit():
return _ComponentType.NUMERIC
if component.isalpha():
if component.isupper():
return _ComponentType.ALPHA_UPPER
elif component.islower():
return _ComponentType.ALPHA_LOWER
else:
raise ValueError('Unknown component type for {!r}'.format(
component))
return _ComponentType.OTHER
def _try_cast(obj, cls):
"""Attempts to cast an object to a class, returning the resulting casted
object or the original object if the cast raises a ValueError.
"""
try:
return cls(obj)
except ValueError:
return obj
def _split_version(version):
"""Splits a version string into a tuple of components using similar rules
to distutils.version.LooseVersion. All version strings are valid, but the
outcome will only split on boundries between:
* lowercase alpha characters
* uppercase alpha characters
* numeric characters
* the literal '.' (dot) character
All other characters are grouped into an "other" category.
Numeric components are converted into integers in the resulting tuple.
An empty tuple is returned for the empty string.
```
>>> _split_version('1000.2.108')
(1000, 2, 28)
>>> _split_version('10A23b')
(10, 'A', 23, 'b')
>>> _split_version('10.23-beta4')
(10, 23, '-', 'beta', 4)
>>> _split_version('FOObarBAZqux')
('FOO', 'bar', 'BAZ', 'qux')
```
"""
if len(version) < 1:
return tuple()
components = []
part = version[0]
part_type = _get_component_type(part)
for char in version[1:]:
char_type = _get_component_type(char)
if part_type == char_type:
part += char
else:
components.append(part)
part = char
part_type = char_type
# Add last part
components.append(part)
# Remove '.' groups and try casting components to ints
components = (_try_cast(c, int) for c in components if c != '.')
return tuple(components)
# -----------------------------------------------------------------------------
# Versions
class InvalidVersionError(Exception):
"""Error indicating an invalid version was encountered.
"""
def __init__(self, version, msg=None):
self.version = version
if msg is None:
msg = 'Invalid version: {}'.format(self.version)
super(InvalidVersionError, self).__init__(msg)
@functools.total_ordering
class Version(object):
"""Similar to the standard distutils.version.LooseVersion, but with a
little more wiggle-room for alpha characters.
"""
__slots__ = ('components', '_str')
def __init__(self, version):
version = six.text_type(version)
# Save the version string since it's impossible to reconstruct it from
# just the parsed components
self._str = version
# Parse version components
self.components = _split_version(version)
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self.components == other.components
# NOTE: Python 2 compatibility.
def __ne__(self, other):
return not self == other
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self.components < other.components
def __hash__(self):
return hash(self.components)
def __str__(self):
return self._str
def __repr__(self):
return '{}({!r})'.format(type(self).__name__, self._str)