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