diff --git a/.gitignore b/.gitignore index 994eb868..2ef76af8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ -# Files +# Patch/Diff Files *.patch *.diff -*.kate-swp # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] .pytest_cache/ +*$py.class # Distribution / packaging .cache @@ -72,3 +72,7 @@ docs/_build/ # PyBuilder target/ + +# Backup files +*~ +*.kate-swp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ae377d9..48e3d82b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,8 +27,24 @@ Additions * :pr:`228`: Added better doctest integration + Removals -------- +* :gh:`225` (:pr:`229`): Output a DeprecationWarning for the following functions: + + - ``semver.parse`` + - ``semver.parse_version_info`` + - ``semver.format_version`` + - ``semver.bump_{major,minor,patch,prerelease,build}`` + - ``semver.finalize_version`` + - ``semver.replace`` + - ``semver.VersionInfo._asdict`` (use the new, public available + function ``semver.VersionInfo.to_dict()``) + - ``semver.VersionInfo._astuple`` (use the new, public available + function ``semver.VersionInfo.to_tuple()``) + + These deprecated functions will be removed in semver 3. + Version 2.9.1 diff --git a/docs/usage.rst b/docs/usage.rst index dd05fa1c..cdf08b9a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -14,9 +14,10 @@ are met. Knowing the Implemented semver.org Version ------------------------------------------ -The semver.org is the authorative specification of how semantical versioning is -definied. To know which version of semver.org is implemented in the semver -libary, use the following constant:: +The semver.org page is the authorative specification of how semantical +versioning is definied. +To know which version of semver.org is implemented in the semver libary, +use the following constant:: >>> semver.SEMVER_SPEC_VERSION '2.0.0' @@ -25,35 +26,81 @@ libary, use the following constant:: Creating a Version ------------------ -A version can be created in different ways: +Due to historical reasons, the semver project offers two ways of +creating a version: -* as a complete version string:: +* through an object oriented approach with the :class:`semver.VersionInfo` + class. This is the preferred method when using semver. + +* through module level functions and builtin datatypes (usually strings + and dicts). + These method are still available for compatibility reasons, but are + marked as deprecated. Using one of these will emit a DeprecationWarning. + + +.. warning:: **Deprecation Warning** + + Module level functions are marked as *deprecated* in version 2.9.2 now. + These functions will be removed in semver 3. + For details, see the sections :ref:`sec_replace_deprecated_functions` and + :ref:`sec_display_deprecation_warnings`. + + +A :class:`semver.VersionInfo` instance can be created in different ways: + + +* From a string:: - >>> semver.parse_version_info("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') -* with individual parts:: +* From individual parts by a dictionary:: - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - >>> semver.VersionInfo(3, 5) - VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> semver.VersionInfo(**d) + VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + + As a minimum requirement, your dictionary needs at least the ``major`` + key, others can be omitted. You get a ``TypeError`` if your + dictionary contains invalid keys. + Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` + are allowed. + +* From a tuple:: + + >>> t = (3, 5, 6) + >>> semver.VersionInfo(*t) + VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) You can pass either an integer or a string for ``major``, ``minor``, or ``patch``:: - >>> semver.VersionInfo("3", "5") - VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) + >>> semver.VersionInfo("3", "5", 6) + VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + +The old, deprecated module level functions are still available. If you +need them, they return different builtin objects (string and dictionary). +Keep in mind, once you have converted a version into a string or dictionary, +it's an ordinary builtin object. It's not a special version object like +the :class:`semver.VersionInfo` class anymore. + +Depending on your use case, the following methods are available: + +* From individual version parts into a string + + In some cases you only need a string from your version data:: + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + +* From a string into a dictionary - In the simplest form, ``prerelease`` and ``build`` can also be - integers:: + To access individual parts, you can use the function :func:`semver.parse`:: - >>> semver.VersionInfo(1, 2, 3, 4, 5) - VersionInfo(major=1, minor=2, patch=3, prerelease='4', build='5') + >>> semver.parse("3.4.5-pre.2+build.4") + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) -If you pass an invalid version string you will get a ``ValueError``:: + If you pass an invalid version string you will get a ``ValueError``:: >>> semver.parse("1.2") Traceback (most recent call last): @@ -172,45 +219,30 @@ If you pass invalid keys you get an exception:: .. _sec.convert.versions: -Converting Different Version Types ----------------------------------- +Converting a VersionInfo instance into Different Types +------------------------------------------------------ -Depending which function you call, you get different types -(as explained in the beginning of this chapter). +Sometimes it is needed to convert a :class:`semver.VersionInfo` instance into +a different type. For example, for displaying or to access all parts. -* From a string into :class:`semver.VersionInfo`:: - - >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') +It is possible to convert a :class:`semver.VersionInfo` instance: -* From :class:`semver.VersionInfo` into a string:: +* Into a string with the builtin function :func:`str`:: >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* From a dictionary into :class:`semver.VersionInfo`:: +* Into a dictionary with :func:`semver.VersionInfo.to_dict`:: - >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> semver.VersionInfo(**d) - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - - As a minimum requirement, your dictionary needs at least the ``major`` - key, others can be omitted. You get a ``TypeError`` if your - dictionary contains invalid keys. - Only ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` - are allowed. - -* From a tuple into :class:`semver.VersionInfo`:: - - >>> t = (3, 5, 6) - >>> semver.VersionInfo(*t) - VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + >>> v = semver.VersionInfo(major=3, minor=4, patch=5) + >>> v.to_dict() + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* From a :class:`semver.VersionInfo` into a dictionary:: +* Into a tuple with :func:`semver.VersionInfo.to_tuple`:: - >>> v = semver.VersionInfo(major=3, minor=4, patch=5) - >>> semver.parse(str(v)) == {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None} - True + >>> v = semver.VersionInfo(major=5, minor=4, patch=2) + >>> v.to_tuple() + (5, 4, 2, None, None) Increasing Parts of a Version @@ -362,8 +394,132 @@ For example: .. code-block:: python - >>> coerce("v1.2") + >>> coerce("v1.2") (VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '') >>> coerce("v2.5.2-bla") (VersionInfo(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') + +.. _sec_replace_deprecated_functions: + +Replacing Deprecated Functions +------------------------------ + +The development team of semver has decided to deprecate certain functions on +the module level. The preferred way of using semver is through the +:class:`semver.VersionInfo` class. + +The deprecated functions can still be used in version 2.x.y. In version 3 of +semver, the deprecated functions will be removed. + +The following list shows the deprecated functions and how you can replace +them with code which is compatible for future versions: + + +* :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` + + Replace them with the respective methods of the :class:`semver.VersionInfo` + class. + For example, the function :func:`semver.bump_major` is replaced by + :func:`semver.VersionInfo.bump_major` and calling the ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.bump_major("3.4.5") + >>> s2 = str(semver.VersionInfo.parse("3.4.5").bump_major()) + >>> s1 == s2 + True + + Likewise with the other module level functions. + +* :func:`semver.finalize_version` + + Replace it with :func:`semver.VersionInfo.finalize_version`: + + .. code-block:: python + + >>> s1 = semver.finalize_version('1.2.3-rc.5') + >>> s2 = str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) + >>> s1 == s2 + True + +* :func:`semver.format_version` + + Replace it with ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') + >>> s2 = str(semver.VersionInfo(5, 4, 3, 'pre.2', 'build.1')) + >>> s1 == s2 + True + +* :func:`semver.parse` + + Replace it with :func:`semver.VersionInfo.parse` and + :func:`semver.VersionInfo.to_dict`: + + .. code-block:: python + + >>> v1 = semver.parse("1.2.3") + >>> v2 = semver.VersionInfo.parse("1.2.3").to_dict() + >>> v1 == v2 + True + +* :func:`semver.parse_version_info` + + Replace it with :func:`semver.VersionInfo.parse`: + + .. code-block:: python + + >>> v1 = semver.parse_version_info("3.4.5") + >>> v2 = semver.VersionInfo.parse("3.4.5") + >>> v1 == v2 + True + +* :func:`semver.replace` + + Replace it with :func:`semver.VersionInfo.replace`: + + .. code-block:: python + + >>> s1 = semver.replace("1.2.3", major=2, patch=10) + >>> s2 = str(semver.VersionInfo.parse('1.2.3').replace(major=2, patch=10)) + >>> s1 == s2 + True + + +.. _sec_display_deprecation_warnings: + +Displaying Deprecation Warnings +------------------------------- + +By default, deprecation warnings are `ignored in Python `_. +This also affects semver's own warnings. + +It is recommended that you turn on deprecation warnings in your scripts. Use one of +the following methods: + +* Use the option `-Wd `_ + to enable default warnings: + + * Directly running the Python command:: + + $ python3 -Wd scriptname.py + + * Add the option in the shebang line (something like ``#!/usr/bin/python3``) + after the command:: + + #!/usr/bin/python3 -Wd + +* In your own scripts add a filter to ensure that *all* warnings are displayed: + + .. code-block:: python + + import warnings + warnings.simplefilter("default") + # Call your semver code + + For further details, see the section + `Overriding the default filter `_ + of the Python documentation. diff --git a/semver.py b/semver.py index aca7242e..aec5e9ef 100644 --- a/semver.py +++ b/semver.py @@ -3,9 +3,11 @@ import argparse import collections -from functools import wraps +from functools import wraps, partial +import inspect import re import sys +import warnings __version__ = "2.9.1" @@ -14,28 +16,6 @@ __maintainer__ = ["Sebastien Celles", "Tom Schraitle"] __maintainer_email__ = "s.celles@gmail.com" -_REGEX = re.compile( - r""" - ^ - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - (?:-(?P - (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) - (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* - ))? - (?:\+(?P - [0-9a-zA-Z-]+ - (?:\.[0-9a-zA-Z-]+)* - ))? - $ - """, - re.VERBOSE, -) - -_LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") #: Contains the implemented semver.org version of the spec SEMVER_SPEC_VERSION = "2.0.0" @@ -47,10 +27,66 @@ def cmp(a, b): return (a > b) - (a < b) +def deprecated(func=None, replace=None, version=None, category=DeprecationWarning): + """ + Decorates a function to output a deprecation warning. + + This function will be removed once major version 3 of semver is + released. + + :param str replace: the function to replace (use the full qualified + name like ``semver.VersionInfo.bump_major``. + :param str version: the first version when this function was deprecated. + :param category: allow you to specify the deprecation warning class + of your choice. By default, it's :class:`DeprecationWarning`, but + you can choose :class:`PendingDeprecationWarning``or a custom class. + """ + + if func is None: + return partial(deprecated, replace=replace, version=version, category=category) + + @wraps(func) + def wrapper(*args, **kwargs): + msg = ["Function '{m}.{f}' is deprecated."] + + if version: + msg.append("Deprecated since version {v}. ") + msg.append("This function will be removed in semver 3.") + if replace: + msg.append("Use {r!r} instead.") + else: + msg.append("Use the respective 'semver.VersionInfo.{r}' instead.") + + # hasattr is needed for Python2 compatibility: + f = func.__qualname__ if hasattr(func, "__qualname__") else func.__name__ + r = replace or f + + frame = inspect.currentframe().f_back + + msg = " ".join(msg) + warnings.warn_explicit( + msg.format(m=func.__module__, f=f, r=r, v=version), + category=category, + filename=inspect.getfile(frame.f_code), + lineno=frame.f_lineno, + ) + # As recommended in the Python documentation + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + # better remove the interpreter stack: + del frame + return func(*args, **kwargs) + + return wrapper + + +@deprecated(version="2.9.2") def parse(version): """ Parse version to major, minor, patch, pre-release, build parts. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.parse` instead. + :param version: version string :return: dictionary with the keys 'build', 'major', 'minor', 'patch', and 'prerelease'. The prerelease or build keys can be None @@ -69,17 +105,7 @@ def parse(version): >>> ver['build'] 'build.4' """ - match = _REGEX.match(version) - if match is None: - raise ValueError("%s is not valid SemVer string" % version) - - version_parts = match.groupdict() - - version_parts["major"] = int(version_parts["major"]) - version_parts["minor"] = int(version_parts["minor"]) - version_parts["patch"] = int(version_parts["patch"]) - - return version_parts + return VersionInfo.parse(version).to_dict() def comparator(operator): @@ -110,6 +136,29 @@ class VersionInfo(object): """ __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") + #: Regex for number in a prerelease + _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") + #: Regex for a semver version + _REGEX = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + (?:-(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* + ))? + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* + ))? + $ + """, + re.VERBOSE, + ) def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): self._major = int(major) @@ -163,10 +212,38 @@ def build(self): def build(self, value): raise AttributeError("attribute 'build' is readonly") - def _astuple(self): + def to_tuple(self): + """ + Convert the VersionInfo object to a tuple. + + .. versionadded:: 2.9.2 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. + + :return: a tuple with all the parts + :rtype: tuple + + >>> semver.VersionInfo(5, 3, 1).to_tuple() + (5, 3, 1, None, None) + """ return (self.major, self.minor, self.patch, self.prerelease, self.build) - def _asdict(self): + def to_dict(self): + """ + Convert the VersionInfo object to an OrderedDict. + + .. versionadded:: 2.9.2 + Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to + make this function available in the public API. + + :return: an OrderedDict with the keys in the order ``major``, ``minor``, + ``patch``, ``prerelease``, and ``build``. + :rtype: :class:`collections.OrderedDict` + + >>> semver.VersionInfo(3, 2, 1).to_dict() + OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ +('prerelease', None), ('build', None)]) + """ return collections.OrderedDict( ( ("major", self.major), @@ -177,12 +254,43 @@ def _asdict(self): ) ) + # For compatibility reasons: + @deprecated(replace="semver.VersionInfo.to_tuple", version="2.9.2") + def _astuple(self): + return self.to_tuple() # pragma: no cover + + _astuple.__doc__ = to_tuple.__doc__ + + @deprecated(replace="semver.VersionInfo.to_dict", version="2.9.2") + def _asdict(self): + return self.to_dict() # pragma: no cover + + _asdict.__doc__ = to_dict.__doc__ + def __iter__(self): """Implement iter(self).""" # As long as we support Py2.7, we can't use the "yield from" syntax - for v in self._astuple(): + for v in self.to_tuple(): yield v + @staticmethod + def _increment_string(string): + """ + Look for the last sequence of number(s) in a string and increment. + + :param str string: the string to search for. + :return: the incremented string + + Source: + http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 + """ + match = VersionInfo._LAST_NUMBER.search(string) + if match: + next_ = str(int(match.group(1)) + 1) + start, end = match.span(1) + string = string[: max(end - len(next_), start)] + next_ + string[end:] + return string + def bump_major(self): """ Raise the major part of the version, return a new object but leave self @@ -195,7 +303,8 @@ def bump_major(self): >>> ver.bump_major() VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) """ - return parse_version_info(bump_major(str(self))) + cls = type(self) + return cls(self._major + 1) def bump_minor(self): """ @@ -209,7 +318,8 @@ def bump_minor(self): >>> ver.bump_minor() VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) """ - return parse_version_info(bump_minor(str(self))) + cls = type(self) + return cls(self._major, self._minor + 1) def bump_patch(self): """ @@ -223,7 +333,8 @@ def bump_patch(self): >>> ver.bump_patch() VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None) """ - return parse_version_info(bump_patch(str(self))) + cls = type(self) + return cls(self._major, self._minor, self._patch + 1) def bump_prerelease(self, token="rc"): """ @@ -239,7 +350,9 @@ def bump_prerelease(self, token="rc"): VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \ build=None) """ - return parse_version_info(bump_prerelease(str(self), token)) + cls = type(self) + prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0") + return cls(self._major, self._minor, self._patch, prerelease) def bump_build(self, token="build"): """ @@ -255,41 +368,62 @@ def bump_build(self, token="build"): VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \ build='build.10') """ - return parse_version_info(bump_build(str(self), token)) + cls = type(self) + build = cls._increment_string(self._build or (token or "build") + ".0") + return cls(self._major, self._minor, self._patch, self._prerelease, build) @comparator def __eq__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) == 0 @comparator def __ne__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) != 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) != 0 @comparator def __lt__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) < 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) < 0 @comparator def __le__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) <= 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) <= 0 @comparator def __gt__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) > 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) > 0 @comparator def __ge__(self, other): - return _compare_by_keys(self._asdict(), _to_dict(other)) >= 0 + return _compare_by_keys(self.to_dict(), _to_dict(other)) >= 0 def __repr__(self): - s = ", ".join("%s=%r" % (key, val) for key, val in self._asdict().items()) + s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) return "%s(%s)" % (type(self).__name__, s) def __str__(self): - return format_version(*(self._astuple())) + """str(self)""" + version = "%d.%d.%d" % (self.major, self.minor, self.patch) + if self.prerelease: + version += "-%s" % self.prerelease + if self.build: + version += "+%s" % self.build + return version def __hash__(self): - return hash(self._astuple()) + return hash(self.to_tuple()) + + def finalize_version(self): + """ + Remove any prerelease and build metadata from the version. + + :return: a new instance with the finalized version string + :rtype: :class:`VersionInfo` + + >>> str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) + '1.2.3' + """ + cls = type(self) + return cls(self.major, self.minor, self.patch) @staticmethod def parse(version): @@ -304,7 +438,17 @@ def parse(version): VersionInfo(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') """ - return parse_version_info(version) + match = VersionInfo._REGEX.match(version) + if match is None: + raise ValueError("%s is not valid SemVer string" % version) + + version_parts = match.groupdict() + + version_parts["major"] = int(version_parts["major"]) + version_parts["minor"] = int(version_parts["minor"]) + version_parts["patch"] = int(version_parts["patch"]) + + return VersionInfo(**version_parts) def replace(self, **parts): """ @@ -320,12 +464,12 @@ def replace(self, **parts): parts :raises: TypeError, if ``parts`` contains invalid keys """ - version = self._asdict() + version = self.to_dict() version.update(parts) try: return VersionInfo(**version) except TypeError: - unknownkeys = set(parts) - set(self._asdict()) + unknownkeys = set(parts) - set(self.to_dict()) error = "replace() got %d unexpected keyword " "argument(s): %s" % ( len(unknownkeys), ", ".join(unknownkeys), @@ -353,16 +497,23 @@ def isvalid(cls, version): def _to_dict(obj): if isinstance(obj, VersionInfo): - return obj._asdict() + return obj.to_dict() elif isinstance(obj, tuple): - return VersionInfo(*obj)._asdict() + return VersionInfo(*obj).to_dict() return obj +@deprecated(replace="semver.VersionInfo.parse", version="2.9.2") def parse_version_info(version): """ Parse version string to a VersionInfo instance. + .. versionadded:: 2.7.2 + Added :func:`parse_version_info` + + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.parse` instead. + .. versionadded:: 2.7.2 Added :func:`parse_version_info` @@ -382,16 +533,7 @@ def parse_version_info(version): >>> version_info.build 'build.4' """ - parts = parse(version) - version_info = VersionInfo( - parts["major"], - parts["minor"], - parts["patch"], - parts["prerelease"], - parts["build"], - ) - - return version_info + return VersionInfo.parse(version) def _nat_cmp(a, b): @@ -458,7 +600,8 @@ def compare(ver1, ver2): 0 """ - v1, v2 = parse(ver1), parse(ver2) + v1 = VersionInfo.parse(ver1).to_dict() + v2 = VersionInfo.parse(ver2).to_dict() return _compare_by_keys(v1, v2) @@ -550,10 +693,14 @@ def min_ver(ver1, ver2): return ver2 +@deprecated(replace="str(versionobject)", version="2.9.2") def format_version(major, minor, patch, prerelease=None, build=None): """ Format a version string according to the Semantic Versioning specification. + .. deprecated:: 2.9.2 + Use ``str(VersionInfo(VERSION)`` instead. + :param int major: the required major part of a version :param int minor: the required minor part of a version :param int patch: the required patch part of a version @@ -565,34 +712,17 @@ def format_version(major, minor, patch, prerelease=None, build=None): >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') '3.4.5-pre.2+build.4' """ - version = "%d.%d.%d" % (major, minor, patch) - if prerelease is not None: - version = version + "-%s" % prerelease - - if build is not None: - version = version + "+%s" % build - - return version - - -def _increment_string(string): - """ - Look for the last sequence of number(s) in a string and increment, from: - - http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 - """ - match = _LAST_NUMBER.search(string) - if match: - next_ = str(int(match.group(1)) + 1) - start, end = match.span(1) - string = string[: max(end - len(next_), start)] + next_ + string[end:] - return string + return str(VersionInfo(major, minor, patch, prerelease, build)) +@deprecated(version="2.9.2") def bump_major(version): """ Raise the major part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_major` instead. + :param: version string :return: the raised version string :rtype: str @@ -600,14 +730,17 @@ def bump_major(version): >>> semver.bump_major("3.4.5") '4.0.0' """ - verinfo = parse(version) - return format_version(verinfo["major"] + 1, 0, 0) + return str(VersionInfo.parse(version).bump_major()) +@deprecated(version="2.9.2") def bump_minor(version): """ Raise the minor part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_minor` instead. + :param: version string :return: the raised version string :rtype: str @@ -615,14 +748,17 @@ def bump_minor(version): >>> semver.bump_minor("3.4.5") '3.5.0' """ - verinfo = parse(version) - return format_version(verinfo["major"], verinfo["minor"] + 1, 0) + return str(VersionInfo.parse(version).bump_minor()) +@deprecated(version="2.9.2") def bump_patch(version): """ Raise the patch part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_patch` instead. + :param: version string :return: the raised version string :rtype: str @@ -630,14 +766,17 @@ def bump_patch(version): >>> semver.bump_patch("3.4.5") '3.4.6' """ - verinfo = parse(version) - return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"] + 1) + return str(VersionInfo.parse(version).bump_patch()) +@deprecated(version="2.9.2") def bump_prerelease(version, token="rc"): """ Raise the prerelease part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_prerelease` instead. + :param version: version string :param token: defaults to 'rc' :return: the raised version string @@ -646,19 +785,17 @@ def bump_prerelease(version, token="rc"): >>> semver.bump_prerelease('3.4.5', 'dev') '3.4.5-dev.1' """ - verinfo = parse(version) - verinfo["prerelease"] = _increment_string( - verinfo["prerelease"] or (token or "rc") + ".0" - ) - return format_version( - verinfo["major"], verinfo["minor"], verinfo["patch"], verinfo["prerelease"] - ) + return str(VersionInfo.parse(version).bump_prerelease(token)) +@deprecated(version="2.9.2") def bump_build(version, token="build"): """ Raise the build part of the version string. + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.bump_build` instead. + :param version: version string :param token: defaults to 'build' :return: the raised version string @@ -667,17 +804,10 @@ def bump_build(version, token="build"): >>> semver.bump_build('3.4.5-rc.1+build.9') '3.4.5-rc.1+build.10' """ - verinfo = parse(version) - verinfo["build"] = _increment_string(verinfo["build"] or (token or "build") + ".0") - return format_version( - verinfo["major"], - verinfo["minor"], - verinfo["patch"], - verinfo["prerelease"], - verinfo["build"], - ) + return str(VersionInfo.parse(version).bump_build(token)) +@deprecated(version="2.9.2") def finalize_version(version): """ Remove any prerelease and build metadata from the version string. @@ -685,6 +815,9 @@ def finalize_version(version): .. versionadded:: 2.7.9 Added :func:`finalize_version` + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.finalize_version` instead. + :param version: version string :return: the finalized version string :rtype: str @@ -692,8 +825,33 @@ def finalize_version(version): >>> semver.finalize_version('1.2.3-rc.5') '1.2.3' """ - verinfo = parse(version) - return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"]) + verinfo = VersionInfo.parse(version) + return str(verinfo.finalize_version()) + + +@deprecated(version="2.9.2") +def replace(version, **parts): + """ + Replace one or more parts of a version and return the new string. + + .. versionadded:: 2.9.0 + Added :func:`replace` + + .. deprecated:: 2.9.2 + Use :func:`semver.VersionInfo.replace` instead. + + :param str version: the version string to replace + :param dict parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the replaced version string + :raises: TypeError, if ``parts`` contains invalid keys + :rtype: str + + >>> import semver + >>> semver.replace("1.2.3", major=2, patch=10) + '2.2.10' + """ + return str(VersionInfo.parse(version).replace(**parts)) def cmd_bump(args): @@ -719,7 +877,7 @@ def cmd_bump(args): # print the help and exit args.parser.parse_args(["bump", "-h"]) - ver = parse_version_info(args.version) + ver = VersionInfo.parse(args.version) # get the respective method and call it func = getattr(ver, maptable[args.bump]) return str(func()) @@ -838,28 +996,6 @@ def main(cliargs=None): return 2 -def replace(version, **parts): - """ - Replace one or more parts of a version and return the new string. - - .. versionadded:: 2.9.0 - Added :func:`replace` - - :param str version: the version string to replace - :param dict parts: the parts to be updated. Valid keys are: - ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` - :return: the replaced version string - :raises: TypeError, if ``parts`` contains invalid keys - :rtype: str - - >>> import semver - >>> semver.replace("1.2.3", major=2, patch=10) - '2.2.10' - """ - version = parse_version_info(version) - return str(version.replace(**parts)) - - if __name__ == "__main__": import doctest diff --git a/setup.cfg b/setup.cfg index 6ab7e562..1cefc4bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,8 @@ [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ testpaths = . docs +filterwarnings = + ignore:Function 'semver.*:DeprecationWarning addopts = --no-cov-on-fail --cov=semver diff --git a/test_semver.py b/test_semver.py index ed7d80bc..4611d771 100644 --- a/test_semver.py +++ b/test_semver.py @@ -14,6 +14,7 @@ cmd_compare, compare, createparser, + deprecated, finalize_version, format_version, main, @@ -54,9 +55,7 @@ def does_not_raise(item): "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] ) def test_should_private_increment_string(string, expected): - from semver import _increment_string - - assert _increment_string(string) == expected + assert VersionInfo._increment_string(string) == expected @pytest.fixture @@ -393,6 +392,18 @@ def test_should_versioninfo_bump_multiple(): assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected +def test_should_versioninfo_to_dict(version): + resultdict = version.to_dict() + assert isinstance(resultdict, dict), "Got type from to_dict" + assert list(resultdict.keys()) == ["major", "minor", "patch", "prerelease", "build"] + + +def test_should_versioninfo_to_tuple(version): + result = version.to_tuple() + assert isinstance(result, tuple), "Got type from to_dict" + assert len(result) == 5, "Different length from to_tuple()" + + def test_should_ignore_extensions_for_bump(): assert bump_patch("3.4.5-rc1+build4") == "3.4.6" @@ -777,6 +788,13 @@ def test_should_raise_systemexit_when_bump_iscalled_with_empty_arguments(): main(["bump"]) +def test_should_process_check_iscalled_with_valid_version(capsys): + result = main(["check", "1.1.1"]) + assert not result + captured = capsys.readouterr() + assert not captured.out + + @pytest.mark.parametrize( "version,parts,expected", [ @@ -827,3 +845,37 @@ def test_replace_raises_ValueError_for_non_numeric_values(): def test_should_versioninfo_isvalid(): assert VersionInfo.isvalid("1.0.0") is True assert VersionInfo.isvalid("foo") is False + + +@pytest.mark.parametrize( + "func, args, kwargs", + [ + (bump_build, ("1.2.3",), {}), + (bump_major, ("1.2.3",), {}), + (bump_minor, ("1.2.3",), {}), + (bump_patch, ("1.2.3",), {}), + (bump_prerelease, ("1.2.3",), {}), + (format_version, (3, 4, 5), {}), + (finalize_version, ("1.2.3-rc.5",), {}), + (parse, ("1.2.3",), {}), + (parse_version_info, ("1.2.3",), {}), + (replace, ("1.2.3",), dict(major=2, patch=10)), + ], +) +def test_should_raise_deprecation_warnings(func, args, kwargs): + with pytest.warns( + DeprecationWarning, match=r"Function 'semver.[_a-zA-Z]+' is deprecated." + ) as record: + func(*args, **kwargs) + if not record: + pytest.fail("Expected a DeprecationWarning for {}".format(func.__name__)) + assert len(record), "Expected one DeprecationWarning record" + + +def test_deprecated_deco_without_argument(): + @deprecated + def mock_func(): + return True + + with pytest.deprecated_call(): + assert mock_func() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy