diff --git a/.gitignore b/.gitignore index ffddda1c..d26b5515 100644 --- a/.gitignore +++ b/.gitignore @@ -259,7 +259,8 @@ fabric.properties # -------- - # Patch/Diff Files *.patch *.diff +docs/_api +!docs/_api/semver.__about__.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e529404..00bc7813 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -102,323 +102,6 @@ Trivial/Internal Changes * :pr:`290`: Add supported Python versions to :command:`black`. - ----- - - -Version 2.13.0 -============== - -:Released: 2020-10-20 -:Maintainer: Tom Schraitle - - -Features --------- - -* :pr:`287`: Document how to create subclass from ``VersionInfo`` - - -Bug Fixes ---------- - -* :pr:`283`: Ensure equal versions have equal hashes. - Version equality means for semver, that ``major``, - ``minor``, ``patch``, and ``prerelease`` parts are - equal in both versions you compare. The ``build`` part - is ignored. - - -Additions ---------- - -n/a - - -Deprecations ------------- - -n/a - - ----- - - -Version 2.12.0 -============== - -:Released: 2020-10-19 -:Maintainer: Tom Schraitle - - -Bug Fixes ---------- - -* :gh:`291` (:pr:`292`): Disallow negative numbers of - ``major``, ``minor``, and ``patch`` for :class:`semver.VersionInfo` - - ----- - - -Version 2.11.0 -============== - -:Released: 2020-10-17 -:Maintainer: Tom Schraitle - - -Bug Fixes ---------- - -* :gh:`276` (:pr:`277`): ``VersionInfo.parse`` should be a class method - Also add authors and update changelog in :gh:`286` -* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError - - ----- - - -Version 2.10.2 -============== - -:Released: 2020-06-15 -:Maintainer: Tom Schraitle - -Features --------- - -:gh:`268`: Increase coverage - - -Bug Fixes ---------- - -* :gh:`260` (:pr:`261`): Fixed ``__getitem__`` returning None on wrong parts -* :pr:`263`: Doc: Add missing "install" subcommand for openSUSE - - -Deprecations ------------- - -* :gh:`160` (:pr:`264`): - * :func:`semver.max_ver` - * :func:`semver.min_ver` - - ----- - - -Version 2.10.1 -============== - -:Released: 2020-05-13 -:Maintainer: Tom Schraitle - - -Features --------- - -* :pr:`249`: Added release policy and version restriction in documentation to - help our users which would like to stay on the major 2 release. -* :pr:`250`: Simplified installation semver on openSUSE with ``obs://``. -* :pr:`256`: Made docstrings consistent - - - -Bug Fixes ---------- - -* :gh:`251` (:pr:`254`): Fixed return type of ``semver.VersionInfo.next_version`` - to always return a ``VersionInfo`` instance. - - ----- - - - -Version 2.10.0 -============== - -:Released: 2020-05-05 -:Maintainer: Tom Schraitle - -Features --------- - -* :pr:`138`: Added ``__getitem__`` magic method to ``semver.VersionInfo`` class. - Allows to access a version like ``version[1]``. -* :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising - the old and deprecated module-level functions. -* :pr:`230`: Add version information in some functions: - - * Use ``.. versionadded::`` RST directive in docstrings to - make it more visible when something was added - * Minor wording fix in docstrings (versions -> version strings) - - -Bug Fixes ---------- - -* :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``, - ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)`` -* :gh:`244` (:pr:`245`): Allow comparison with ``VersionInfo``, tuple/list, dict, and string. - - -Additions ---------- - -* :pr:`228`: Added better doctest integration - - -Deprecations ------------- -* :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 -============= -:Released: 2020-02-16 -:Maintainer: Tom Schraitle - -Features --------- - -* :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) -* :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub -* :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation -* :gh:`191` (:pr:`194`): Created manpage for pysemver -* :gh:`196` (:pr:`197`): Added distribution specific installation instructions -* :gh:`201` (:pr:`202`): Reformatted source code with black -* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` - and extend :command:`pysemver` with :command:`check` subcommand -* :gh:`210` (:pr:`215`): Document how to deal with invalid versions -* :pr:`212`: Improve docstrings according to PEP257 - -Bug Fixes ---------- - -* :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments - - ----- - -Version 2.9.0 -============= -:Released: 2019-10-30 -:Maintainer: Sébastien Celles - -Features --------- - -* :gh:`59` (:pr:`164`): Implemented a command line interface -* :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section -* :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` -* :gh:`112`, :gh:`113`: Added Python 3.7 support -* :pr:`120`: Improved test_immutable function with properties -* :pr:`125`: Created :file:`setup.cfg` for pytest and tox -* :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` -* :gh:`142` (:pr:`143`): Improved usage section -* :gh:`144` (:pr:`156`): Added :func:`semver.replace` and :func:`semver.VersionInfo.replace` - functions -* :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` -* :pr:`157`: Introduce :file:`conftest.py` to improve doctests -* :pr:`165`: Improved code coverage -* :pr:`166`: Reworked :file:`.gitignore` file -* :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` - -Bug Fixes ---------- - -* :gh:`102`: Fixed comparison between VersionInfo and tuple -* :gh:`103`: Disallow comparison between VersionInfo and string (and int) -* :gh:`121` (:pr:`122`): Use python3 instead of python3.4 in :file:`tox.ini` -* :pr:`123`: Improved :func:`__repr__` and derive class name from :func:`type` -* :gh:`128` (:pr:`129`): Fixed wrong datatypes in docstring for :func:`semver.format_version` -* :gh:`135` (:pr:`140`): Converted prerelease and build to string -* :gh:`136` (:pr:`151`): Added testsuite to tarball -* :gh:`154` (:pr:`155`): Improved README description - -Removals --------- - -* :gh:`111` (:pr:`110`): Dropped Python 3.3 -* :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` - - ----- - -Version 2.8.2 -============= -:Released: 2019-05-19 -:Maintainer: Sébastien Celles - -Skipped, not released. - ----- - -Version 2.8.1 -============= -:Released: 2018-07-09 -:Maintainer: Sébastien Celles - -Features --------- - -* :gh:`40` (:pr:`88`): Added a static parse method to VersionInfo -* :gh:`77` (:pr:`47`): Converted multiple tests into pytest.mark.parametrize -* :gh:`87`, :gh:`94` (:pr:`93`): Removed named tuple inheritance. -* :gh:`89` (:pr:`90`): Added doctests. - -Bug Fixes ---------- - -* :gh:`98` (:pr:`99`): Set prerelease and build to None by default -* :gh:`96` (:pr:`97`): Made VersionInfo immutable - - ----- - -Version 2.8.0 -============= -:Released: 2018-05-16 -:Maintainer: Sébastien Celles - - -Changes -------- - -* :gh:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so - py.test can autodiscover test file - -Additions ---------- - -* :gh:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file -* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` - -Removals --------- - -* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility - - .. Local variables: coding: utf-8 diff --git a/README.rst b/README.rst index 2baef45c..d4f29819 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. |MAINT| replace:: ``maint/v2`` .. _MAINT: https://github.com/python-semver/python-semver/tree/maint/v2 + The module follows the ``MAJOR.MINOR.PATCH`` style: * ``MAJOR`` version when you make incompatible API changes, @@ -45,11 +46,11 @@ To import this library, use: >>> import semver Working with the library is quite straightforward. To turn a version string into the -different parts, use the ``semver.VersionInfo.parse`` function: +different parts, use the ``semver.Version.parse`` function: .. code-block:: python - >>> ver = semver.VersionInfo.parse('1.2.3-pre.2+build.4') + >>> ver = semver.Version.parse('1.2.3-pre.2+build.4') >>> ver.major 1 >>> ver.minor @@ -62,21 +63,21 @@ different parts, use the ``semver.VersionInfo.parse`` function: 'build.4' To raise parts of a version, there are a couple of functions available for -you. The function ``semver.VersionInfo.bump_major`` leaves the original object untouched, but -returns a new ``semver.VersionInfo`` instance with the raised major part: +you. The function ``semver.Version.bump_major`` leaves the original object untouched, but +returns a new ``semver.Version`` instance with the raised major part: .. code-block:: python - >>> ver = semver.VersionInfo.parse("3.4.5") + >>> ver = semver.Version.parse("3.4.5") >>> ver.bump_major() - VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) + Version(major=4, minor=0, patch=0, prerelease=None, build=None) It is allowed to concatenate different "bump functions": .. code-block:: python >>> ver.bump_major().bump_minor() - VersionInfo(major=4, minor=1, patch=0, prerelease=None, build=None) + Version(major=4, minor=1, patch=0, prerelease=None, build=None) To compare two versions, semver provides the ``semver.compare`` function. The return value indicates the relationship between the first and second diff --git a/changelog.d/169.deprecation.rst b/changelog.d/169.deprecation.rst new file mode 100644 index 00000000..9ce5ef6b --- /dev/null +++ b/changelog.d/169.deprecation.rst @@ -0,0 +1 @@ +Deprecate CLI functions not imported from ``semver.cli``. \ No newline at end of file diff --git a/changelog.d/169.feature.rst b/changelog.d/169.feature.rst new file mode 100644 index 00000000..1b762676 --- /dev/null +++ b/changelog.d/169.feature.rst @@ -0,0 +1,10 @@ +Create semver package and split code among different modules in the packages. + +* Remove :file:`semver.py` +* Create :file:`src/semver/__init__.py` +* Create :file:`src/semver/cli.py` for all CLI methods +* Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions +* Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` +* Create :file:`src/semver/_types.py` to hold type aliases +* Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions +* Create :file:`src/semver/__about__.py` for all the metadata variables diff --git a/changelog.d/169.trivial.rst b/changelog.d/169.trivial.rst new file mode 100644 index 00000000..536e2b88 --- /dev/null +++ b/changelog.d/169.trivial.rst @@ -0,0 +1,8 @@ +Adapted infrastructure code to the new project layout. + +* Replace :file:`setup.py` with :file:`setup.cfg` because the :file:`setup.cfg` is easier to use +* Adapt documentation code snippets where needed +* Adapt tests +* Changed the ``deprecated`` to hardcode the ``semver`` package name in the warning. + +Increase coverage to 100% for all non-deprecated APIs \ No newline at end of file diff --git a/changelog.d/304.doc.rst b/changelog.d/304.doc.rst new file mode 100644 index 00000000..3fa09bc6 --- /dev/null +++ b/changelog.d/304.doc.rst @@ -0,0 +1,5 @@ +Several improvements in documentation: + +* Reorganize API documentation. +* Add migration chapter from semver2 to semver3. +* Distinguish between changlog for version 2 and 3 diff --git a/changelog.d/304.trivial.rst b/changelog.d/304.trivial.rst new file mode 100644 index 00000000..fe0d012a --- /dev/null +++ b/changelog.d/304.trivial.rst @@ -0,0 +1,10 @@ +Support PEP-561 :file:`py.typed`. + +According to the mentioned PEP: + + "Package maintainers who wish to support type checking + of their code MUST add a marker file named :file:`py.typed` + to their package supporting typing." + +Add package_data to :file:`setup.cfg` to include this marker in dist +and whl file. diff --git a/changelog.d/305.doc.rst b/changelog.d/305.doc.rst new file mode 100644 index 00000000..1ce69247 --- /dev/null +++ b/changelog.d/305.doc.rst @@ -0,0 +1 @@ +Add note about :class:`Version` rename. \ No newline at end of file diff --git a/changelog.d/305.feature.rst b/changelog.d/305.feature.rst new file mode 100644 index 00000000..98ef9665 --- /dev/null +++ b/changelog.d/305.feature.rst @@ -0,0 +1 @@ +Rename :class:`VersionInfo` to :class:`Version` but keep an alias for compatibility \ No newline at end of file diff --git a/docs/_api/semver.__about__.rst b/docs/_api/semver.__about__.rst new file mode 100644 index 00000000..22395ebd --- /dev/null +++ b/docs/_api/semver.__about__.rst @@ -0,0 +1,5 @@ +semver.\_\_about\_\_ module +=========================== + +.. automodule:: semver.__about__ + :members: diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 33ff51f1..002e6b2f 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -26,6 +26,11 @@ div.related.top nav { margin-top: 0.5em; } +.sphinxsidebarwrapper .caption { + margin-top: 1em; + margin-bottom: -0.75em; +} + .section h1 { font-weight: 700; } diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 6bae6eed..7a114d41 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -2,3 +2,48 @@ Import the theme's layout. #} {% extends "!layout.html" %} + +{%- block footer %} + + +{% if theme_github_banner|lower != 'false' %} + + Fork me on GitHub + +{% endif %} +{% if theme_analytics_id %} + +{% endif %} +{%- endblock %} \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index 0003fefc..9d884601 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,10 @@ .. _api: +### API -=== +### -.. automodule:: semver - :members: - :undoc-members: +.. toctree:: + :maxdepth: 4 + + _api/semver \ No newline at end of file diff --git a/docs/changelog-2.7.9-and-before.rst b/docs/changelog-2.7.9-and-before.rst deleted file mode 100644 index f7acc1e1..00000000 --- a/docs/changelog-2.7.9-and-before.rst +++ /dev/null @@ -1,353 +0,0 @@ -################ -Older Change Log -################ - -This changelog contains older entries from -2.7.9 and before. - -Version 2.7.9 -============= - -:Released: 2017-09-23 -:Maintainer: Kostiantyn Rybnikov - - -Additions ---------- - -* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. - - ----- - -Version 2.7.8 -============= - -:Released: 2017-08-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`62`: Support custom default names for pre and build - - ----- - -Version 2.7.7 -============= - -:Released: 2017-05-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects -* :pr:`56`: Added support for Python 3.6 - - ----- - -Version 2.7.2 -============= - -:Released: 2016-11-08 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added :func:`semver.parse_version_info` to parse a version string to a - version info tuple. - -Bug Fixes ---------- - -* :gh:`37`: Removed trailing zeros from prelease doesn't allow to - parse 0 pre-release version - -* Refine parsing to conform more strictly to SemVer 2.0.0. - - SemVer 2.0.0 specification §9 forbids leading zero on identifiers in - the prerelease version. - - ----- - -Version 2.6.0 -============= - -:Released: 2016-06-08 -:Maintainer: Kostiantyn Rybnikov - -Removals --------- - -* Remove comparison of build component. - - SemVer 2.0.0 specification recommends that build component is - ignored in comparisons. - - ----- - -Version 2.5.0 -============= - -:Released: 2016-05-25 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Support matching 'not equal' with “!=”. - -Changes -------- - -* Made separate builds for tests on Travis CI. - - ----- - -Version 2.4.2 -============= - -:Released: 2016-05-16 -:Maintainer: Kostiantyn Rybnikov - -Changes -------- - -* Migrated README document to reStructuredText format. - -* Used Setuptools for distribution management. - -* Migrated test cases to Py.test. - -* Added configuration for Tox test runner. - - ----- - -Version 2.4.1 -============= - -:Released: 2016-03-04 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* :gh:`23`: Compared build component of a version. - - ----- - -Version 2.4.0 -============= - -:Released: 2016-02-12 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* :gh:`21`: Compared alphanumeric components correctly. - - ----- - -Version 2.3.1 -============= - -:Released: 2016-01-30 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Declared granted license name in distribution metadata. - - ----- - -Version 2.3.0 -============= - -:Released: 2016-01-29 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added functions to increment prerelease and build components in a - version. - - ----- - -Version 2.2.1 -============= - -:Released: 2015-08-04 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Corrected comparison when any component includes zero. - - ----- - -Version 2.2.0 -============= - -:Released: 2015-06-21 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Add functions to determined minimum and maximum version. - -* Add code examples for recently-added functions. - - ----- - -Version 2.1.2 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Restored current README document to distribution manifest. - - ----- - -Version 2.1.1 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Removed absent document from distribution manifest. - - ----- - -Version 2.1.0 -============= - -:Released: 2015-05-22 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Documented installation instructions. - -* Documented project home page. - -* Added function to format a version string from components. - -* Added functions to increment specific components in a version. - -Changes -------- - -* Migrated README document to Markdown format. - -Bug Fixes ---------- - -* Corrected code examples in README document. - - ----- - -Version 2.0.2 -============= - -:Released: 2015-04-14 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Added configuration for Travis continuous integration. - -* Explicitly declared supported Python versions. - - ----- - -Version 2.0.1 -============= - -:Released: 2014-09-24 -:Maintainer: Konstantine Rybnikov - -Bug Fixes ---------- - -* :gh:`9`: Fixed comparison of equal version strings. - - ----- - -Version 2.0.0 -============= - -:Released: 2014-05-24 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Grant license in this code base under BSD 3-clause license terms. - -Changes -------- - -* Update parser to SemVer standard 2.0.0. - -* Ignore build component for comparison. - - ----- - -Version 0.0.2 -============= - -:Released: 2012-05-10 -:Maintainer: Konstantine Rybnikov - -Changes -------- - -* Use standard library Distutils for distribution management. - - ----- - -Version 0.0.1 -============= - -:Released: 2012-04-28 -:Maintainer: Konstantine Rybnikov - -* Initial release. - - -.. - Local variables: - coding: utf-8 - mode: text - mode: rst - End: - vim: fileencoding=utf-8 filetype=rst : diff --git a/docs/changelog-semver2.rst b/docs/changelog-semver2.rst new file mode 100644 index 00000000..dca94413 --- /dev/null +++ b/docs/changelog-semver2.rst @@ -0,0 +1,670 @@ +################## +Change Log semver2 +################## + +This changelog contains older entries for semver2. + +---- + + +Version 2.13.0 +============== + +:Released: 2020-10-20 +:Maintainer: Tom Schraitle + + +Features +-------- + +* :pr:`287`: Document how to create subclass from ``VersionInfo`` + + +Bug Fixes +--------- + +* :pr:`283`: Ensure equal versions have equal hashes. + Version equality means for semver, that ``major``, + ``minor``, ``patch``, and ``prerelease`` parts are + equal in both versions you compare. The ``build`` part + is ignored. + + +Additions +--------- + +n/a + + +Deprecations +------------ + +n/a + + +---- + + +Version 2.12.0 +============== + +:Released: 2020-10-19 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`291` (:pr:`292`): Disallow negative numbers of + ``major``, ``minor``, and ``patch`` for :class:`semver.VersionInfo` + + +---- + + +Version 2.11.0 +============== + +:Released: 2020-10-17 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`276` (:pr:`277`): ``VersionInfo.parse`` should be a class method + Also add authors and update changelog in :gh:`286` +* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError + + +---- + + +Version 2.10.2 +============== + +:Released: 2020-06-15 +:Maintainer: Tom Schraitle + +Features +-------- + +:gh:`268`: Increase coverage + + +Bug Fixes +--------- + +* :gh:`260` (:pr:`261`): Fixed ``__getitem__`` returning None on wrong parts +* :pr:`263`: Doc: Add missing "install" subcommand for openSUSE + + +Deprecations +------------ + +* :gh:`160` (:pr:`264`): + * :func:`semver.max_ver` + * :func:`semver.min_ver` + + +---- + + +Version 2.10.1 +============== + +:Released: 2020-05-13 +:Maintainer: Tom Schraitle + + +Features +-------- + +* :pr:`249`: Added release policy and version restriction in documentation to + help our users which would like to stay on the major 2 release. +* :pr:`250`: Simplified installation semver on openSUSE with ``obs://``. +* :pr:`256`: Made docstrings consistent + + + +Bug Fixes +--------- + +* :gh:`251` (:pr:`254`): Fixed return type of ``semver.VersionInfo.next_version`` + to always return a ``VersionInfo`` instance. + + +---- + + + +Version 2.10.0 +============== + +:Released: 2020-05-05 +:Maintainer: Tom Schraitle + +Features +-------- + +* :pr:`138`: Added ``__getitem__`` magic method to ``semver.VersionInfo`` class. + Allows to access a version like ``version[1]``. +* :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising + the old and deprecated module-level functions. +* :pr:`230`: Add version information in some functions: + + * Use ``.. versionadded::`` RST directive in docstrings to + make it more visible when something was added + * Minor wording fix in docstrings (versions -> version strings) + + +Bug Fixes +--------- + +* :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``, + ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)`` +* :gh:`244` (:pr:`245`): Allow comparison with ``VersionInfo``, tuple/list, dict, and string. + + +Additions +--------- + +* :pr:`228`: Added better doctest integration + + +Deprecations +------------ +* :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 +============= +:Released: 2020-02-16 +:Maintainer: Tom Schraitle + +Features +-------- + +* :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) +* :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub +* :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation +* :gh:`191` (:pr:`194`): Created manpage for pysemver +* :gh:`196` (:pr:`197`): Added distribution specific installation instructions +* :gh:`201` (:pr:`202`): Reformatted source code with black +* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` + and extend :command:`pysemver` with :command:`check` subcommand +* :gh:`210` (:pr:`215`): Document how to deal with invalid versions +* :pr:`212`: Improve docstrings according to PEP257 + +Bug Fixes +--------- + +* :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments + + +---- + +Version 2.9.0 +============= +:Released: 2019-10-30 +:Maintainer: Sébastien Celles + +Features +-------- + +* :gh:`59` (:pr:`164`): Implemented a command line interface +* :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section +* :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` +* :gh:`112`, :gh:`113`: Added Python 3.7 support +* :pr:`120`: Improved test_immutable function with properties +* :pr:`125`: Created :file:`setup.cfg` for pytest and tox +* :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` +* :gh:`142` (:pr:`143`): Improved usage section +* :gh:`144` (:pr:`156`): Added :func:`semver.replace` and :func:`semver.VersionInfo.replace` + functions +* :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` +* :pr:`157`: Introduce :file:`conftest.py` to improve doctests +* :pr:`165`: Improved code coverage +* :pr:`166`: Reworked :file:`.gitignore` file +* :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` + +Bug Fixes +--------- + +* :gh:`102`: Fixed comparison between VersionInfo and tuple +* :gh:`103`: Disallow comparison between VersionInfo and string (and int) +* :gh:`121` (:pr:`122`): Use python3 instead of python3.4 in :file:`tox.ini` +* :pr:`123`: Improved :func:`__repr__` and derive class name from :func:`type` +* :gh:`128` (:pr:`129`): Fixed wrong datatypes in docstring for :func:`semver.format_version` +* :gh:`135` (:pr:`140`): Converted prerelease and build to string +* :gh:`136` (:pr:`151`): Added testsuite to tarball +* :gh:`154` (:pr:`155`): Improved README description + +Removals +-------- + +* :gh:`111` (:pr:`110`): Dropped Python 3.3 +* :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` + + +---- + +Version 2.8.2 +============= +:Released: 2019-05-19 +:Maintainer: Sébastien Celles + +Skipped, not released. + +---- + +Version 2.8.1 +============= +:Released: 2018-07-09 +:Maintainer: Sébastien Celles + +Features +-------- + +* :gh:`40` (:pr:`88`): Added a static parse method to VersionInfo +* :gh:`77` (:pr:`47`): Converted multiple tests into pytest.mark.parametrize +* :gh:`87`, :gh:`94` (:pr:`93`): Removed named tuple inheritance. +* :gh:`89` (:pr:`90`): Added doctests. + +Bug Fixes +--------- + +* :gh:`98` (:pr:`99`): Set prerelease and build to None by default +* :gh:`96` (:pr:`97`): Made VersionInfo immutable + + +---- + +Version 2.8.0 +============= +:Released: 2018-05-16 +:Maintainer: Sébastien Celles + + +Changes +------- + +* :gh:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so + py.test can autodiscover test file + +Additions +--------- + +* :gh:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file +* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` + +Removals +-------- + +* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility + +---- + + +Version 2.7.9 +============= + +:Released: 2017-09-23 +:Maintainer: Kostiantyn Rybnikov + + +Additions +--------- + +* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. + + +---- + +Version 2.7.8 +============= + +:Released: 2017-08-25 +:Maintainer: Kostiantyn Rybnikov + +* :gh:`62`: Support custom default names for pre and build + + +---- + +Version 2.7.7 +============= + +:Released: 2017-05-25 +:Maintainer: Kostiantyn Rybnikov + +* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects +* :pr:`56`: Added support for Python 3.6 + + +---- + +Version 2.7.2 +============= + +:Released: 2016-11-08 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Added :func:`semver.parse_version_info` to parse a version string to a + version info tuple. + +Bug Fixes +--------- + +* :gh:`37`: Removed trailing zeros from prelease doesn't allow to + parse 0 pre-release version + +* Refine parsing to conform more strictly to SemVer 2.0.0. + + SemVer 2.0.0 specification §9 forbids leading zero on identifiers in + the prerelease version. + + +---- + +Version 2.6.0 +============= + +:Released: 2016-06-08 +:Maintainer: Kostiantyn Rybnikov + +Removals +-------- + +* Remove comparison of build component. + + SemVer 2.0.0 specification recommends that build component is + ignored in comparisons. + + +---- + +Version 2.5.0 +============= + +:Released: 2016-05-25 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Support matching 'not equal' with “!=”. + +Changes +------- + +* Made separate builds for tests on Travis CI. + + +---- + +Version 2.4.2 +============= + +:Released: 2016-05-16 +:Maintainer: Kostiantyn Rybnikov + +Changes +------- + +* Migrated README document to reStructuredText format. + +* Used Setuptools for distribution management. + +* Migrated test cases to Py.test. + +* Added configuration for Tox test runner. + + +---- + +Version 2.4.1 +============= + +:Released: 2016-03-04 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* :gh:`23`: Compared build component of a version. + + +---- + +Version 2.4.0 +============= + +:Released: 2016-02-12 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* :gh:`21`: Compared alphanumeric components correctly. + + +---- + +Version 2.3.1 +============= + +:Released: 2016-01-30 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Declared granted license name in distribution metadata. + + +---- + +Version 2.3.0 +============= + +:Released: 2016-01-29 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Added functions to increment prerelease and build components in a + version. + + +---- + +Version 2.2.1 +============= + +:Released: 2015-08-04 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Corrected comparison when any component includes zero. + + +---- + +Version 2.2.0 +============= + +:Released: 2015-06-21 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Add functions to determined minimum and maximum version. + +* Add code examples for recently-added functions. + + +---- + +Version 2.1.2 +============= + +:Released: 2015-05-23 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Restored current README document to distribution manifest. + + +---- + +Version 2.1.1 +============= + +:Released: 2015-05-23 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Removed absent document from distribution manifest. + + +---- + +Version 2.1.0 +============= + +:Released: 2015-05-22 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Documented installation instructions. + +* Documented project home page. + +* Added function to format a version string from components. + +* Added functions to increment specific components in a version. + +Changes +------- + +* Migrated README document to Markdown format. + +Bug Fixes +--------- + +* Corrected code examples in README document. + + +---- + +Version 2.0.2 +============= + +:Released: 2015-04-14 +:Maintainer: Konstantine Rybnikov + +Additions +--------- + +* Added configuration for Travis continuous integration. + +* Explicitly declared supported Python versions. + + +---- + +Version 2.0.1 +============= + +:Released: 2014-09-24 +:Maintainer: Konstantine Rybnikov + +Bug Fixes +--------- + +* :gh:`9`: Fixed comparison of equal version strings. + + +---- + +Version 2.0.0 +============= + +:Released: 2014-05-24 +:Maintainer: Konstantine Rybnikov + +Additions +--------- + +* Grant license in this code base under BSD 3-clause license terms. + +Changes +------- + +* Update parser to SemVer standard 2.0.0. + +* Ignore build component for comparison. + + +---- + +Version 0.0.2 +============= + +:Released: 2012-05-10 +:Maintainer: Konstantine Rybnikov + +Changes +------- + +* Use standard library Distutils for distribution management. + + +---- + +Version 0.0.1 +============= + +:Released: 2012-04-28 +:Maintainer: Konstantine Rybnikov + +* Initial release. + + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + End: + vim: fileencoding=utf-8 filetype=rst : diff --git a/docs/coerce.py b/docs/coerce.py index 3e5eb21b..7da20315 100644 --- a/docs/coerce.py +++ b/docs/coerce.py @@ -1,5 +1,7 @@ import re -import semver +from semver import Version +from typing import Optional, Tuple + BASEVERSION = re.compile( r"""[vV]? @@ -15,9 +17,9 @@ ) -def coerce(version): +def coerce(version: str) -> Tuple[Version, Optional[str]]: """ - Convert an incomplete version string into a semver-compatible VersionInfo + Convert an incomplete version string into a semver-compatible Version object * Tries to detect a "basic" version string (``major.minor.patch``). @@ -25,10 +27,10 @@ def coerce(version): set to zero to obtain a valid semver version. :param str version: the version string to convert - :return: a tuple with a :class:`VersionInfo` instance (or ``None`` + :return: a tuple with a :class:`Version` instance (or ``None`` if it's not a version) and the rest of the string which doesn't belong to a basic version. - :rtype: tuple(:class:`VersionInfo` | None, str) + :rtype: tuple(:class:`Version` | None, str) """ match = BASEVERSION.search(version) if not match: @@ -37,6 +39,6 @@ def coerce(version): ver = { key: 0 if value is None else value for key, value in match.groupdict().items() } - ver = semver.VersionInfo(**ver) + ver = Version(**ver) rest = match.string[match.end() :] # noqa:E203 return ver, rest diff --git a/docs/conf.py b/docs/conf.py index 3653ecce..f5e04b19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,12 +16,35 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # +import codecs import os +import re import sys -sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("../src/")) +# from semver import __version__ # noqa: E402 -from semver import __version__ # noqa: E402 + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: + return f.read() + + +def find_version(*file_paths): + """ + Build a path from *file_paths* and search for a ``__version__`` + string inside. + """ + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") # -- General configuration ------------------------------------------------ @@ -35,12 +58,16 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.autosummary", "sphinx_autodoc_typehints", "sphinx.ext.intersphinx", - "sphinx.ext.napoleon", "sphinx.ext.extlinks", ] +autoclass_content = "class" +autodoc_default_options = {} + + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -62,16 +89,16 @@ # built documents. # # The short X.Y version. -version = __version__ +release = find_version("../src/semver/__about__.py") # The full version, including alpha/beta/rc tags. -release = version +version = release # .rsplit(u".", 1)[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/index.rst b/docs/index.rst index aefdc843..405d9e27 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Semver |version| -- Semantic Versioning install usage + migratetosemver3 development api @@ -28,7 +29,7 @@ Semver |version| -- Semantic Versioning :hidden: changelog - changelog-2.7.9-and-before + changelog-semver2 Indices and Tables ================== diff --git a/docs/migratetosemver3.rst b/docs/migratetosemver3.rst new file mode 100644 index 00000000..d6d90954 --- /dev/null +++ b/docs/migratetosemver3.rst @@ -0,0 +1,42 @@ +.. _semver2-to-3: + +Migrating from semver2 to semver3 +================================= + +This chapter describes the visible differences for +users and how your code stays compatible for semver3. + +Although the development team tries to make the transition +to semver3 as smooth as possible, at some point change +is inevitable. + +For a more detailed overview of all the changes, refer +to our :ref:`changelog`. + + +Use Version instead of VersionInfo +---------------------------------- + +The :class:`VersionInfo` has been renamed to :class:`Version` +to have a more succinct name. +An alias has been created to preserve compatibility but +using old name has been deprecated. + +If you still need the old version, use this line: + +.. code-block:: python + + from semver.version import Version as VersionInfo + + + +Use semver.cli instead of semver +-------------------------------- + +All functions related to CLI parsing are moved to :mod:`semver.cli`. +If you are such functions, like :func:`semver.cmd_bump `, +import it from :mod:`semver.cli` in the future: + +.. code-block:: python + + from semver.cli import cmd_bump \ No newline at end of file diff --git a/docs/readme.rst b/docs/readme.rst index 0aa732a1..034e9ee6 100644 --- a/docs/readme.rst +++ b/docs/readme.rst @@ -1,2 +1,4 @@ -.. include:: ../README.rst +If you are searching for how to stay compatible +with semver3, refer to :ref:`semver2-to-3`. +.. include:: ../README.rst diff --git a/docs/semverwithvprefix.py b/docs/semverwithvprefix.py index 13298d5f..304ce772 100644 --- a/docs/semverwithvprefix.py +++ b/docs/semverwithvprefix.py @@ -1,15 +1,15 @@ -from semver import VersionInfo +from semver import Version -class SemVerWithVPrefix(VersionInfo): +class SemVerWithVPrefix(Version): """ - A subclass of VersionInfo which allows a "v" prefix + A subclass of Version which allows a "v" prefix """ @classmethod def parse(cls, version): """ - Parse version string to a VersionInfo instance. + Parse version string to a Version instance. :param version: version string with "v" or "V" prefix :type version: str diff --git a/docs/usage.rst b/docs/usage.rst index 4e2b6f92..94a115a8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,7 +1,7 @@ Using semver ============ -The ``semver`` module can store a version in the :class:`semver.VersionInfo` class. +The ``semver`` module can store a version in the :class:`semver.Version` class. For historical reasons, a version can be also stored as a string or dictionary. Each type can be converted into the other, if the minimum requirements @@ -26,7 +26,7 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-dev.1' + '3.0.0-dev.2' Creating a Version @@ -35,7 +35,7 @@ Creating a Version Due to historical reasons, the semver project offers two ways of creating a version: -* through an object oriented approach with the :class:`semver.VersionInfo` +* through an object oriented approach with the :class:`semver.Version` class. This is the preferred method when using semver. * through module level functions and builtin datatypes (usually string @@ -52,30 +52,46 @@ creating a version: :ref:`sec_display_deprecation_warnings`. -A :class:`semver.VersionInfo` instance can be created in different ways: +A :class:`semver.Version` instance can be created in different ways: * From a string (a Unicode string in Python 2):: - >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> semver.VersionInfo.parse(u"5.3.1") - VersionInfo(major=5, minor=3, patch=1, prerelease=None, build=None) + >>> semver.Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> semver.Version.parse(u"5.3.1") + Version(major=5, minor=3, patch=1, prerelease=None, build=None) * From a byte string:: - >>> semver.VersionInfo.parse(b"2.3.4") - VersionInfo(major=2, minor=3, patch=4, prerelease=None, build=None) + >>> semver.Version.parse(b"2.3.4") + Version(major=2, minor=3, patch=4, prerelease=None, build=None) * From individual parts by a dictionary:: >>> 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') + >>> semver.Version(**d) + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') Keep in mind, the ``major``, ``minor``, ``patch`` parts has to - be positive. + be positive. - >>> semver.VersionInfo(-1) + >>> semver.Version(-1) + Traceback (most recent call last): + ... + ValueError: 'major' is negative. A version can only be positive. + + As a minimum requirement, your dictionary needs at least the + be positive. + + >>> semver.Version(-1) + Traceback (most recent call last): + ... + ValueError: 'major' is negative. A version can only be positive. + + As a minimum requirement, your dictionary needs at least the + be positive. + + >>> semver.Version(-1) Traceback (most recent call last): ... ValueError: 'major' is negative. A version can only be positive. @@ -89,20 +105,20 @@ A :class:`semver.VersionInfo` instance can be created in different ways: * From a tuple:: >>> t = (3, 5, 6) - >>> semver.VersionInfo(*t) - VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + >>> semver.Version(*t) + Version(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", 6) - VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + >>> semver.Version("3", "5", 6) + Version(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. +the :class:`semver.Version` class anymore. Depending on your use case, the following methods are available: @@ -137,13 +153,13 @@ Parsing a Version String * With :func:`semver.parse_version_info`:: >>> semver.parse_version_info("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') -* With :func:`semver.VersionInfo.parse` (basically the same as +* With :func:`semver.Version.parse` (basically the same as :func:`semver.parse_version_info`):: - >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> semver.Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') * With :func:`semver.parse`:: @@ -155,13 +171,13 @@ Checking for a Valid Semver Version ----------------------------------- If you need to check a string if it is a valid semver version, use the -classmethod :func:`semver.VersionInfo.isvalid`: +classmethod :func:`semver.Version.isvalid`: .. code-block:: python - >>> semver.VersionInfo.isvalid("1.0.0") + >>> semver.Version.isvalid("1.0.0") True - >>> semver.VersionInfo.isvalid("invalid") + >>> semver.Version.isvalid("invalid") False @@ -170,12 +186,12 @@ classmethod :func:`semver.VersionInfo.isvalid`: Accessing Parts of a Version Through Names ------------------------------------------ -The :class:`semver.VersionInfo` contains attributes to access the different +The :class:`semver.Version` contains attributes to access the different parts of a version: .. code-block:: python - >>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4") + >>> v = semver.Version.parse("3.4.5-pre.2+build.4") >>> v.major 3 >>> v.minor @@ -197,16 +213,16 @@ If you do, you get an ``AttributeError``:: If you need to replace different parts of a version, refer to section :ref:`sec.replace.parts`. -In case you need the different parts of a version stepwise, iterate over the :class:`semver.VersionInfo` instance:: +In case you need the different parts of a version stepwise, iterate over the :class:`semver.Version` instance:: - >>> for item in semver.VersionInfo.parse("3.4.5-pre.2+build.4"): + >>> for item in semver.Version.parse("3.4.5-pre.2+build.4"): ... print(item) 3 4 5 pre.2 build.4 - >>> list(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) + >>> list(semver.Version.parse("3.4.5-pre.2+build.4")) [3, 4, 5, 'pre.2', 'build.4'] @@ -218,15 +234,15 @@ Accessing Parts Through Index Numbers .. versionadded:: 2.10.0 Another way to access parts of a version is to use an index notation. The underlying -:class:`VersionInfo ` object allows to access its data through -the magic method :func:`__getitem__ `. +:class:`Version ` object allows to access its data through +the magic method :func:`__getitem__ `. For example, the ``major`` part can be accessed by index number 0 (zero). Likewise the other parts: .. code-block:: python - >>> ver = semver.VersionInfo.parse("10.3.2-pre.5+build.10") + >>> ver = semver.Version.parse("10.3.2-pre.5+build.10") >>> ver[0], ver[1], ver[2], ver[3], ver[4] (10, 3, 2, 'pre.5', 'build.10') @@ -249,7 +265,7 @@ Negative numbers or undefined parts raise an :class:`IndexError` exception: .. code-block:: python - >>> ver = semver.VersionInfo.parse("10.3.2") + >>> ver = semver.Version.parse("10.3.2") >>> ver[3] Traceback (most recent call last): ... @@ -265,13 +281,13 @@ Replacing Parts of a Version ---------------------------- If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`semver.VersionInfo.replace` or :func:`semver.replace`: +unmodified, use the function :func:`semver.Version.replace` or :func:`semver.replace`: -* From a :class:`semver.VersionInfo` instance:: +* From a :class:`semver.Version` instance:: - >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(major=2, minor=2) - VersionInfo(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') + Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') * From a version string:: @@ -284,7 +300,7 @@ If you pass invalid keys you get an exception:: Traceback (most recent call last): ... TypeError: replace() got 1 unexpected keyword argument(s): invalidkey - >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(invalidkey=2) Traceback (most recent call last): ... @@ -293,28 +309,28 @@ If you pass invalid keys you get an exception:: .. _sec.convert.versions: -Converting a VersionInfo instance into Different Types +Converting a Version instance into Different Types ------------------------------------------------------ -Sometimes it is needed to convert a :class:`semver.VersionInfo` instance into +Sometimes it is needed to convert a :class:`semver.Version` instance into a different type. For example, for displaying or to access all parts. -It is possible to convert a :class:`semver.VersionInfo` instance: +It is possible to convert a :class:`semver.Version` instance: * Into a string with the builtin function :func:`str`:: - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* Into a dictionary with :func:`semver.VersionInfo.to_dict`:: +* Into a dictionary with :func:`semver.Version.to_dict`:: - >>> v = semver.VersionInfo(major=3, minor=4, patch=5) + >>> v = semver.Version(major=3, minor=4, patch=5) >>> v.to_dict() OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* Into a tuple with :func:`semver.VersionInfo.to_tuple`:: +* Into a tuple with :func:`semver.Version.to_tuple`:: - >>> v = semver.VersionInfo(major=5, minor=4, patch=2) + >>> v = semver.Version(major=5, minor=4, patch=2) >>> v.to_tuple() (5, 4, 2, None, None) @@ -325,27 +341,27 @@ Raising Parts of a Version The ``semver`` module contains the following functions to raise parts of a version: -* :func:`semver.VersionInfo.bump_major`: raises the major part and set all other parts to +* :func:`semver.Version.bump_major`: raises the major part and set all other parts to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_minor`: raises the minor part and sets ``patch`` to zero. +* :func:`semver.Version.bump_minor`: raises the minor part and sets ``patch`` to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_patch`: raises the patch part. Set ``prerelease`` and +* :func:`semver.Version.bump_patch`: raises the patch part. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_prerelease`: raises the prerelease part and set +* :func:`semver.Version.bump_prerelease`: raises the prerelease part and set ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_build`: raises the build part. +* :func:`semver.Version.bump_build`: raises the build part. .. code-block:: python - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_major()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_major()) '4.0.0' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_minor()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_minor()) '3.5.0' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_patch()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_patch()) '3.4.6' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_prerelease()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) '3.4.5-pre.3' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_build()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_build()) '3.4.5-pre.2+build.5' Likewise the module level functions :func:`semver.bump_major`. @@ -355,23 +371,23 @@ Increasing Parts of a Version Taking into Account Prereleases ------------------------------------------------------------- .. versionadded:: 2.10.0 - Added :func:`semver.VersionInfo.next_version`. + Added :func:`semver.Version.next_version`. If you want to raise your version and take prereleases into account, -the function :func:`semver.VersionInfo.next_version` would perhaps a +the function :func:`semver.Version.next_version` would perhaps a better fit. .. code-block:: python - >>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4") + >>> v = semver.Version.parse("3.4.5-pre.2+build.4") >>> str(v.next_version(part="prerelease")) '3.4.5-pre.3' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.VersionInfo.parse("3.4.5+build.4").next_version(part="patch")) + >>> str(semver.Version.parse("3.4.5+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease")) + >>> str(semver.Version.parse("0.1.4").next_version("prerelease")) '0.1.5-rc.1' @@ -394,23 +410,23 @@ To compare two versions depends on your type: The return value is negative if ``version1 < version2``, zero if ``version1 == version2`` and strictly positive if ``version1 > version2``. -* **Two** :class:`semver.VersionInfo` **instances** +* **Two** :class:`semver.Version` **instances** Use the specific operator. Currently, the operators ``<``, ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - >>> v1 = semver.VersionInfo.parse("3.4.5") - >>> v2 = semver.VersionInfo.parse("3.5.1") + >>> v1 = semver.Version.parse("3.4.5") + >>> v2 = semver.Version.parse("3.5.1") >>> v1 < v2 True >>> v1 > v2 False -* **A** :class:`semver.VersionInfo` **type and a** :func:`tuple` **or** :func:`list` +* **A** :class:`semver.Version` **type and a** :func:`tuple` **or** :func:`list` - Use the operator as with two :class:`semver.VersionInfo` types:: + Use the operator as with two :class:`semver.Version` types:: - >>> v = semver.VersionInfo.parse("3.4.5") + >>> v = semver.Version.parse("3.4.5") >>> v > (1, 0) True >>> v < [3, 5] @@ -423,7 +439,7 @@ To compare two versions depends on your type: >>> [3, 5] > v True -* **A** :class:`semver.VersionInfo` **type and a** :func:`str` +* **A** :class:`semver.Version` **type and a** :func:`str` You can use also raw strings to compare:: @@ -446,7 +462,7 @@ To compare two versions depends on your type: ... ValueError: 1.0 is not valid SemVer string -* **A** :class:`semver.VersionInfo` **type and a** :func:`dict` +* **A** :class:`semver.Version` **type and a** :func:`dict` You can also use a dictionary. In contrast to strings, you can have an "incomplete" version (as the other parts are set to zero):: @@ -483,16 +499,16 @@ Version equality means for semver, that major, minor, patch, and prerelease parts are equal in both versions you compare. The build part is ignored. For example:: - >>> v = semver.VersionInfo.parse("1.2.3-rc4+1e4664d") + >>> v = semver.Version.parse("1.2.3-rc4+1e4664d") >>> v == "1.2.3-rc4+dedbeef" True -This also applies when a :class:`semver.VersionInfo` is a member of a set, or a +This also applies when a :class:`semver.Version` is a member of a set, or a dictionary key:: >>> d = {} - >>> v1 = semver.VersionInfo.parse("1.2.3-rc4+1e4664d") - >>> v2 = semver.VersionInfo.parse("1.2.3-rc4+dedbeef") + >>> v1 = semver.Version.parse("1.2.3-rc4+1e4664d") + >>> v2 = semver.Version.parse("1.2.3-rc4+dedbeef") >>> d[v1] = 1 >>> d[v2] 1 @@ -538,34 +554,34 @@ Getting Minimum and Maximum of Multiple Versions The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in favor of their builtin counterparts :func:`max` and :func:`min`. -Since :class:`semver.VersionInfo` implements :func:`__gt__()` and :func:`__lt__()`, it can be used with builtins requiring +Since :class:`semver.Version` implements :func:`__gt__()` and :func:`__lt__()`, it can be used with builtins requiring .. code-block:: python - >>> max([semver.VersionInfo(0, 1, 0), semver.VersionInfo(0, 2, 0), semver.VersionInfo(0, 1, 3)]) - VersionInfo(major=0, minor=2, patch=0, prerelease=None, build=None) - >>> min([semver.VersionInfo(0, 1, 0), semver.VersionInfo(0, 2, 0), semver.VersionInfo(0, 1, 3)]) - VersionInfo(major=0, minor=1, patch=0, prerelease=None, build=None) + >>> max([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + Version(major=0, minor=2, patch=0, prerelease=None, build=None) + >>> min([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + Version(major=0, minor=1, patch=0, prerelease=None, build=None) Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type -(convertible to :class:`semver.VersionInfo`). +(convertible to :class:`semver.Version`). For example, here are the maximum and minimum versions of a list of version strings: .. code-block:: python - >>> str(max(map(semver.VersionInfo.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> str(max(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) '2.1.0' - >>> str(min(map(semver.VersionInfo.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> str(min(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) '0.4.99' And the same can be done with tuples: .. code-block:: python - >>> max(map(lambda v: semver.VersionInfo(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> max(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (2, 1, 0, None, None) - >>> min(map(lambda v: semver.VersionInfo(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> min(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (0, 4, 99, None, None) For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. @@ -600,7 +616,7 @@ information and returns a tuple with two items: :language: python -The function returns a *tuple*, containing a :class:`VersionInfo` +The function returns a *tuple*, containing a :class:`Version` instance or None as the first element and the rest as the second element. The second element (the rest) can be used to make further adjustments. @@ -609,9 +625,9 @@ For example: .. code-block:: python >>> coerce("v1.2") - (VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '') + (Version(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') + (Version(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') .. _sec_replace_deprecated_functions: @@ -622,7 +638,7 @@ Replacing Deprecated Functions .. versionchanged:: 2.10.0 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. + :class:`semver.Version` class. The deprecated functions can still be used in version 2.10.0 and above. In version 3 of semver, the deprecated functions will be removed. @@ -633,15 +649,15 @@ 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` + Replace them with the respective methods of the :class:`semver.Version` class. For example, the function :func:`semver.bump_major` is replaced by - :func:`semver.VersionInfo.bump_major` and calling the ``str(versionobject)``: + :func:`semver.Version.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()) + >>> s2 = str(semver.Version.parse("3.4.5").bump_major()) >>> s1 == s2 True @@ -649,12 +665,12 @@ them with code which is compatible for future versions: * :func:`semver.finalize_version` - Replace it with :func:`semver.VersionInfo.finalize_version`: + Replace it with :func:`semver.Version.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()) + >>> s2 = str(semver.Version.parse('1.2.3-rc.5').finalize_version()) >>> s1 == s2 True @@ -665,7 +681,7 @@ them with code which is compatible for future versions: .. 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')) + >>> s2 = str(semver.Version(5, 4, 3, 'pre.2', 'build.1')) >>> s1 == s2 True @@ -676,7 +692,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(semver.VersionInfo.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(max(map(semver.Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True @@ -687,41 +703,41 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(semver.VersionInfo.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(min(map(semver.Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True * :func:`semver.parse` - Replace it with :func:`semver.VersionInfo.parse` and - :func:`semver.VersionInfo.to_dict`: + Replace it with :func:`semver.Version.parse` and + :func:`semver.Version.to_dict`: .. code-block:: python >>> v1 = semver.parse("1.2.3") - >>> v2 = semver.VersionInfo.parse("1.2.3").to_dict() + >>> v2 = semver.Version.parse("1.2.3").to_dict() >>> v1 == v2 True * :func:`semver.parse_version_info` - Replace it with :func:`semver.VersionInfo.parse`: + Replace it with :func:`semver.Version.parse`: .. code-block:: python >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = semver.VersionInfo.parse("3.4.5") + >>> v2 = semver.Version.parse("3.4.5") >>> v1 == v2 True * :func:`semver.replace` - Replace it with :func:`semver.VersionInfo.replace`: + Replace it with :func:`semver.Version.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)) + >>> s2 = str(semver.Version.parse('1.2.3').replace(major=2, patch=10)) >>> s1 == s2 True @@ -764,12 +780,12 @@ the following methods: .. _sec_creating_subclasses_from_versioninfo: -Creating Subclasses from VersionInfo +Creating Subclasses from Version ------------------------------------ If you do not like creating functions to modify the behavior of semver (as shown in section :ref:`sec_dealing_with_invalid_versions`), you can -also create a subclass of the :class:`VersionInfo` class. +also create a subclass of the :class:`Version` class. For example, if you want to output a "v" prefix before a version, but the other behavior is the same, use the following code: diff --git a/pyproject.toml b/pyproject.toml index b3ee70a3..1b406dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] -include = '\.pyi?$' # diff = true exclude = ''' ( diff --git a/semver.py b/semver.py deleted file mode 100644 index 09bca561..00000000 --- a/semver.py +++ /dev/null @@ -1,1226 +0,0 @@ -"""Python helper for Semantic Versioning (http://semver.org)""" -from __future__ import print_function - -import argparse -import collections -import inspect -import re -import sys -import warnings -from functools import partial, wraps -from types import FrameType -from typing import ( - Any, - Callable, - Collection, - Dict, - Iterable, - Iterator, - List, - Optional, - SupportsInt, - Tuple, - TypeVar, - Union, - cast, -) - -__version__ = "3.0.0-dev.1" -__author__ = "Kostiantyn Rybnikov" -__author_email__ = "k-bx@k-bx.com" -__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] -__maintainer_email__ = "s.celles@gmail.com" -__description__ = "Python helper for Semantic Versioning (http://semver.org)" - -#: Our public interface -__all__ = ( - # - # Module level function: - "bump_build", - "bump_major", - "bump_minor", - "bump_patch", - "bump_prerelease", - "compare", - "deprecated", - "finalize_version", - "format_version", - "match", - "max_ver", - "min_ver", - "parse", - "parse_version_info", - "replace", - # - # CLI interface - "cmd_bump", - "cmd_check", - "cmd_compare", - "createparser", - "main", - "process", - # - # Constants and classes - "SEMVER_SPEC_VERSION", - "VersionInfo", -) - - -#: Contains the implemented semver.org version of the spec -SEMVER_SPEC_VERSION = "2.0.0" - - -# Types -VersionPart = Union[int, Optional[str]] -Comparable = Union["VersionInfo", Dict[str, VersionPart], Collection[VersionPart], str] -Comparator = Callable[["VersionInfo", Comparable], bool] -String = Union[str, bytes] -VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]] -VersionDict = Dict[str, VersionPart] -VersionIterator = Iterator[VersionPart] - - -def cmp(a, b): - """Return negative if ab.""" - return (a > b) - (a < b) - - -def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: - # Taken from six project - """ - Coerce *s* to `str`. - - * `str` -> `str` - * `bytes` -> decoded to `str` - - :param s: the string to convert - :type s: str | bytes - :param encoding: the encoding to apply, defaults to "utf-8" - :type encoding: str - :param errors: set a different error handling scheme, - defaults to "strict". - Other possible values are `ignore`, `replace`, and - `xmlcharrefreplace` as well as any other name - registered with :func:`codecs.register_error`. - :type errors: str - :raises TypeError: if ``s`` is not str or bytes type - :return: the converted string - :rtype: str - """ - if isinstance(s, bytes): - s = s.decode(encoding, errors) - elif not isinstance(s, String.__args__): # type: ignore - raise TypeError("not expecting type '%s'" % type(s)) - return s - - -F = TypeVar("F", bound=Callable) - - -def deprecated( - func: F = None, - replace: str = None, - version: str = None, - category=DeprecationWarning, -) -> Union[Callable[..., F], partial]: - """ - Decorates a function to output a deprecation warning. - - :param func: the function to decorate - :param replace: the function to replace (use the full qualified - name like ``semver.VersionInfo.bump_major``. - :param 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. - :return: decorated function which is marked as deprecated - """ - - if func is None: - return partial(deprecated, replace=replace, version=version, category=category) - - @wraps(func) - def wrapper(*args, **kwargs) -> Callable[..., F]: - msg_list = ["Function '{m}.{f}' is deprecated."] - - if version: - msg_list.append("Deprecated since version {v}. ") - msg_list.append("This function will be removed in semver 3.") - if replace: - msg_list.append("Use {r!r} instead.") - else: - msg_list.append("Use the respective 'semver.VersionInfo.{r}' instead.") - - f = cast(F, func).__qualname__ - r = replace or f - - frame = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back) - - msg = " ".join(msg_list) - 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) # type: ignore - - return wrapper - - -@deprecated(version="2.10.0") -def parse(version): - """ - Parse version to major, minor, patch, pre-release, build parts. - - .. deprecated:: 2.10.0 - 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 - if not provided - - >>> ver = semver.parse('3.4.5-pre.2+build.4') - >>> ver['major'] - 3 - >>> ver['minor'] - 4 - >>> ver['patch'] - 5 - >>> ver['prerelease'] - 'pre.2' - >>> ver['build'] - 'build.4' - """ - return VersionInfo.parse(version).to_dict() - - -def comparator(operator: Comparator) -> Comparator: - """Wrap a VersionInfo binary op method in a type-check.""" - - @wraps(operator) - def wrapper(self: "VersionInfo", other: Comparable) -> bool: - comparable_types = ( - VersionInfo, - dict, - tuple, - list, - *String.__args__, # type: ignore - ) - if not isinstance(other, comparable_types): - raise TypeError( - "other type %r must be in %r" % (type(other), comparable_types) - ) - return operator(self, other) - - return wrapper - - -class VersionInfo: - """ - A semver compatible version class. - - :param major: version when you make incompatible API changes. - :param minor: version when you add functionality in - a backwards-compatible manner. - :param patch: version when you make backwards-compatible bug fixes. - :param prerelease: an optional prerelease string - :param build: an optional build string - """ - - __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: SupportsInt, - minor: SupportsInt = 0, - patch: SupportsInt = 0, - prerelease: Union[String, int] = None, - build: Union[String, int] = None, - ): - # Build a dictionary of the arguments except prerelease and build - version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} - - for name, value in version_parts.items(): - if value < 0: - raise ValueError( - "{!r} is negative. A version can only be positive.".format(name) - ) - - self._major = version_parts["major"] - self._minor = version_parts["minor"] - self._patch = version_parts["patch"] - self._prerelease = None if prerelease is None else str(prerelease) - self._build = None if build is None else str(build) - - @property - def major(self) -> int: - """The major part of a version (read-only).""" - return self._major - - @major.setter - def major(self, value): - raise AttributeError("attribute 'major' is readonly") - - @property - def minor(self) -> int: - """The minor part of a version (read-only).""" - return self._minor - - @minor.setter - def minor(self, value): - raise AttributeError("attribute 'minor' is readonly") - - @property - def patch(self) -> int: - """The patch part of a version (read-only).""" - return self._patch - - @patch.setter - def patch(self, value): - raise AttributeError("attribute 'patch' is readonly") - - @property - def prerelease(self) -> Optional[str]: - """The prerelease part of a version (read-only).""" - return self._prerelease - - @prerelease.setter - def prerelease(self, value): - raise AttributeError("attribute 'prerelease' is readonly") - - @property - def build(self) -> Optional[str]: - """The build part of a version (read-only).""" - return self._build - - @build.setter - def build(self, value): - raise AttributeError("attribute 'build' is readonly") - - def to_tuple(self) -> VersionTuple: - """ - Convert the VersionInfo object to a tuple. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to - make this function available in the public API. - - :return: a tuple with all the parts - - >>> semver.VersionInfo(5, 3, 1).to_tuple() - (5, 3, 1, None, None) - """ - return (self.major, self.minor, self.patch, self.prerelease, self.build) - - def to_dict(self) -> VersionDict: - """ - Convert the VersionInfo object to an OrderedDict. - - .. versionadded:: 2.10.0 - 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``. - - >>> semver.VersionInfo(3, 2, 1).to_dict() - OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ -('prerelease', None), ('build', None)]) - """ - return collections.OrderedDict( - ( - ("major", self.major), - ("minor", self.minor), - ("patch", self.patch), - ("prerelease", self.prerelease), - ("build", self.build), - ) - ) - - def __iter__(self) -> VersionIterator: - """Implement iter(self).""" - yield from self.to_tuple() - - @staticmethod - def _increment_string(string: str) -> str: - """ - Look for the last sequence of number(s) in a string and increment. - - :param 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) -> "VersionInfo": - """ - Raise the major part of the version, return a new object but leave self - untouched. - - :return: new object with the raised major part - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver.bump_major() - VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major + 1) - - def bump_minor(self) -> "VersionInfo": - """ - Raise the minor part of the version, return a new object but leave self - untouched. - - :return: new object with the raised minor part - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver.bump_minor() - VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major, self._minor + 1) - - def bump_patch(self) -> "VersionInfo": - """ - Raise the patch part of the version, return a new object but leave self - untouched. - - :return: new object with the raised patch part - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver.bump_patch() - VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major, self._minor, self._patch + 1) - - def bump_prerelease(self, token: str = "rc") -> "VersionInfo": - """ - Raise the prerelease part of the version, return a new object but leave - self untouched. - - :param token: defaults to 'rc' - :return: new object with the raised prerelease part - - >>> ver = semver.VersionInfo.parse("3.4.5-rc.1") - >>> ver.bump_prerelease() - VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \ -build=None) - """ - 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: str = "build") -> "VersionInfo": - """ - Raise the build part of the version, return a new object but leave self - untouched. - - :param token: defaults to 'build' - :return: new object with the raised build part - - >>> ver = semver.VersionInfo.parse("3.4.5-rc.1+build.9") - >>> ver.bump_build() - VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \ -build='build.10') - """ - 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) - - def compare(self, other: Comparable) -> int: - """ - Compare self with other. - - :param other: the second version - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - - >>> semver.VersionInfo.parse("1.0.0").compare("2.0.0") - -1 - >>> semver.VersionInfo.parse("2.0.0").compare("1.0.0") - 1 - >>> semver.VersionInfo.parse("2.0.0").compare("2.0.0") - 0 - >>> semver.VersionInfo.parse("2.0.0").compare(dict(major=2, minor=0, patch=0)) - 0 - """ - cls = type(self) - if isinstance(other, String.__args__): # type: ignore - other = cls.parse(other) - elif isinstance(other, dict): - other = cls(**other) - elif isinstance(other, (tuple, list)): - other = cls(*other) - elif not isinstance(other, cls): - raise TypeError( - f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, " - f"but got {type(other)}" - ) - - v1 = self.to_tuple()[:3] - v2 = other.to_tuple()[:3] - x = cmp(v1, v2) - if x: - return x - - rc1, rc2 = self.prerelease, other.prerelease - rccmp = _nat_cmp(rc1, rc2) - - if not rccmp: - return 0 - if not rc1: - return 1 - elif not rc2: - return -1 - - return rccmp - - def next_version(self, part: str, prerelease_token: str = "rc") -> "VersionInfo": - """ - Determines next version, preserving natural order. - - .. versionadded:: 2.10.0 - - This function is taking prereleases into account. - The "major", "minor", and "patch" raises the respective parts like - the ``bump_*`` functions. The real difference is using the - "preprelease" part. It gives you the next patch version of the prerelease, - for example: - - >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease")) - '0.1.5-rc.1' - - :param part: One of "major", "minor", "patch", or "prerelease" - :param prerelease_token: prefix string of prerelease, defaults to 'rc' - :return: new object with the appropriate part raised - """ - validparts = { - "major", - "minor", - "patch", - "prerelease", - # "build", # currently not used - } - if part not in validparts: - raise ValueError( - "Invalid part. Expected one of {validparts}, but got {part!r}".format( - validparts=validparts, part=part - ) - ) - version = self - if (version.prerelease or version.build) and ( - part == "patch" - or (part == "minor" and version.patch == 0) - or (part == "major" and version.minor == version.patch == 0) - ): - return version.replace(prerelease=None, build=None) - - if part in ("major", "minor", "patch"): - return getattr(version, "bump_" + part)() - - if not version.prerelease: - version = version.bump_patch() - return version.bump_prerelease(prerelease_token) - - @comparator - def __eq__(self, other: Comparable) -> bool: # type: ignore - return self.compare(other) == 0 - - @comparator - def __ne__(self, other: Comparable) -> bool: # type: ignore - return self.compare(other) != 0 - - @comparator - def __lt__(self, other: Comparable) -> bool: - return self.compare(other) < 0 - - @comparator - def __le__(self, other: Comparable) -> bool: - return self.compare(other) <= 0 - - @comparator - def __gt__(self, other: Comparable) -> bool: - return self.compare(other) > 0 - - @comparator - def __ge__(self, other: Comparable) -> bool: - return self.compare(other) >= 0 - - def __getitem__( - self, index: Union[int, slice] - ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: - """ - self.__getitem__(index) <==> self[index] - - Implement getitem. If the part requested is undefined, or a part of the - range requested is undefined, it will throw an index error. - Negative indices are not supported - - :param Union[int, slice] index: a positive integer indicating the - offset or a :func:`slice` object - :raises IndexError: if index is beyond the range or a part is None - :return: the requested part of the version at position index - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver[0], ver[1], ver[2] - (3, 4, 5) - """ - if isinstance(index, int): - index = slice(index, index + 1) - index = cast(slice, index) - - if ( - isinstance(index, slice) - and (index.start is not None and index.start < 0) - or (index.stop is not None and index.stop < 0) - ): - raise IndexError("Version index cannot be negative") - - part = tuple( - filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index])) - ) - - if len(part) == 1: - return part[0] - elif not part: - raise IndexError("Version part undefined") - return part - - def __repr__(self) -> str: - s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) - return "%s(%s)" % (type(self).__name__, s) - - def __str__(self) -> str: - """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) -> int: - return hash(self.to_tuple()[:4]) - - def finalize_version(self) -> "VersionInfo": - """ - Remove any prerelease and build metadata from the version. - - :return: a new instance with the finalized version string - - >>> 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) - - def match(self, match_expr: str) -> bool: - """ - Compare self to match a match expression. - - :param match_expr: operator and version; valid operators are - < smaller than - > greater than - >= greator or equal than - <= smaller or equal than - == equal - != not equal - :return: True if the expression matches the version, otherwise False - - >>> semver.VersionInfo.parse("2.0.0").match(">=1.0.0") - True - >>> semver.VersionInfo.parse("1.0.0").match(">1.0.0") - False - """ - prefix = match_expr[:2] - if prefix in (">=", "<=", "==", "!="): - match_version = match_expr[2:] - elif prefix and prefix[0] in (">", "<"): - prefix = prefix[0] - match_version = match_expr[1:] - else: - raise ValueError( - "match_expr parameter should be in format , " - "where is one of " - "['<', '>', '==', '<=', '>=', '!=']. " - "You provided: %r" % match_expr - ) - - possibilities_dict = { - ">": (1,), - "<": (-1,), - "==": (0,), - "!=": (-1, 1), - ">=": (0, 1), - "<=": (-1, 0), - } - - possibilities = possibilities_dict[prefix] - cmp_res = self.compare(match_version) - - return cmp_res in possibilities - - @classmethod - def parse(cls, version: String) -> "VersionInfo": - """ - Parse version string to a VersionInfo instance. - - .. versionchanged:: 2.11.0 - Changed method from static to classmethod to - allow subclasses. - - :param version: version string - :return: a :class:`VersionInfo` instance - :raises ValueError: if version is invalid - - >>> semver.VersionInfo.parse('3.4.5-pre.2+build.4') - VersionInfo(major=3, minor=4, patch=5, \ -prerelease='pre.2', build='build.4') - """ - version_str = ensure_str(version) - match = cls._REGEX.match(version_str) - if match is None: - raise ValueError(f"{version_str} is not valid SemVer string") - - matched_version_parts: Dict[str, Any] = match.groupdict() - - return cls(**matched_version_parts) - - def replace(self, **parts: Union[int, Optional[str]]) -> "VersionInfo": - """ - Replace one or more parts of a version and return a new - :class:`VersionInfo` object, but leave self untouched - - .. versionadded:: 2.9.0 - Added :func:`VersionInfo.replace` - - :param parts: the parts to be updated. Valid keys are: - ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` - :return: the new :class:`VersionInfo` object with the changed - parts - :raises TypeError: if ``parts`` contains invalid keys - """ - version = self.to_dict() - version.update(parts) - try: - return VersionInfo(**version) # type: ignore - except TypeError: - unknownkeys = set(parts) - set(self.to_dict()) - error = "replace() got %d unexpected keyword " "argument(s): %s" % ( - len(unknownkeys), - ", ".join(unknownkeys), - ) - raise TypeError(error) - - @classmethod - def isvalid(cls, version: str) -> bool: - """ - Check if the string is a valid semver version. - - .. versionadded:: 2.9.1 - - :param version: the version string to check - :return: True if the version string is a valid semver version, False - otherwise. - """ - try: - cls.parse(version) - return True - except ValueError: - return False - - -@deprecated(replace="semver.VersionInfo.parse", version="2.10.0") -def parse_version_info(version): - """ - Parse version string to a VersionInfo instance. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.parse` instead. - - .. versionadded:: 2.7.2 - Added :func:`semver.parse_version_info` - - :param version: version string - :return: a :class:`VersionInfo` instance - - >>> version_info = semver.VersionInfo.parse("3.4.5-pre.2+build.4") - >>> version_info.major - 3 - >>> version_info.minor - 4 - >>> version_info.patch - 5 - >>> version_info.prerelease - 'pre.2' - >>> version_info.build - 'build.4' - """ - return VersionInfo.parse(version) - - -def _nat_cmp(a: Optional[str], b: Optional[str]) -> int: - def convert(text: str) -> Union[int, str]: - return int(text) if re.match("^[0-9]+$", text) else text # type: ignore - - def split_key(key: str) -> List[Union[int, str]]: - return [convert(c) for c in key.split(".")] - - def cmp_prerelease_tag(a: Union[int, str], b: Union[int, str]) -> int: - if isinstance(a, int) and isinstance(b, int): - return cmp(a, b) - elif isinstance(a, int): - return -1 - elif isinstance(b, int): - return 1 - else: - return cmp(a, b) - - a, b = a or "", b or "" - a_parts, b_parts = split_key(a), split_key(b) - for sub_a, sub_b in zip(a_parts, b_parts): - cmp_result = cmp_prerelease_tag(sub_a, sub_b) - if cmp_result != 0: - return cmp_result - else: - return cmp(len(a), len(b)) - - -@deprecated(version="2.10.0") -def compare(ver1, ver2): - """ - Compare two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 - """ - v1 = VersionInfo.parse(ver1) - return v1.compare(ver2) - - -@deprecated(version="2.10.0") -def match(version, match_expr): - """ - Compare two versions strings through a comparison. - - :param version: a version string - :param match_expr: operator and version; valid operators are - < smaller than - > greater than - >= greator or equal than - <= smaller or equal than - == equal - != not equal - :return: True if the expression matches the version, otherwise False - - >>> semver.match("2.0.0", ">=1.0.0") - True - >>> semver.match("1.0.0", ">1.0.0") - False - """ - ver = VersionInfo.parse(version) - return ver.match(match_expr) - - -@deprecated(replace="max", version="2.10.2") -def max_ver(ver1, ver2): - """ - Returns the greater version of two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: the greater version of the two - - >>> semver.max_ver("1.0.0", "2.0.0") - '2.0.0' - """ - if isinstance(ver1, String.__args__): - ver1 = VersionInfo.parse(ver1) - elif not isinstance(ver1, VersionInfo): - raise TypeError() - cmp_res = ver1.compare(ver2) - if cmp_res >= 0: - return str(ver1) - else: - return ver2 - - -@deprecated(replace="min", version="2.10.2") -def min_ver(ver1, ver2): - """ - Returns the smaller version of two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: the smaller version of the two - - >>> semver.min_ver("1.0.0", "2.0.0") - '1.0.0' - """ - ver1 = VersionInfo.parse(ver1) - cmp_res = ver1.compare(ver2) - if cmp_res <= 0: - return str(ver1) - else: - return ver2 - - -@deprecated(replace="str(versionobject)", version="2.10.0") -def format_version(major, minor, patch, prerelease=None, build=None): - """ - Format a version string according to the Semantic Versioning specification. - - .. deprecated:: 2.10.0 - Use ``str(VersionInfo(VERSION)`` instead. - - :param major: the required major part of a version - :param minor: the required minor part of a version - :param patch: the required patch part of a version - :param prerelease: the optional prerelease part of a version - :param build: the optional build part of a version - :return: the formatted string - - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - """ - return str(VersionInfo(major, minor, patch, prerelease, build)) - - -@deprecated(version="2.10.0") -def bump_major(version): - """ - Raise the major part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_major` instead. - - :param: version string - :return: the raised version string - - >>> semver.bump_major("3.4.5") - '4.0.0' - """ - return str(VersionInfo.parse(version).bump_major()) - - -@deprecated(version="2.10.0") -def bump_minor(version): - """ - Raise the minor part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_minor` instead. - - :param: version string - :return: the raised version string - - >>> semver.bump_minor("3.4.5") - '3.5.0' - """ - return str(VersionInfo.parse(version).bump_minor()) - - -@deprecated(version="2.10.0") -def bump_patch(version): - """ - Raise the patch part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_patch` instead. - - :param: version string - :return: the raised version string - - >>> semver.bump_patch("3.4.5") - '3.4.6' - """ - return str(VersionInfo.parse(version).bump_patch()) - - -@deprecated(version="2.10.0") -def bump_prerelease(version, token="rc"): - """ - Raise the prerelease part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_prerelease` instead. - - :param version: version string - :param token: defaults to 'rc' - :return: the raised version string - - >>> semver.bump_prerelease('3.4.5', 'dev') - '3.4.5-dev.1' - """ - return str(VersionInfo.parse(version).bump_prerelease(token)) - - -@deprecated(version="2.10.0") -def bump_build(version, token="build"): - """ - Raise the build part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_build` instead. - - :param version: version string - :param token: defaults to 'build' - :return: the raised version string - - >>> semver.bump_build('3.4.5-rc.1+build.9') - '3.4.5-rc.1+build.10' - """ - return str(VersionInfo.parse(version).bump_build(token)) - - -@deprecated(version="2.10.0") -def finalize_version(version): - """ - Remove any prerelease and build metadata from the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.finalize_version` instead. - - .. versionadded:: 2.7.9 - Added :func:`finalize_version` - - :param version: version string - :return: the finalized version string - - >>> semver.finalize_version('1.2.3-rc.5') - '1.2.3' - """ - verinfo = VersionInfo.parse(version) - return str(verinfo.finalize_version()) - - -@deprecated(version="2.10.0") -def replace(version, **parts): - """ - Replace one or more parts of a version and return the new string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.replace` instead. - - .. versionadded:: 2.9.0 - Added :func:`replace` - - :param version: the version string to replace - :param 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 - - >>> import semver - >>> semver.replace("1.2.3", major=2, patch=10) - '2.2.10' - """ - return str(VersionInfo.parse(version).replace(**parts)) - - -# ---- CLI -def cmd_bump(args: argparse.Namespace) -> str: - """ - Subcommand: Bumps a version. - - Synopsis: bump - can be major, minor, patch, prerelease, or build - - :param args: The parsed arguments - :return: the new, bumped version - """ - maptable = { - "major": "bump_major", - "minor": "bump_minor", - "patch": "bump_patch", - "prerelease": "bump_prerelease", - "build": "bump_build", - } - if args.bump is None: - # When bump is called without arguments, - # print the help and exit - args.parser.parse_args(["bump", "-h"]) - - ver = VersionInfo.parse(args.version) - # get the respective method and call it - func = getattr(ver, maptable[cast(str, args.bump)]) - return str(func()) - - -def cmd_check(args: argparse.Namespace) -> None: - """ - Subcommand: Checks if a string is a valid semver version. - - Synopsis: check - - :param args: The parsed arguments - """ - if VersionInfo.isvalid(args.version): - return None - raise ValueError("Invalid version %r" % args.version) - - -def cmd_compare(args: argparse.Namespace) -> str: - """ - Subcommand: Compare two versions - - Synopsis: compare - - :param args: The parsed arguments - """ - return str(compare(args.version1, args.version2)) - - -def cmd_nextver(args: argparse.Namespace) -> str: - """ - Subcommand: Determines the next version, taking prereleases into account. - - Synopsis: nextver - - :param args: The parsed arguments - """ - version = VersionInfo.parse(args.version) - return str(version.next_version(args.part)) - - -def createparser() -> argparse.ArgumentParser: - """ - Create an :class:`argparse.ArgumentParser` instance. - - :return: parser instance - """ - parser = argparse.ArgumentParser(prog=__package__, description=__doc__) - - parser.add_argument( - "--version", action="version", version="%(prog)s " + __version__ - ) - - s = parser.add_subparsers() - # create compare subcommand - parser_compare = s.add_parser("compare", help="Compare two versions") - parser_compare.set_defaults(func=cmd_compare) - parser_compare.add_argument("version1", help="First version") - parser_compare.add_argument("version2", help="Second version") - - # create bump subcommand - parser_bump = s.add_parser("bump", help="Bumps a version") - parser_bump.set_defaults(func=cmd_bump) - sb = parser_bump.add_subparsers(title="Bump commands", dest="bump") - - # Create subparsers for the bump subparser: - for p in ( - sb.add_parser("major", help="Bump the major part of the version"), - sb.add_parser("minor", help="Bump the minor part of the version"), - sb.add_parser("patch", help="Bump the patch part of the version"), - sb.add_parser("prerelease", help="Bump the prerelease part of the version"), - sb.add_parser("build", help="Bump the build part of the version"), - ): - p.add_argument("version", help="Version to raise") - - # Create the check subcommand - parser_check = s.add_parser( - "check", help="Checks if a string is a valid semver version" - ) - parser_check.set_defaults(func=cmd_check) - parser_check.add_argument("version", help="Version to check") - - # Create the nextver subcommand - parser_nextver = s.add_parser( - "nextver", help="Determines the next version, taking prereleases into account." - ) - parser_nextver.set_defaults(func=cmd_nextver) - parser_nextver.add_argument("version", help="Version to raise") - parser_nextver.add_argument( - "part", help="One of 'major', 'minor', 'patch', or 'prerelease'" - ) - return parser - - -def process(args: argparse.Namespace) -> str: - """ - Process the input from the CLI. - - :param args: The parsed arguments - :param parser: the parser instance - :return: result of the selected action - """ - if not hasattr(args, "func"): - args.parser.print_help() - raise SystemExit() - - # Call the respective function object: - return args.func(args) - - -def main(cliargs: List[str] = None) -> int: - """ - Entry point for the application script. - - :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) - :return: error code - """ - try: - parser = createparser() - args = parser.parse_args(args=cliargs) - # Save parser instance: - args.parser = parser - result = process(args) - if result is not None: - print(result) - return 0 - - except (ValueError, TypeError) as err: - print("ERROR", err, file=sys.stderr) - return 2 - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/setup.cfg b/setup.cfg index 5abd4bbb..873be36d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,55 @@ +# +# Metadata for setup.py +# +# See https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html + +[metadata] +name = semver +version = attr: semver.__about__.__version__ +description = attr: semver.__about__.__description__ +long_description = file: README.rst +long_description_content_type = text/x-rst +author = attr: semver.__about__.__author__ +author_email = attr: semver.__about__.__author_email__ +maintainer = attr: semver.__about__.__maintainer__ +maintainer_email = attr: semver.__about__.__maintainer_email__ +url = https://github.com/python-semver/python-semver +download_url = https://github.com/python-semver/python-semver/downloads +project_urls = + Documentation = https://python-semver.rtfd.io + Releases = https://github.com/python-semver/python-semver/releases + Bug Tracker = https://github.com/python-semver/python-semver/issues +classifiers = + Environment :: Web Environment + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Topic :: Software Development :: Libraries :: Python Modules +license = BSD + +[options] +package_dir = + =src +packages = find: +python_requires = >=3.6.* +include_package_data = True + +[options.entry_points] +console_scripts = + pysemver = semver.cli:main + +[options.packages.find] +where = src + +[options.package_data] +semver = py.typed + [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs @@ -15,13 +67,14 @@ addopts = max-line-length = 88 ignore = F821,W503 exclude = - .env, - venv, - .eggs, - .tox, - .git, - __pycache__, - build, + src/semver/__init__.py + .env + venv + .eggs + .tox + .git + __pycache__ + build dist docs conftest.py @@ -32,6 +85,7 @@ count = False max-line-length = 88 statistics = True exclude = + src/semver/__init__.py .env, .eggs, .tox, diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 57ee4b26..88990ad8 --- a/setup.py +++ b/setup.py @@ -1,75 +1,4 @@ #!/usr/bin/env python3 -# import semver as package -from os.path import dirname, join -from setuptools import setup -import re +import setuptools - -VERSION_MATCH = re.compile(r"__version__ = ['\"]([^'\"]*)['\"]", re.M) - - -def read_file(filename): - """ - Read RST file and return content - - :param filename: the RST file - :return: content of the RST file - """ - with open(join(dirname(__file__), filename)) as f: - return f.read() - - -def find_meta(meta): - """ - Extract __*meta*__ from META_FILE. - """ - meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M - ) - if meta_match: - return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) - - -NAME = "semver" -META_FILE = read_file("semver.py") - - -# ----------------------------------------------------------------------------- -setup( - name=NAME, - version=find_meta("version"), - description=find_meta("description").strip(), - long_description=read_file("README.rst"), - long_description_content_type="text/x-rst", - author=find_meta("author"), - author_email=find_meta("author_email"), - url="https://github.com/python-semver/python-semver", - download_url="https://github.com/python-semver/python-semver/downloads", - project_urls={ - "Documentation": "https://python-semver.rtfd.io", - "Releases": "https://github.com/python-semver/python-semver/releases", - "Bug Tracker": "https://github.com/python-semver/python-semver/issues", - }, - py_modules=[NAME], - include_package_data=True, - license="BSD", - classifiers=[ - # See https://pypi.org/pypi?%3Aaction=list_classifiers - "Environment :: Web Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - # "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - python_requires=">=3.6.*", - tests_require=["tox", "virtualenv", "wheel"], - entry_points={"console_scripts": ["pysemver = semver:main"]}, -) +setuptools.setup() # For compatibility with python 3.6 diff --git a/src/semver/__about__.py b/src/semver/__about__.py new file mode 100644 index 00000000..aa293425 --- /dev/null +++ b/src/semver/__about__.py @@ -0,0 +1,29 @@ +""" +Metadata about semver. + +Contains information about semver's version, the implemented version +of the semver specifictation, author, maintainers, and description. + +.. autodata:: __version__ +""" + +#: Semver version +__version__ = "3.0.0-dev.2" + +#: Original semver author +__author__ = "Kostiantyn Rybnikov" + +#: Author's email address +__author_email__ = "k-bx@k-bx.com" + +#: Current maintainer +__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] + +#: Maintainer's email address +__maintainer_email__ = "s.celles@gmail.com" + +#: Short description about semver +__description__ = "Python helper for Semantic Versioning (http://semver.org)" + +#: Supported semver specification +SEMVER_SPEC_VERSION = "2.0.0" diff --git a/src/semver/__init__.py b/src/semver/__init__.py new file mode 100644 index 00000000..c6726f2e --- /dev/null +++ b/src/semver/__init__.py @@ -0,0 +1,39 @@ +""" +semver package major release 3. + +A Python module for semantic versioning. Simplifies comparing versions. +""" + +from ._deprecated import ( + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + compare, + finalize_version, + format_version, + match, + max_ver, + min_ver, + parse, + parse_version_info, + replace, + cmd_bump, + cmd_compare, + cmd_nextver, + cmd_check, + createparser, + process, + main, +) +from .version import Version, VersionInfo +from .__about__ import ( + __version__, + __author__, + __maintainer__, + __author_email__, + __description__, + __maintainer_email__, + SEMVER_SPEC_VERSION, +) diff --git a/src/semver/__main__.py b/src/semver/__main__.py new file mode 100644 index 00000000..7fde54d7 --- /dev/null +++ b/src/semver/__main__.py @@ -0,0 +1,28 @@ +""" +Module to support call with :file:`__main__.py`. Used to support the following +call:: + + $ python3 -m semver ... + +This makes it also possible to "run" a wheel like in this command:: + + $ python3 semver-3*-py3-none-any.whl/semver -h + +""" +import os.path +import sys +from typing import List + +from semver import cli + + +def main(cliargs: List[str] = None) -> int: + if __package__ == "": + path = os.path.dirname(os.path.dirname(__file__)) + sys.path[0:0] = [path] + + return cli.main(cliargs) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py new file mode 100644 index 00000000..545a2438 --- /dev/null +++ b/src/semver/_deprecated.py @@ -0,0 +1,387 @@ +""" +Contains all deprecated functions. + +.. autofunction: deprecated +""" +import inspect +import warnings +from functools import partial, wraps +from types import FrameType +from typing import Type, Union, Callable, cast + +from . import cli +from .version import Version +from ._types import F, String + + +def deprecated( + func: F = None, + replace: str = None, + version: str = None, + category: Type[Warning] = DeprecationWarning, +) -> Union[Callable[..., F], partial]: + """ + Decorates a function to output a deprecation warning. + + :param func: the function to decorate + :param replace: the function to replace (use the full qualified + name like ``semver.Version.bump_major``. + :param 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. + :return: decorated function which is marked as deprecated + """ + + if func is None: + return partial(deprecated, replace=replace, version=version, category=category) + + @wraps(func) + def wrapper(*args, **kwargs) -> Callable[..., F]: + msg_list = ["Function 'semver.{f}' is deprecated."] + + if version: + msg_list.append("Deprecated since version {v}. ") + msg_list.append("This function will be removed in semver 3.") + if replace: + msg_list.append("Use {r!r} instead.") + else: + msg_list.append("Use the respective 'semver.Version.{r}' instead.") + + f = cast(F, func).__qualname__ + r = replace or f + + frame = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back) + + msg = " ".join(msg_list) + warnings.warn_explicit( + msg.format(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) # type: ignore + + return wrapper + + +@deprecated(version="2.10.0") +def parse(version): + """ + Parse version to major, minor, patch, pre-release, build parts. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.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 + if not provided + :rtype: dict + + >>> ver = semver.parse('3.4.5-pre.2+build.4') + >>> ver['major'] + 3 + >>> ver['minor'] + 4 + >>> ver['patch'] + 5 + >>> ver['prerelease'] + 'pre.2' + >>> ver['build'] + 'build.4' + """ + return Version.parse(version).to_dict() + + +@deprecated(replace="semver.Version.parse", version="2.10.0") +def parse_version_info(version): + """ + Parse version string to a VersionInfo instance. + + .. deprecated:: 2.10.0 + Use :func:`semver.VersionInfo.parse` instead. + .. versionadded:: 2.7.2 + Added :func:`semver.parse_version_info` + + :param version: version string + :return: a :class:`VersionInfo` instance + + >>> version_info = semver.Version.parse("3.4.5-pre.2+build.4") + >>> version_info.major + 3 + >>> version_info.minor + 4 + >>> version_info.patch + 5 + >>> version_info.prerelease + 'pre.2' + >>> version_info.build + 'build.4' + """ + return Version.parse(version) + + +@deprecated(version="2.10.0") +def compare(ver1, ver2): + """ + Compare two versions strings. + + :param ver1: version string 1 + :param ver2: version string 2 + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + :rtype: int + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + """ + v1 = Version.parse(ver1) + return v1.compare(ver2) + + +@deprecated(version="2.10.0") +def match(version, match_expr): + """ + Compare two versions strings through a comparison. + + :param str version: a version string + :param str match_expr: operator and version; valid operators are + < smaller than + > greater than + >= greator or equal than + <= smaller or equal than + == equal + != not equal + :return: True if the expression matches the version, otherwise False + :rtype: bool + + >>> semver.match("2.0.0", ">=1.0.0") + True + >>> semver.match("1.0.0", ">1.0.0") + False + """ + ver = Version.parse(version) + return ver.match(match_expr) + + +@deprecated(replace="max", version="2.10.2") +def max_ver(ver1, ver2): + """ + Returns the greater version of two versions strings. + + :param ver1: version string 1 + :param ver2: version string 2 + :return: the greater version of the two + :rtype: :class:`Version` + + >>> semver.max_ver("1.0.0", "2.0.0") + '2.0.0' + """ + if isinstance(ver1, String.__args__): # type: ignore + ver1 = Version.parse(ver1) + elif not isinstance(ver1, Version): + raise TypeError() + cmp_res = ver1.compare(ver2) + if cmp_res >= 0: + return str(ver1) + else: + return ver2 + + +@deprecated(replace="min", version="2.10.2") +def min_ver(ver1, ver2): + """ + Returns the smaller version of two versions strings. + + :param ver1: version string 1 + :param ver2: version string 2 + :return: the smaller version of the two + :rtype: :class:`Version` + + >>> semver.min_ver("1.0.0", "2.0.0") + '1.0.0' + """ + ver1 = Version.parse(ver1) + cmp_res = ver1.compare(ver2) + if cmp_res <= 0: + return str(ver1) + else: + return ver2 + + +@deprecated(replace="str(versionobject)", version="2.10.0") +def format_version(major, minor, patch, prerelease=None, build=None): + """ + Format a version string according to the Semantic Versioning specification. + + .. deprecated:: 2.10.0 + Use ``str(Version(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 + :param str prerelease: the optional prerelease part of a version + :param str build: the optional build part of a version + :return: the formatted string + :rtype: str + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + """ + return str(Version(major, minor, patch, prerelease, build)) + + +@deprecated(version="2.10.0") +def bump_major(version): + """ + Raise the major part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_major` instead. + + :param: version string + :return: the raised version string + :rtype: str + + >>> semver.bump_major("3.4.5") + '4.0.0' + """ + return str(Version.parse(version).bump_major()) + + +@deprecated(version="2.10.0") +def bump_minor(version): + """ + Raise the minor part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_minor` instead. + + :param: version string + :return: the raised version string + :rtype: str + + >>> semver.bump_minor("3.4.5") + '3.5.0' + """ + return str(Version.parse(version).bump_minor()) + + +@deprecated(version="2.10.0") +def bump_patch(version): + """ + Raise the patch part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_patch` instead. + + :param: version string + :return: the raised version string + :rtype: str + + >>> semver.bump_patch("3.4.5") + '3.4.6' + """ + return str(Version.parse(version).bump_patch()) + + +@deprecated(version="2.10.0") +def bump_prerelease(version, token="rc"): + """ + Raise the prerelease part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_prerelease` instead. + + :param version: version string + :param token: defaults to 'rc' + :return: the raised version string + :rtype: str + + >>> semver.bump_prerelease('3.4.5', 'dev') + '3.4.5-dev.1' + """ + return str(Version.parse(version).bump_prerelease(token)) + + +@deprecated(version="2.10.0") +def bump_build(version, token="build"): + """ + Raise the build part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_build` instead. + + :param version: version string + :param token: defaults to 'build' + :return: the raised version string + :rtype: str + + >>> semver.bump_build('3.4.5-rc.1+build.9') + '3.4.5-rc.1+build.10' + """ + return str(Version.parse(version).bump_build(token)) + + +@deprecated(version="2.10.0") +def finalize_version(version): + """ + Remove any prerelease and build metadata from the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.finalize_version` instead. + + .. versionadded:: 2.7.9 + Added :func:`finalize_version` + + :param version: version string + :return: the finalized version string + :rtype: str + + >>> semver.finalize_version('1.2.3-rc.5') + '1.2.3' + """ + verinfo = Version.parse(version) + return str(verinfo.finalize_version()) + + +@deprecated(version="2.10.0") +def replace(version, **parts): + """ + Replace one or more parts of a version and return the new string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.replace` instead. + .. versionadded:: 2.9.0 + Added :func:`replace` + + :param version: the version string to replace + :param 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 + + >>> import semver + >>> semver.replace("1.2.3", major=2, patch=10) + '2.2.10' + """ + return str(Version.parse(version).replace(**parts)) + + +# CLI +cmd_bump = deprecated(cli.cmd_bump, "semver.cli.cmd_bump", "3.0.0") +cmd_check = deprecated(cli.cmd_check, "semver.cli.cmd_check", "3.0.0") +cmd_compare = deprecated(cli.cmd_compare, "semver.cli.cmd_compare", "3.0.0") +cmd_nextver = deprecated(cli.cmd_nextver, "semver.cli.cmd_nextver", "3.0.0") +createparser = deprecated(cli.createparser, "semver.cli.createparser", "3.0.0") +process = deprecated(cli.process, "semver.cli.process", "3.0.0") +main = deprecated(cli.main, "semver.cli.main", "3.0.0") diff --git a/src/semver/_types.py b/src/semver/_types.py new file mode 100644 index 00000000..823c7349 --- /dev/null +++ b/src/semver/_types.py @@ -0,0 +1,8 @@ +from typing import Union, Optional, Tuple, Dict, Iterable, Callable, TypeVar + +VersionPart = Union[int, Optional[str]] +VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]] +VersionDict = Dict[str, VersionPart] +VersionIterator = Iterable[VersionPart] +String = Union[str, bytes] +F = TypeVar("F", bound=Callable) diff --git a/src/semver/cli.py b/src/semver/cli.py new file mode 100644 index 00000000..ca400373 --- /dev/null +++ b/src/semver/cli.py @@ -0,0 +1,164 @@ +"""CLI parsing for :command:`pysemver` command.""" + +import argparse +import sys +from typing import cast, List + +from .version import Version +from .__about__ import __version__ + + +def cmd_bump(args: argparse.Namespace) -> str: + """ + Subcommand: Bumps a version. + + Synopsis: bump + can be major, minor, patch, prerelease, or build + + :param args: The parsed arguments + :return: the new, bumped version + """ + maptable = { + "major": "bump_major", + "minor": "bump_minor", + "patch": "bump_patch", + "prerelease": "bump_prerelease", + "build": "bump_build", + } + if args.bump is None: + # When bump is called without arguments, + # print the help and exit + args.parser.parse_args(["bump", "-h"]) + + ver = Version.parse(args.version) + # get the respective method and call it + func = getattr(ver, maptable[cast(str, args.bump)]) + return str(func()) + + +def cmd_check(args: argparse.Namespace) -> None: + """ + Subcommand: Checks if a string is a valid semver version. + + Synopsis: check + + :param args: The parsed arguments + """ + if Version.isvalid(args.version): + return None + raise ValueError("Invalid version %r" % args.version) + + +def cmd_compare(args: argparse.Namespace) -> str: + """ + Subcommand: Compare two versions + + Synopsis: compare + + :param args: The parsed arguments + """ + ver1 = Version.parse(args.version1) + return str(ver1.compare(args.version2)) + + +def cmd_nextver(args: argparse.Namespace) -> str: + """ + Subcommand: Determines the next version, taking prereleases into account. + + Synopsis: nextver + + :param args: The parsed arguments + """ + version = Version.parse(args.version) + return str(version.next_version(args.part)) + + +def createparser() -> argparse.ArgumentParser: + """ + Create an :class:`argparse.ArgumentParser` instance. + + :return: parser instance + """ + parser = argparse.ArgumentParser(prog=__package__, description=__doc__) + + parser.add_argument( + "--version", action="version", version="%(prog)s " + __version__ + ) + + s = parser.add_subparsers() + # create compare subcommand + parser_compare = s.add_parser("compare", help="Compare two versions") + parser_compare.set_defaults(func=cmd_compare) + parser_compare.add_argument("version1", help="First version") + parser_compare.add_argument("version2", help="Second version") + + # create bump subcommand + parser_bump = s.add_parser("bump", help="Bumps a version") + parser_bump.set_defaults(func=cmd_bump) + sb = parser_bump.add_subparsers(title="Bump commands", dest="bump") + + # Create subparsers for the bump subparser: + for p in ( + sb.add_parser("major", help="Bump the major part of the version"), + sb.add_parser("minor", help="Bump the minor part of the version"), + sb.add_parser("patch", help="Bump the patch part of the version"), + sb.add_parser("prerelease", help="Bump the prerelease part of the version"), + sb.add_parser("build", help="Bump the build part of the version"), + ): + p.add_argument("version", help="Version to raise") + + # Create the check subcommand + parser_check = s.add_parser( + "check", help="Checks if a string is a valid semver version" + ) + parser_check.set_defaults(func=cmd_check) + parser_check.add_argument("version", help="Version to check") + + # Create the nextver subcommand + parser_nextver = s.add_parser( + "nextver", help="Determines the next version, taking prereleases into account." + ) + parser_nextver.set_defaults(func=cmd_nextver) + parser_nextver.add_argument("version", help="Version to raise") + parser_nextver.add_argument( + "part", help="One of 'major', 'minor', 'patch', or 'prerelease'" + ) + return parser + + +def process(args: argparse.Namespace) -> str: + """ + Process the input from the CLI. + + :param args: The parsed arguments + :param parser: the parser instance + :return: result of the selected action + """ + if not hasattr(args, "func"): + args.parser.print_help() + raise SystemExit() + + # Call the respective function object: + return args.func(args) + + +def main(cliargs: List[str] = None) -> int: + """ + Entry point for the application script. + + :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) + :return: error code + """ + try: + parser = createparser() + args = parser.parse_args(args=cliargs) + # Save parser instance: + args.parser = parser + result = process(args) + if result is not None: + print(result) + return 0 + + except (ValueError, TypeError) as err: + print("ERROR", err, file=sys.stderr) + return 2 diff --git a/src/semver/py.typed b/src/semver/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/semver/version.py b/src/semver/version.py new file mode 100644 index 00000000..64353011 --- /dev/null +++ b/src/semver/version.py @@ -0,0 +1,660 @@ +"""Version handling.""" + +import collections +import re +from functools import wraps +from typing import ( + Any, + Dict, + Iterable, + Optional, + SupportsInt, + Tuple, + Union, + cast, + Callable, + Collection, +) + +from ._types import ( + VersionTuple, + VersionDict, + VersionIterator, + String, + VersionPart, +) + +# These types are required here because of circular imports +Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str] +Comparator = Callable[["Version", Comparable], bool] + + +def cmp(a, b): # TODO: type hints + """Return negative if ab.""" + return (a > b) - (a < b) + + +def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: + # Taken from six project + """ + Coerce *s* to `str`. + + * `str` -> `str` + * `bytes` -> decoded to `str` + + :param s: the string to convert + :type s: str | bytes + :param encoding: the encoding to apply, defaults to "utf-8" + :type encoding: str + :param errors: set a different error handling scheme, + defaults to "strict". + Other possible values are `ignore`, `replace`, and + `xmlcharrefreplace` as well as any other name + registered with :func:`codecs.register_error`. + :type errors: str + :raises TypeError: if ``s`` is not str or bytes type + :return: the converted string + :rtype: str + """ + if isinstance(s, bytes): + s = s.decode(encoding, errors) + elif not isinstance(s, String.__args__): # type: ignore + raise TypeError("not expecting type '%s'" % type(s)) + return s + + +def comparator(operator: Comparator) -> Comparator: + """Wrap a Version binary op method in a type-check.""" + + @wraps(operator) + def wrapper(self: "Version", other: Comparable) -> bool: + comparable_types = ( + Version, + dict, + tuple, + list, + *String.__args__, # type: ignore + ) + if not isinstance(other, comparable_types): + raise TypeError( + "other type %r must be in %r" % (type(other), comparable_types) + ) + return operator(self, other) + + return wrapper + + +def _nat_cmp(a, b): # TODO: type hints + def convert(text): + return int(text) if re.match("^[0-9]+$", text) else text + + def split_key(key): + return [convert(c) for c in key.split(".")] + + def cmp_prerelease_tag(a, b): + if isinstance(a, int) and isinstance(b, int): + return cmp(a, b) + elif isinstance(a, int): + return -1 + elif isinstance(b, int): + return 1 + else: + return cmp(a, b) + + a, b = a or "", b or "" + a_parts, b_parts = split_key(a), split_key(b) + for sub_a, sub_b in zip(a_parts, b_parts): + cmp_result = cmp_prerelease_tag(sub_a, sub_b) + if cmp_result != 0: + return cmp_result + else: + return cmp(len(a), len(b)) + + +class Version: + """ + A semver compatible version class. + + :param major: version when you make incompatible API changes. + :param minor: version when you add functionality in + a backwards-compatible manner. + :param patch: version when you make backwards-compatible bug fixes. + :param prerelease: an optional prerelease string + :param build: an optional build string + """ + + __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: SupportsInt, + minor: SupportsInt = 0, + patch: SupportsInt = 0, + prerelease: Union[String, int] = None, + build: Union[String, int] = None, + ): + # Build a dictionary of the arguments except prerelease and build + version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} + + for name, value in version_parts.items(): + if value < 0: + raise ValueError( + "{!r} is negative. A version can only be positive.".format(name) + ) + + self._major = version_parts["major"] + self._minor = version_parts["minor"] + self._patch = version_parts["patch"] + self._prerelease = None if prerelease is None else str(prerelease) + self._build = None if build is None else str(build) + + @property + def major(self) -> int: + """The major part of a version (read-only).""" + return self._major + + @major.setter + def major(self, value): + raise AttributeError("attribute 'major' is readonly") + + @property + def minor(self) -> int: + """The minor part of a version (read-only).""" + return self._minor + + @minor.setter + def minor(self, value): + raise AttributeError("attribute 'minor' is readonly") + + @property + def patch(self) -> int: + """The patch part of a version (read-only).""" + return self._patch + + @patch.setter + def patch(self, value): + raise AttributeError("attribute 'patch' is readonly") + + @property + def prerelease(self) -> Optional[str]: + """The prerelease part of a version (read-only).""" + return self._prerelease + + @prerelease.setter + def prerelease(self, value): + raise AttributeError("attribute 'prerelease' is readonly") + + @property + def build(self) -> Optional[str]: + """The build part of a version (read-only).""" + return self._build + + @build.setter + def build(self, value): + raise AttributeError("attribute 'build' is readonly") + + def to_tuple(self) -> VersionTuple: + """ + Convert the VersionInfo object to a tuple. + + .. versionadded:: 2.10.0 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. + + :return: a tuple with all the parts + + >>> semver.Version(5, 3, 1).to_tuple() + (5, 3, 1, None, None) + """ + return (self.major, self.minor, self.patch, self.prerelease, self.build) + + def to_dict(self) -> VersionDict: + """ + Convert the VersionInfo object to an OrderedDict. + + .. versionadded:: 2.10.0 + 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``. + + >>> semver.Version(3, 2, 1).to_dict() + OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ +('prerelease', None), ('build', None)]) + """ + return collections.OrderedDict( + ( + ("major", self.major), + ("minor", self.minor), + ("patch", self.patch), + ("prerelease", self.prerelease), + ("build", self.build), + ) + ) + + def __iter__(self) -> VersionIterator: + """Implement iter(self).""" + yield from self.to_tuple() + + @staticmethod + def _increment_string(string: str) -> str: + """ + Look for the last sequence of number(s) in a string and increment. + + :param string: the string to search for. + :return: the incremented string + + Source: + http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 + """ + match = Version._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) -> "Version": + """ + Raise the major part of the version, return a new object but leave self + untouched. + + :return: new object with the raised major part + + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_major() + Version(major=4, minor=0, patch=0, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major + 1) + + def bump_minor(self) -> "Version": + """ + Raise the minor part of the version, return a new object but leave self + untouched. + + :return: new object with the raised minor part + + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_minor() + Version(major=3, minor=5, patch=0, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major, self._minor + 1) + + def bump_patch(self) -> "Version": + """ + Raise the patch part of the version, return a new object but leave self + untouched. + + :return: new object with the raised patch part + + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_patch() + Version(major=3, minor=4, patch=6, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major, self._minor, self._patch + 1) + + def bump_prerelease(self, token: str = "rc") -> "Version": + """ + Raise the prerelease part of the version, return a new object but leave + self untouched. + + :param token: defaults to 'rc' + :return: new object with the raised prerelease part + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_prerelease() + Version(major=3, minor=4, patch=5, prerelease='rc.2', \ +build=None) + """ + 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: str = "build") -> "Version": + """ + Raise the build part of the version, return a new object but leave self + untouched. + + :param token: defaults to 'build' + :return: new object with the raised build part + + >>> ver = semver.parse("3.4.5-rc.1+build.9") + >>> ver.bump_build() + Version(major=3, minor=4, patch=5, prerelease='rc.1', \ +build='build.10') + """ + 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) + + def compare(self, other: Comparable) -> int: + """ + Compare self with other. + + :param other: the second version + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + + + >>> semver.compare("2.0.0") + -1 + >>> semver.compare("1.0.0") + 1 + >>> semver.compare("2.0.0") + 0 + >>> semver.compare(dict(major=2, minor=0, patch=0)) + 0 + """ + cls = type(self) + if isinstance(other, String.__args__): # type: ignore + other = cls.parse(other) + elif isinstance(other, dict): + other = cls(**other) + elif isinstance(other, (tuple, list)): + other = cls(*other) + elif not isinstance(other, cls): + raise TypeError( + f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, " + f"but got {type(other)}" + ) + + v1 = self.to_tuple()[:3] + v2 = other.to_tuple()[:3] + x = cmp(v1, v2) + if x: + return x + + rc1, rc2 = self.prerelease, other.prerelease + rccmp = _nat_cmp(rc1, rc2) + + if not rccmp: + return 0 + if not rc1: + return 1 + elif not rc2: + return -1 + + return rccmp + + def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": + """ + Determines next version, preserving natural order. + + .. versionadded:: 2.10.0 + + This function is taking prereleases into account. + The "major", "minor", and "patch" raises the respective parts like + the ``bump_*`` functions. The real difference is using the + "preprelease" part. It gives you the next patch version of the + prerelease, for example: + + >>> str(semver.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' + + :param part: One of "major", "minor", "patch", or "prerelease" + :param prerelease_token: prefix string of prerelease, defaults to 'rc' + :return: new object with the appropriate part raised + """ + validparts = { + "major", + "minor", + "patch", + "prerelease", + # "build", # currently not used + } + if part not in validparts: + raise ValueError( + "Invalid part. Expected one of {validparts}, but got {part!r}".format( + validparts=validparts, part=part + ) + ) + version = self + if (version.prerelease or version.build) and ( + part == "patch" + or (part == "minor" and version.patch == 0) + or (part == "major" and version.minor == version.patch == 0) + ): + return version.replace(prerelease=None, build=None) + + if part in ("major", "minor", "patch"): + return getattr(version, "bump_" + part)() + + if not version.prerelease: + version = version.bump_patch() + return version.bump_prerelease(prerelease_token) + + @comparator + def __eq__(self, other: Comparable) -> bool: # type: ignore + return self.compare(other) == 0 + + @comparator + def __ne__(self, other: Comparable) -> bool: # type: ignore + return self.compare(other) != 0 + + @comparator + def __lt__(self, other: Comparable) -> bool: + return self.compare(other) < 0 + + @comparator + def __le__(self, other: Comparable) -> bool: + return self.compare(other) <= 0 + + @comparator + def __gt__(self, other: Comparable) -> bool: + return self.compare(other) > 0 + + @comparator + def __ge__(self, other: Comparable) -> bool: + return self.compare(other) >= 0 + + def __getitem__( + self, index: Union[int, slice] + ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: + """ + self.__getitem__(index) <==> self[index] Implement getitem. If the part + requested is undefined, or a part of the range requested is undefined, + it will throw an index error. Negative indices are not supported. + + :param Union[int, slice] index: a positive integer indicating the + offset or a :func:`slice` object + :raises IndexError: if index is beyond the range or a part is None + :return: the requested part of the version at position index + >>> ver = semver.Version.parse("3.4.5") + >>> ver[0], ver[1], ver[2] + (3, 4, 5) + """ + if isinstance(index, int): + index = slice(index, index + 1) + index = cast(slice, index) + + if ( + isinstance(index, slice) + and (index.start is not None and index.start < 0) + or (index.stop is not None and index.stop < 0) + ): + raise IndexError("Version index cannot be negative") + + part = tuple( + filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index])) + ) + + if len(part) == 1: + return part[0] + elif not part: + raise IndexError("Version part undefined") + return part + + def __repr__(self) -> str: + s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) + return "%s(%s)" % (type(self).__name__, s) + + def __str__(self) -> str: + """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) -> int: + return hash(self.to_tuple()[:4]) + + def finalize_version(self) -> "Version": + """ + Remove any prerelease and build metadata from the version. + :return: a new instance with the finalized version string + >>> str(semver.Version.parse('1.2.3-rc.5').finalize_version()) + '1.2.3' + """ + cls = type(self) + return cls(self.major, self.minor, self.patch) + + def match(self, match_expr: str) -> bool: + """ + Compare self to match a match expression. + + :param match_expr: operator and version; valid operators are + < smaller than + > greater than + >= greator or equal than + <= smaller or equal than + == equal + != not equal + :return: True if the expression matches the version, otherwise False + + >>> semver.Version.parse("2.0.0").match(">=1.0.0") + True + >>> semver.Version.parse("1.0.0").match(">1.0.0") + False + """ + prefix = match_expr[:2] + if prefix in (">=", "<=", "==", "!="): + match_version = match_expr[2:] + elif prefix and prefix[0] in (">", "<"): + prefix = prefix[0] + match_version = match_expr[1:] + else: + raise ValueError( + "match_expr parameter should be in format , " + "where is one of " + "['<', '>', '==', '<=', '>=', '!=']. " + "You provided: %r" % match_expr + ) + + possibilities_dict = { + ">": (1,), + "<": (-1,), + "==": (0,), + "!=": (-1, 1), + ">=": (0, 1), + "<=": (-1, 0), + } + + possibilities = possibilities_dict[prefix] + cmp_res = self.compare(match_version) + + return cmp_res in possibilities + + @classmethod + def parse(cls, version: String) -> "Version": + """ + Parse version string to a VersionInfo instance. + + .. versionchanged:: 2.11.0 + Changed method from static to classmethod to + allow subclasses. + + :param version: version string + :return: a :class:`VersionInfo` instance + :raises ValueError: if version is invalid + + >>> semver.Version.parse('3.4.5-pre.2+build.4') + VersionInfo(major=3, minor=4, patch=5, \ +prerelease='pre.2', build='build.4') + """ + version_str = ensure_str(version) + match = cls._REGEX.match(version_str) + if match is None: + raise ValueError(f"{version_str} is not valid SemVer string") + + matched_version_parts: Dict[str, Any] = match.groupdict() + + return cls(**matched_version_parts) + + def replace(self, **parts: Union[int, Optional[str]]) -> "Version": + """ + Replace one or more parts of a version and return a new + :class:`Version` object, but leave self untouched + + .. versionadded:: 2.9.0 + Added :func:`Version.replace` + + :param parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the new :class:`Version` object with the changed + parts + :raises TypeError: if ``parts`` contains invalid keys + """ + version = self.to_dict() + version.update(parts) + try: + return Version(**version) # type: ignore + except TypeError: + unknownkeys = set(parts) - set(self.to_dict()) + error = "replace() got %d unexpected keyword " "argument(s): %s" % ( + len(unknownkeys), + ", ".join(unknownkeys), + ) + raise TypeError(error) + + @classmethod + def isvalid(cls, version: str) -> bool: + """ + Check if the string is a valid semver version. + + .. versionadded:: 2.9.1 + + :param version: the version string to check + :return: True if the version string is a valid semver version, False + otherwise. + """ + try: + cls.parse(version) + return True + except ValueError: + return False + + +#: Keep the VersionInfo name for compatibility +VersionInfo = Version diff --git a/tests/conftest.py b/tests/conftest.py index 2e935d0b..f7f927cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ @pytest.fixture(autouse=True) def add_semver(doctest_namespace): + doctest_namespace["Version"] = semver.Version doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix @@ -23,8 +24,8 @@ def version(): Creates a version :return: a version type - :rtype: VersionInfo + :rtype: Version """ - return semver.VersionInfo( + return semver.Version( major=1, minor=2, patch=3, prerelease="alpha.1.2", build="build.11.e0f985a" ) diff --git a/tests/test_compare.py b/tests/test_compare.py index 41caa08d..1c99f450 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -1,6 +1,7 @@ import pytest -from semver import VersionInfo, compare +import semver +from semver import Version, compare @pytest.mark.parametrize( @@ -123,15 +124,15 @@ def test_should_get_more_rc1(): def test_should_compare_prerelease_with_numbers_and_letters(): - v1 = VersionInfo(major=1, minor=9, patch=1, prerelease="1unms", build=None) - v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build="1asd") + v1 = Version(major=1, minor=9, patch=1, prerelease="1unms", build=None) + v2 = Version(major=1, minor=9, patch=1, prerelease=None, build="1asd") assert v1 < v2 assert compare("1.9.1-1unms", "1.9.1+1") == -1 def test_should_compare_version_info_objects(): - v1 = VersionInfo(major=0, minor=10, patch=4) - v2 = VersionInfo(major=0, minor=10, patch=4, prerelease="beta.1", build=None) + v1 = Version(major=0, minor=10, patch=4) + v2 = Version(major=0, minor=10, patch=4, prerelease="beta.1", build=None) # use `not` to enforce using comparision operators assert v1 != v2 @@ -141,7 +142,7 @@ def test_should_compare_version_info_objects(): assert not (v1 <= v2) assert not (v1 == v2) - v3 = VersionInfo(major=0, minor=10, patch=4) + v3 = Version(major=0, minor=10, patch=4) assert not (v1 != v3) assert not (v1 > v3) @@ -150,7 +151,7 @@ def test_should_compare_version_info_objects(): assert v1 <= v3 assert v1 == v3 - v4 = VersionInfo(major=0, minor=10, patch=5) + v4 = Version(major=0, minor=10, patch=5) assert v1 != v4 assert not (v1 > v4) assert not (v1 >= v4) @@ -160,7 +161,7 @@ def test_should_compare_version_info_objects(): def test_should_compare_version_dictionaries(): - v1 = VersionInfo(major=0, minor=10, patch=4) + v1 = Version(major=0, minor=10, patch=4) v2 = dict(major=0, minor=10, patch=4, prerelease="beta.1", build=None) assert v1 != v2 @@ -199,8 +200,8 @@ def test_should_compare_version_dictionaries(): ), # fmt: on ) def test_should_compare_version_tuples(t): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v0 = Version(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") assert v0 < t assert v0 <= t @@ -228,8 +229,8 @@ def test_should_compare_version_tuples(t): ), # fmt: on ) def test_should_compare_version_list(lst): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v0 = Version(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") assert v0 < lst assert v0 <= lst @@ -257,8 +258,8 @@ def test_should_compare_version_list(lst): ), # fmt: on ) def test_should_compare_version_string(s): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v0 = Version(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") assert v0 < s assert v0 <= s @@ -277,7 +278,7 @@ def test_should_compare_version_string(s): @pytest.mark.parametrize("s", ("1", "1.0", "1.0.x")) def test_should_not_allow_to_compare_invalid_versionstring(s): - v = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") with pytest.raises(ValueError): v < s with pytest.raises(ValueError): @@ -285,19 +286,19 @@ def test_should_not_allow_to_compare_invalid_versionstring(s): def test_should_not_allow_to_compare_version_with_int(): - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") with pytest.raises(TypeError): v1 > 1 with pytest.raises(TypeError): 1 > v1 with pytest.raises(TypeError): - v1.compare(1) + semver.compare(1) def test_should_compare_prerelease_and_build_with_numbers(): - assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < VersionInfo( + assert Version(major=1, minor=9, patch=1, prerelease=1, build=1) < Version( major=1, minor=9, patch=1, prerelease=2, build=1 ) - assert VersionInfo(1, 9, 1, 1, 1) < VersionInfo(1, 9, 1, 2, 1) - assert VersionInfo("2") < VersionInfo(10) - assert VersionInfo("2") < VersionInfo("10") + assert Version(1, 9, 1, 1, 1) < Version(1, 9, 1, 2, 1) + assert Version("2") < Version(10) + assert Version("2") < Version("10") diff --git a/tests/test_deprecated_functions.py b/tests/test_deprecated_functions.py index 8a04e3e9..0b5123cc 100644 --- a/tests/test_deprecated_functions.py +++ b/tests/test_deprecated_functions.py @@ -1,22 +1,31 @@ +from argparse import Namespace + import pytest from semver import ( - bump_build, + parse, + parse_version_info, + compare, + match, + max_ver, + min_ver, + format_version, bump_major, bump_minor, bump_patch, bump_prerelease, - compare, - deprecated, + bump_build, finalize_version, - format_version, - match, - max_ver, - min_ver, - parse, - parse_version_info, replace, + cmd_bump, + cmd_compare, + cmd_check, + cmd_nextver, + createparser, + process, + main, ) +from semver._deprecated import deprecated @pytest.mark.parametrize( @@ -36,6 +45,17 @@ (replace, ("1.2.3",), dict(major=2, patch=10)), (max_ver, ("1.2.3", "1.2.4"), {}), (min_ver, ("1.2.3", "1.2.4"), {}), + (cmd_bump, (Namespace(bump="major", version="1.2.3"),), {}), + (cmd_compare, (Namespace(version1="1.2.3", version2="2.1.3"),), {}), + (cmd_check, (Namespace(version="1.2.3"),), {}), + (cmd_nextver, (Namespace(version="1.2.3", part="major"),), {}), + (createparser, (), {}), + ( + process, + (Namespace(func=cmd_compare, version1="1.2.3", version2="2.1.3"),), + {}, + ), + (main, (["bump", "major", "1.2.3"],), {}), ], ) def test_should_raise_deprecation_warnings(func, args, kwargs): diff --git a/tests/test_format.py b/tests/test_format.py index b1c6ad5b..73ff3122 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo, finalize_version, format_version +from semver import Version, finalize_version, format_version @pytest.mark.parametrize( @@ -28,7 +28,7 @@ def test_should_correctly_format_version(): def test_parse_method_for_version_info(): s_version = "1.2.3-alpha.1.2+build.11.e0f985a" - v = VersionInfo.parse(s_version) + v = Version.parse(s_version) assert str(v) == s_version @@ -36,28 +36,28 @@ def test_parse_method_for_version_info(): "version, expected", [ ( - VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None)", + Version(major=1, minor=2, patch=3, prerelease=None, build=None), + "Version(major=1, minor=2, patch=3, prerelease=None, build=None)", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build=None)", + Version(major=1, minor=2, patch=3, prerelease="r.1", build=None), + "Version(major=1, minor=2, patch=3, prerelease='r.1', build=None)", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build=None)", + Version(major=1, minor=2, patch=3, prerelease="dev.1", build=None), + "Version(major=1, minor=2, patch=3, prerelease='dev.1', build=None)", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build="b.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build='b.1')", + Version(major=1, minor=2, patch=3, prerelease="dev.1", build="b.1"), + "Version(major=1, minor=2, patch=3, prerelease='dev.1', build='b.1')", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="b.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='b.1')", + Version(major=1, minor=2, patch=3, prerelease="r.1", build="b.1"), + "Version(major=1, minor=2, patch=3, prerelease='r.1', build='b.1')", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="build.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='build.1')", + Version(major=1, minor=2, patch=3, prerelease="r.1", build="build.1"), + "Version(major=1, minor=2, patch=3, prerelease='r.1', build='build.1')", ), ], ) diff --git a/tests/test_index.py b/tests/test_index.py index d54ea110..79e45025 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo +from semver import Version @pytest.mark.parametrize( @@ -24,7 +24,7 @@ ], ) def test_version_info_should_be_accessed_with_index(version, index, expected): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) assert version_info[index] == expected @@ -54,7 +54,7 @@ def test_version_info_should_be_accessed_with_index(version, index, expected): def test_version_info_should_be_accessed_with_slice_object( version, slice_object, expected ): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) assert version_info[slice_object] == expected @@ -74,7 +74,7 @@ def test_version_info_should_be_accessed_with_slice_object( ], ) def test_version_info_should_throw_index_error(version, index): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) with pytest.raises(IndexError, match=r"Version part undefined"): version_info[index] @@ -90,6 +90,6 @@ def test_version_info_should_throw_index_error(version, index): ], ) def test_version_info_should_throw_index_error_when_negative_index(version, index): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) with pytest.raises(IndexError, match=r"Version index cannot be negative"): version_info[index] diff --git a/tests/test_parsing.py b/tests/test_parsing.py index c31cca18..25c55c74 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo, parse, parse_version_info +from semver import Version, parse, parse_version_info @pytest.mark.parametrize( @@ -58,7 +58,7 @@ def test_parse_version_info_str_hash(): v = parse_version_info(s_version) assert v.__str__() == s_version d = {} - d[v] = "" # to ensure that VersionInfo are hashable + d[v] = "" # to ensure that Version are hashable @pytest.mark.parametrize( @@ -115,12 +115,12 @@ def test_equal_versions_have_equal_hashes(): def test_parse_method_for_version_info(): s_version = "1.2.3-alpha.1.2+build.11.e0f985a" - v = VersionInfo.parse(s_version) + v = Version.parse(s_version) assert str(v) == s_version def test_next_version_with_invalid_parts(): - version = VersionInfo.parse("1.0.1") + version = Version.parse("1.0.1") with pytest.raises(ValueError): version.next_version("invalid") @@ -151,7 +151,7 @@ def test_next_version_with_invalid_parts(): ], ) def test_next_version_with_versioninfo(version, part, expected): - ver = VersionInfo.parse(version) + ver = Version.parse(version) next_version = ver.next_version(part) - assert isinstance(next_version, VersionInfo) + assert isinstance(next_version, Version) assert str(next_version) == expected diff --git a/tests/test_pysemver-cli.py b/tests/test_pysemver-cli.py index 1fbeef26..e783a0b4 100644 --- a/tests/test_pysemver-cli.py +++ b/tests/test_pysemver-cli.py @@ -1,9 +1,18 @@ from argparse import Namespace from contextlib import contextmanager +from unittest.mock import patch import pytest -from semver import cmd_bump, cmd_check, cmd_compare, cmd_nextver, createparser, main +from semver import ( + cmd_bump, + cmd_check, + cmd_compare, + cmd_nextver, + createparser, + main, + __main__, +) @contextmanager @@ -125,3 +134,11 @@ def test_should_process_check_iscalled_with_valid_version(capsys): assert not result captured = capsys.readouterr() assert not captured.out + + +@pytest.mark.parametrize("package_name", ["", "semver"]) +def test_main_file_should_call_cli_main(package_name): + with patch("semver.__main__.cli.main") as mocked_main: + with patch("semver.__main__.__package__", package_name): + __main__.main() + mocked_main.assert_called_once() diff --git a/tests/test_replace.py b/tests/test_replace.py index e8e417a7..f223eddb 100644 --- a/tests/test_replace.py +++ b/tests/test_replace.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo, replace +from semver import Version, replace @pytest.mark.parametrize( @@ -42,9 +42,9 @@ def test_replace_raises_TypeError_for_invalid_keyword_arg(): ], ) def test_should_return_versioninfo_with_replaced_parts(version, parts, expected): - assert VersionInfo.parse(version).replace(**parts) == VersionInfo.parse(expected) + assert Version.parse(version).replace(**parts) == Version.parse(expected) def test_replace_raises_ValueError_for_non_numeric_values(): with pytest.raises(ValueError): - VersionInfo.parse("1.2.3").replace(major="x") + Version.parse("1.2.3").replace(major="x") diff --git a/tests/test_semver.py b/tests/test_semver.py index 630ebbce..b15bfeaf 100644 --- a/tests/test_semver.py +++ b/tests/test_semver.py @@ -1,13 +1,13 @@ import pytest # noqa -from semver import VersionInfo +from semver import Version @pytest.mark.parametrize( "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] ) def test_should_private_increment_string(string, expected): - assert VersionInfo._increment_string(string) == expected + assert Version._increment_string(string) == expected @pytest.mark.parametrize( @@ -21,7 +21,7 @@ def test_should_private_increment_string(string, expected): ) def test_should_not_allow_negative_numbers(ver): with pytest.raises(ValueError, match=".* is negative. .*"): - VersionInfo(**ver) + Version(**ver) def test_should_versioninfo_to_dict(version): @@ -47,31 +47,36 @@ def test_version_info_should_be_iterable(version): def test_should_be_able_to_use_strings_as_major_minor_patch(): - v = VersionInfo("1", "2", "3") + v = Version("1", "2", "3") assert isinstance(v.major, int) assert isinstance(v.minor, int) assert isinstance(v.patch, int) assert v.prerelease is None assert v.build is None - assert VersionInfo("1", "2", "3") == VersionInfo(1, 2, 3) + assert Version("1", "2", "3") == Version(1, 2, 3) def test_using_non_numeric_string_as_major_minor_patch_throws(): with pytest.raises(ValueError): - VersionInfo("a") + Version("a") with pytest.raises(ValueError): - VersionInfo(1, "a") + Version(1, "a") with pytest.raises(ValueError): - VersionInfo(1, 2, "a") + Version(1, 2, "a") def test_should_be_able_to_use_integers_as_prerelease_build(): - v = VersionInfo(1, 2, 3, 4, 5) + v = Version(1, 2, 3, 4, 5) assert isinstance(v.prerelease, str) assert isinstance(v.build, str) - assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, "4", "5") + assert Version(1, 2, 3, 4, 5) == Version(1, 2, 3, "4", "5") def test_should_versioninfo_isvalid(): - assert VersionInfo.isvalid("1.0.0") is True - assert VersionInfo.isvalid("foo") is False + assert Version.isvalid("1.0.0") is True + assert Version.isvalid("foo") is False + + +def test_versioninfo_compare_should_raise_when_passed_invalid_value(): + with pytest.raises(TypeError): + Version(1, 2, 3).compare(4) diff --git a/tests/test_subclass.py b/tests/test_subclass.py index afd10b4a..cbf9d271 100644 --- a/tests/test_subclass.py +++ b/tests/test_subclass.py @@ -1,8 +1,8 @@ -from semver import VersionInfo +from semver import Version def test_subclass_from_versioninfo(): - class SemVerWithVPrefix(VersionInfo): + class SemVerWithVPrefix(Version): @classmethod def parse(cls, version): if not version[0] in ("v", "V"): diff --git a/tests/test_typeerror-274.py b/tests/test_typeerror-274.py index a0375d0d..61480bcf 100644 --- a/tests/test_typeerror-274.py +++ b/tests/test_typeerror-274.py @@ -3,6 +3,7 @@ import pytest import semver +import semver.version PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 @@ -52,7 +53,7 @@ class TestEnsure: def test_ensure_binary_raise_type_error(self): with pytest.raises(TypeError): - semver.ensure_str(8) + semver.version.ensure_str(8) def test_errors_and_encoding(self): ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore") @@ -77,10 +78,10 @@ def test_ensure_binary_raise(self): ) def test_ensure_str(self): - converted_unicode = semver.ensure_str( + converted_unicode = semver.version.ensure_str( self.UNICODE_EMOJI, encoding="utf-8", errors="strict" ) - converted_binary = semver.ensure_str( + converted_binary = semver.version.ensure_str( self.BINARY_EMOJI, encoding="utf-8", errors="strict" ) diff --git a/tox.ini b/tox.ini index d253f1f6..73fbfc58 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,12 @@ envlist = py{36,37,38,39,310} docs mypy +isolated_build = True [testenv] description = Run test suite for {basepython} -whitelist_externals = make +allowlist_externals = make commands = pytest {posargs:} deps = pytest @@ -16,7 +17,6 @@ deps = setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 - [testenv:black] description = Check for formatting changes basepython = python3 @@ -35,14 +35,14 @@ commands = flake8 {posargs:} description = Check code style basepython = python3 deps = mypy -commands = mypy {posargs:--ignore-missing-imports .} +commands = mypy {posargs:--ignore-missing-imports --check-untyped-defs src} [testenv:docstrings] description = Check for PEP257 compatible docstrings basepython = python3 deps = docformatter -commands = docformatter --check {posargs:--pre-summary-newline semver.py} +commands = docformatter --check {posargs:--pre-summary-newline -r src} [testenv:checks] @@ -65,8 +65,21 @@ description = Build HTML documentation basepython = python3 deps = -r{toxinidir}/docs/requirements.txt skip_install = true -commands = make -C docs html - +allowlist_externals = + make + rm + echo + sed +commands_pre = + sphinx-apidoc --module-first -f --separate -H semver -o docs/_api src/semver src/semver/_types.py src/semver/_deprecated.py + # we don't need this, it just add another level and it's all in docs/api.rst + - rm docs/_api/modules.rst + # Include the semver.__about__ module before semver.cli: + sed -i '/semver\.cli/i\ \ \ semver.__about__' docs/_api/semver.rst +commands = + make -C docs html +commands_post = + echo "Find the HTML documentation at {toxinidir}/docs/_build/html/index.html" [testenv:man] description = Build the manpage 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