forked from dr-leo/pandaSDMX
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
"""Handle SDMX version identifiers.""" | ||
|
||
import re | ||
from typing import Optional, Union | ||
|
||
import packaging.version | ||
|
||
#: Regular expressions (:class:`re.Pattern`) for version strings. | ||
#: | ||
#: - :py:`"2_1"` SDMX 2.1, e.g. "1.0" | ||
#: - :py:`"3_0"` SDMX 3.0, e.g. "1.0.0-draft" | ||
#: - :py:`"py"` Python-compatible versions, using :mod:`packaging.version`. | ||
VERSION_PATTERNS = { | ||
"2_1": re.compile(r"^(?P<release>[0-9]+(?:\.[0-9]+){1})$"), | ||
"3_0": re.compile(r"^(?P<release>[0-9]+(?:\.[0-9]+){2})(-(?P<ext>.+))?$"), | ||
"py": re.compile( | ||
r"^\s*" + packaging.version.VERSION_PATTERN + r"\s*$", | ||
re.VERBOSE | re.IGNORECASE, | ||
), | ||
} | ||
|
||
|
||
class Version(packaging.version.Version): | ||
"""Version. | ||
Parameters | ||
---------- | ||
version : str | ||
String expression | ||
""" | ||
|
||
#: Type of version expression; one of the keys of :data:`.VERSION_PATTERNS`. | ||
kind: str | ||
|
||
def __init__(self, version: str): | ||
for kind, pattern in VERSION_PATTERNS.items(): | ||
match = pattern.match(version) | ||
if match: | ||
break | ||
|
||
if not match: | ||
raise packaging.version.InvalidVersion(version) | ||
|
||
self.kind = kind | ||
|
||
if kind == "py": | ||
tmp = packaging.version.Version(version) | ||
self._version = tmp._version | ||
else: | ||
# Store the parsed out pieces of the version | ||
try: | ||
ext = match.group("ext") | ||
except IndexError: | ||
local = None | ||
else: | ||
local = (ext,) if ext else None | ||
self._version = packaging.version._Version( | ||
epoch=0, | ||
release=tuple(int(i) for i in match.group("release").split(".")), | ||
pre=None, | ||
post=None, | ||
dev=None, | ||
local=local, | ||
) | ||
|
||
self._update_key() | ||
|
||
def _update_key(self): | ||
# Generate a key which will be used for sorting | ||
self._key = packaging.version._cmpkey( | ||
self._version.epoch, | ||
self._version.release, | ||
self._version.pre, | ||
self._version.post, | ||
self._version.dev, | ||
self._version.local, | ||
) | ||
|
||
def __str__(self): | ||
if self.kind == "3_0": | ||
parts = [".".join(str(x) for x in self.release)] | ||
if self.ext: | ||
parts.append(f"-{self.ext}") | ||
return "".join(parts) | ||
else: | ||
return super().__str__() | ||
|
||
@property | ||
def patch(self) -> int: | ||
"""Alias for :attr:`.Version.micro`.""" | ||
return self.micro | ||
|
||
@property | ||
def ext(self) -> Optional[str]: | ||
"""SDMX 3.0 version 'extension'. | ||
For :py:`kind="py"`, this is equivalent to :attr:`.Version.local`. | ||
""" | ||
if self._version.local is None: | ||
return None | ||
else: | ||
return "".join(map(str, self._version.local)) | ||
|
||
def increment(self, **kwargs: Union[bool, int]) -> "Version": | ||
"""Return a Version that is incrementally greater than the current Version. | ||
If no arguments are given, then by default :py:`minor=True`. | ||
.. todo:: Allow to increment or set :attr:`ext`. | ||
Parameters | ||
---------- | ||
major : bool or int, *optional* | ||
If given, increment the :attr:`.Version.major` part. | ||
minor : bool or int, *optional* | ||
If given, increment the :attr:`.Version.minor` part. | ||
patch : bool or int, *optional* | ||
If given, increment the :attr:`.Version.patch` part. | ||
""" | ||
if not kwargs: | ||
kwargs["minor"] = 1 | ||
|
||
# Convert self._version into a mutable dict | ||
|
||
N_release = len(self._version.release) # Number of parts in `release` tuple | ||
parts = dict( | ||
major=self._version.release[0] if N_release > 0 else 0, | ||
minor=self._version.release[1] if N_release > 1 else 0, | ||
patch=self._version.release[2] if N_release > 2 else 0, | ||
) | ||
|
||
# Increment parts according to kwargs | ||
for part, value in kwargs.items(): | ||
try: | ||
parts[part] += int(value) | ||
except KeyError: | ||
raise NotImplementedError(f"increment(..., {part}={value})") | ||
|
||
# Construct a new Version object | ||
result = type(self)(str(self)) | ||
# Overwrite its private _version attribute and key | ||
result._version = packaging.version._Version( | ||
epoch=self._version.epoch, | ||
release=tuple(parts.values()), | ||
pre=self._version.pre, | ||
post=self._version.post, | ||
dev=self._version.dev, | ||
local=self._version.local, | ||
) | ||
result._update_key() | ||
|
||
return result | ||
|
||
|
||
def increment(value: Union[packaging.version.Version, str], **kwargs) -> Version: | ||
"""Increment the version `existing`. | ||
Identical to :py:`Version(str(value)).increment(**kwargs)`. | ||
See also | ||
-------- | ||
Version.increment | ||
""" | ||
return Version(str(value)).increment(**kwargs) | ||
|
||
|
||
def parse(value: str) -> Version: | ||
"""Parse the given version string. | ||
Identical to :py:`Version(value)`. | ||
See also | ||
-------- | ||
Version | ||
""" | ||
return Version(value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import pytest | ||
from packaging.version import InvalidVersion, Version | ||
|
||
from sdmx.model.version import increment, parse | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"value, expected", | ||
( | ||
# SDMX 2.1 | ||
("0.0", Version("0.0")), | ||
("1.0", Version("1.0")), | ||
# SDMX 3.0 | ||
("0.0.0-dev1", Version("0.0.0+dev1")), | ||
("1.0.0-dev1", Version("1.0.0+dev1")), | ||
# Python | ||
("1!1.2.3+abc.dev1", Version("1!1.2.3+abc.dev1")), | ||
# Invalid | ||
pytest.param("foo", None, marks=pytest.mark.xfail(raises=InvalidVersion)), | ||
), | ||
) | ||
def test_parse(value, expected) -> None: | ||
v = parse(value) | ||
|
||
assert expected == v | ||
|
||
# Value round-trips | ||
assert value == str(v) | ||
|
||
# Attributes can be accessed | ||
v.major | ||
v.minor | ||
v.patch | ||
v.local | ||
v.ext | ||
|
||
# Object's increment() method can be called | ||
assert v < v.increment(patch=1) < v.increment(minor=1) < v.increment(major=1) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"kwargs, expected", | ||
( | ||
(dict(), Version("1.1.0")), | ||
(dict(major=1), Version("2.0.0")), | ||
(dict(minor=1), Version("1.1.0")), | ||
(dict(patch=1), Version("1.0.1")), | ||
pytest.param( | ||
dict(ext=1), None, marks=pytest.mark.xfail(raises=NotImplementedError) | ||
), | ||
), | ||
) | ||
def test_increment(kwargs, expected): | ||
# Version.increment() method | ||
assert expected == parse("1.0.0").increment(**kwargs) | ||
|
||
# increment() function | ||
assert expected == increment("1.0.0", **kwargs) |