diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bff8173b..f310a7e3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 8f36dbc9..fb23fcf8 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -7,11 +7,18 @@ on: pull_request: branches: [ master ] +concurrency: + # only cancel in-progress runs of the same workflow + group: ${{ github.workflow }}-${{ github.ref }} + # ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + + jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Output env variables run: | echo "Default branch=${default-branch}" @@ -32,12 +39,12 @@ jobs: echo "\n" echo "::debug::---end" - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: - python-version: 3.6 + python-version: 3.7 - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip setuptools setuptools-scm pip install tox tox-gh-actions - name: Check run: | @@ -49,12 +56,18 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", + "3.8", + "3.9", + "3.10", + "3.11", + # "3.12-dev" + ] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.gitignore b/.gitignore index d26b5515..c0859cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -259,8 +259,12 @@ fabric.properties # -------- -# Patch/Diff Files -*.patch -*.diff +# Ignore files in the project's root: +/*.patch +/*.diff +/*.py +# but not this file: +!/setup.py + docs/_api !docs/_api/semver.__about__.rst diff --git a/BUILDING.rst b/BUILDING.rst new file mode 100644 index 00000000..61f3d4cb --- /dev/null +++ b/BUILDING.rst @@ -0,0 +1,60 @@ +.. _build-semver: + +Building semver +=============== + +.. _PEP 517: https://www.python.org/dev/peps/pep-0517/ +.. _PEP 621: https://www.python.org/dev/peps/pep-0621/ +.. _A Practical Guide to Setuptools and Pyproject.toml: https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/ +.. _Declarative config: https://setuptools.rtfd.io/en/latest/userguide/declarative_config.html + + +This project changed slightly its way how it is built. The reason for this +was to still support the "traditional" way with :command:`setup.py`, +but at the same time try out the newer way with :file:`pyproject.toml`. +As Python 3.6 got deprecated, this project does support from now on only +:file:`pyproject.toml`. + + +Background information +---------------------- + +Skip this section and head over to :ref:`build-pyproject-build` if you just +want to know how to build semver. +This section gives some background information how this project is set up. + +The traditional way with :command:`setup.py` in this project uses a +`Declarative config`_. With this approach, the :command:`setup.py` is +stripped down to its bare minimum and all the metadata is stored in +:file:`setup.cfg`. + +The new :file:`pyproject.toml` contains only information about the build backend, currently setuptools.build_meta. The idea is taken from +`A Practical Guide to Setuptools and Pyproject.toml`_. +Setuptools-specific configuration keys as defined in `PEP 621`_ are currently +not used. + + +.. _build-pyproject-build: + +Building with pyproject-build +----------------------------- + +To build semver you need: + +* The :mod:`build` module which implements the `PEP 517`_ build + frontend. + Install it with:: + + pip install build + + Some Linux distributions has already packaged it. If you prefer + to use the module with your package manager, search for + :file:`python-build` or :file:`python3-build` and install it. + +* The command :command:`pyproject-build` from the :mod:`build` module. + +To build semver, run:: + + pyproject-build + +After the command is finished, you can find two files in the :file:`dist` folder: a ``.tar.gz`` and a ``.whl`` file. \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3173507f..7f515aa1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,94 @@ in our repository. .. towncrier release notes start +Version 3.0.0-dev.4 +=================== + +:Released: 2022-12-18 +:Maintainer: + + +Bug Fixes +--------- + +* :gh:`374`: Correct Towncrier's config entries in the :file:`pyproject.toml` file. + The old entries ``[[tool.towncrier.type]]`` are deprecated and need + to be replaced by ``[tool.towncrier.fragment.]``. + + + +Deprecations +------------ + +* :gh:`372`: Deprecate support for Python 3.6. + + Python 3.6 reached its end of life and isn't supported anymore. + At the time of writing (Dec 2022), the lowest version is 3.7. + + Although the `poll `_ + didn't cast many votes, the majority agree to remove support for + Python 3.6. + + + +Improved Documentation +---------------------- + +* :gh:`335`: Add new section "Converting versions between PyPI and semver" the limitations + and possible use cases to convert from one into the other versioning scheme. + +* :gh:`340`: Describe how to get version from a file + +* :gh:`343`: Describe combining Pydantic with semver in the "Advanced topic" + section. + +* :gh:`350`: Restructure usage section. Create subdirectory "usage/" and splitted + all section into different files. + +* :gh:`351`: Introduce new topics for: + + * "Migration to semver3" + * "Advanced topics" + + + +Features +-------- + +* :pr:`359`: Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional + minor and patch parts. + +* :pr:`362`: Make :meth:`.Version.match` accept a bare version string as match expression, defaulting to + equality testing. + +* :gh:`364`: Enhance :file:`pyproject.toml` to make it possible to use the + :command:`pyproject-build` command from the build module. + For more information, see :ref:`build-semver`. + +* :gh:`365`: Improve :file:`pyproject.toml`. + + * Use setuptools, add metadata. Taken approach from + `A Practical Guide to Setuptools and Pyproject.toml + `_. + * Doc: Describe building of semver + * Remove :file:`.travis.yml` in :file:`MANIFEST.in` + (not needed anymore) + * Distinguish between Python 3.6 and others in :file:`tox.ini` + * Add skip_missing_interpreters option for :file:`tox.ini` + * GH Action: Upgrade setuptools and setuptools-scm and test + against 3.11.0-rc.2 + + + +Trivial/Internal Changes +------------------------ + +* :gh:`378`: Fix some typos in Towncrier configuration + + + +---- + Version 3.0.0-dev.3 =================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5fd75ab2..fe531990 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -64,7 +64,7 @@ We recommend the following workflow: a. Write test cases and run the complete test suite, see :ref:`testsuite` for details. - b. Write a changelog entry, see section :ref:`changelog`. + b. Write a changelog entry, see section :ref:`add-changelog`. c. If you have implemented a new feature, document it into our documentation to help our reader. See section :ref:`doc` for @@ -99,11 +99,11 @@ You can decide to run the complete test suite or only part of it: $ tox --skip-missing-interpreters It is possible to use one or more specific Python versions. Use the ``-e`` - option and one or more abbreviations (``py36`` for Python 3.6, ``py37`` for - Python 3.7 etc.):: + option and one or more abbreviations (``py37`` for Python 3.7, + ``py38`` for Python 3.8 etc.):: - $ tox -e py36 - $ tox -e py36,py37 + $ tox -e py37 + $ tox -e py37,py38 To get a complete list and a short description, run:: @@ -116,7 +116,7 @@ You can decide to run the complete test suite or only part of it: :func:`test_immutable_major` in the file :file:`test_bump.py` for all Python versions:: - $ tox -e py36 -- tests/test_bump.py::test_should_bump_major + $ tox -e py37 -- tests/test_bump.py::test_should_bump_major By default, pytest prints only a dot for each test function. To reveal the executed test function, use the following syntax:: @@ -124,16 +124,16 @@ You can decide to run the complete test suite or only part of it: $ tox -- -v You can combine the specific test function with the ``-e`` option, for - example, to limit the tests for Python 3.6 and 3.7 only:: + example, to limit the tests for Python 3.7 and 3.8 only:: - $ tox -e py36,py37 -- tests/test_bump.py::test_should_bump_major + $ tox -e py37,py38 -- tests/test_bump.py::test_should_bump_major Our code is checked against formatting, style, type, and docstring issues (`black`_, `flake8`_, `mypy`_, and `docformatter`_). It is recommended to run your tests in combination with :command:`checks`, for example:: - $ tox -e checks,py36,py37 + $ tox -e checks,py37,py38 .. _doc: @@ -214,7 +214,7 @@ documentation includes: edge cases. -.. _changelog: +.. _add-changelog: Adding a Changelog Entry ------------------------ diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9e63f4d4..e5fef99b 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -2,7 +2,7 @@ Contributors ############ -Python SemVer library +Python Semver library ##################### This document records the primary maintainers and significant @@ -14,9 +14,13 @@ Thank you to everyone whose work has made this possible. Primary maintainers =================== -* Kostiantyn Rybnikov +* Tom Schraitle * Sébastien Celles +Old maintainer: + +* Kostiantyn Rybnikov + Significant contributors ======================== @@ -37,7 +41,6 @@ Significant contributors * robi-wan * sbrudenell * T. Jameson Little -* Tom Schraitle * Thomas Laferriere * Tuure Laurinolli * Tyler Cross diff --git a/MANIFEST.in b/MANIFEST.in index 80257f1f..e37851c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,8 @@ include *.rst include *.txt -include test_*.py +include tests/test_*.py -exclude .travis.yml prune docs/_build recursive-exclude .github * -global-exclude *.py[cod] __pycache__ *.so *.dylib +global-exclude __pycache__ diff --git a/README.rst b/README.rst index 5c28cc69..cad99a04 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. note:: - This project works for Python 3.6 and greater only. If you are + This project works for Python 3.7 and greater only. If you are looking for a compatible version for Python 2, use the maintenance branch |MAINT|_. @@ -25,7 +25,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. 2.x.y However, keep in mind, the major 2 release is frozen: no new features nor backports will be integrated. - We recommend to upgrade your workflow to Python 3.x to gain support, + We recommend to upgrade your workflow to Python 3 to gain support, bugfixes, and new features. .. |MAINT| replace:: ``maint/v2`` @@ -112,7 +112,7 @@ There are other functions to discover. Read on! .. |docs| image:: https://readthedocs.org/projects/python-semver/badge/?version=latest :target: http://python-semver.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. _semantic versioning: http://semver.org/ +.. _semantic versioning: https://semver.org/ .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Black Formatter diff --git a/changelog.d/pr384.bugfix.rst b/changelog.d/pr384.bugfix.rst new file mode 100644 index 00000000..ca0b08d0 --- /dev/null +++ b/changelog.d/pr384.bugfix.rst @@ -0,0 +1,11 @@ +General cleanup, reformat files: + +* Reformat source code with black again as some config options + did accidentely exclude the semver source code. + Mostly remove some includes/excludes in the black config. +* Integrate concurrency in GH Action +* Ignore Python files on project dirs in .gitignore +* Remove unused patterns in MANIFEST.in +* Use ``extend-exclude`` for flake in :file:`setup.cfg`` and adapt list. +* Use ``skip_install=True`` in :file:`tox.ini` for black + diff --git a/docs/coerce.py b/docs/advanced/coerce.py similarity index 100% rename from docs/coerce.py rename to docs/advanced/coerce.py diff --git a/docs/advanced/combine-pydantic-and-semver.rst b/docs/advanced/combine-pydantic-and-semver.rst new file mode 100644 index 00000000..a9249daf --- /dev/null +++ b/docs/advanced/combine-pydantic-and-semver.rst @@ -0,0 +1,53 @@ +Combining Pydantic and semver +============================= + +According to its homepage, `Pydantic `_ +"enforces type hints at runtime, and provides user friendly errors when data +is invalid." + +To work with Pydantic, use the following steps: + + +1. Derive a new class from :class:`~semver.version.Version` + first and add the magic methods :py:meth:`__get_validators__` + and :py:meth:`__modify_schema__` like this: + + .. code-block:: python + + from semver import Version + + class PydanticVersion(Version): + @classmethod + def __get_validators__(cls): + """Return a list of validator methods for pydantic models.""" + yield cls.parse + + @classmethod + def __modify_schema__(cls, field_schema): + """Inject/mutate the pydantic field schema in-place.""" + field_schema.update(examples=["1.0.2", + "2.15.3-alpha", + "21.3.15-beta+12345", + ] + ) + +2. Create a new model (in this example :class:`MyModel`) and derive + it from :class:`pydantic.BaseModel`: + + .. code-block:: python + + import pydantic + + class MyModel(pydantic.BaseModel): + version: PydanticVersion + +3. Use your model like this: + + .. code-block:: python + + model = MyModel.parse_obj({"version": "1.2.3"}) + + The attribute :py:attr:`model.version` will be an instance of + :class:`~semver.version.Version`. + If the version is invalid, the construction will raise a + :py:exc:`pydantic.ValidationError`. diff --git a/docs/advanced/convert-pypi-to-semver.rst b/docs/advanced/convert-pypi-to-semver.rst new file mode 100644 index 00000000..76653ceb --- /dev/null +++ b/docs/advanced/convert-pypi-to-semver.rst @@ -0,0 +1,207 @@ +Converting versions between PyPI and semver +=========================================== + +.. Link + https://packaging.pypa.io/en/latest/_modules/packaging/version.html#InvalidVersion + +When packaging for PyPI, your versions are defined through `PEP 440`_. +This is the standard version scheme for Python packages and +implemented by the :class:`packaging.version.Version` class. + +However, these versions are different from semver versions +(cited from `PEP 440`_): + +* The "Major.Minor.Patch" (described in this PEP as "major.minor.micro") + aspects of semantic versioning (clauses 1-8 in the 2.0.0 + specification) are fully compatible with the version scheme defined + in this PEP, and abiding by these aspects is encouraged. + +* Semantic versions containing a hyphen (pre-releases - clause 10) + or a plus sign (builds - clause 11) are *not* compatible with this PEP + and are not permitted in the public version field. + +In other words, it's not always possible to convert between these different +versioning schemes without information loss. It depends on what parts are +used. The following table gives a mapping between these two versioning +schemes: + ++--------------+----------------+ +| PyPI Version | Semver version | ++==============+================+ +| ``epoch`` | n/a | ++--------------+----------------+ +| ``major`` | ``major`` | ++--------------+----------------+ +| ``minor`` | ``minor`` | ++--------------+----------------+ +| ``micro`` | ``patch`` | ++--------------+----------------+ +| ``pre`` | ``prerelease`` | ++--------------+----------------+ +| ``dev`` | ``build`` | ++--------------+----------------+ +| ``post`` | n/a | ++--------------+----------------+ + + +.. _convert_pypi_to_semver: + +From PyPI to semver +------------------- + +We distinguish between the following use cases: + + +* **"Incomplete" versions** + + If you only have a major part, this shouldn't be a problem. + The initializer of :class:`semver.Version ` takes + care to fill missing parts with zeros (except for major). + + .. code-block:: python + + >>> from packaging.version import Version as PyPIVersion + >>> from semver import Version + + >>> p = PyPIVersion("3.2") + >>> p.release + (3, 2) + >>> Version(*p.release) + Version(major=3, minor=2, patch=0, prerelease=None, build=None) + +* **Major, minor, and patch** + + This is the simplest and most compatible approch. Both versioning + schemes are compatible without information loss. + + .. code-block:: python + + >>> p = PyPIVersion("3.0.0") + >>> p.base_version + '3.0.0' + >>> p.release + (3, 0, 0) + >>> Version(*p.release) + Version(major=3, minor=0, patch=0, prerelease=None, build=None) + +* **With** ``pre`` **part only** + + A prerelease exists in both versioning schemes. As such, both are + a natural candidate. A prelease in PyPI version terms is the same + as a "release candidate", or "rc". + + .. code-block:: python + + >>> p = PyPIVersion("2.1.6.pre5") + >>> p.base_version + '2.1.6' + >>> p.pre + ('rc', 5) + >>> pre = "".join([str(i) for i in p.pre]) + >>> Version(*p.release, pre) + Version(major=2, minor=1, patch=6, prerelease='rc5', build=None) + +* **With only development version** + + Semver doesn't have a "development" version. + However, we could use Semver's ``build`` part: + + .. code-block:: python + + >>> p = PyPIVersion("3.0.0.dev2") + >>> p.base_version + '3.0.0' + >>> p.dev + 2 + >>> Version(*p.release, build=f"dev{p.dev}") + Version(major=3, minor=0, patch=0, prerelease=None, build='dev2') + +* **With a** ``post`` **version** + + Semver doesn't know the concept of a post version. As such, there + is currently no way to convert it reliably. + +* **Any combination** + + There is currently no way to convert a PyPI version which consists + of, for example, development *and* post parts. + + +You can use the following function to convert a PyPI version into +semver: + +.. code-block:: python + + def convert2semver(ver: packaging.version.Version) -> semver.Version: + """Converts a PyPI version into a semver version + + :param packaging.version.Version ver: the PyPI version + :return: a semver version + :raises ValueError: if epoch or post parts are used + """ + if not ver.epoch: + raise ValueError("Can't convert an epoch to semver") + if not ver.post: + raise ValueError("Can't convert a post part to semver") + + pre = None if not ver.pre else "".join([str(i) for i in ver.pre]) + semver.Version(*ver.release, prerelease=pre, build=ver.dev) + + +.. _convert_semver_to_pypi: + +From semver to PyPI +------------------- + +We distinguish between the following use cases: + + +* **Major, minor, and patch** + + .. code-block:: python + + >>> from packaging.version import Version as PyPIVersion + >>> from semver import Version + + >>> v = Version(1, 2, 3) + >>> PyPIVersion(str(v.finalize_version())) + + +* **With** ``pre`` **part only** + + .. code-block:: python + + >>> v = Version(2, 1, 4, prerelease="rc1") + >>> PyPIVersion(str(v)) + + +* **With only development version** + + .. code-block:: python + + >>> v = Version(3, 2, 8, build="dev4") + >>> PyPIVersion(f"{v.finalize_version()}{v.build}") + + +If you are unsure about the parts of the version, the following +function helps to convert the different parts: + +.. code-block:: python + + def convert2pypi(ver: semver.Version) -> packaging.version.Version: + """Converts a semver version into a version from PyPI + + A semver prerelease will be converted into a + prerelease of PyPI. + A semver build will be converted into a development + part of PyPI + :param semver.Version ver: the semver version + :return: a PyPI version + """ + v = ver.finalize_version() + prerelease = ver.prerelease if ver.prerelease else "" + build = ver.build if ver.build else "" + return PyPIVersion(f"{v}{prerelease}{build}") + + +.. _PEP 440: https://www.python.org/dev/peps/pep-0440/ diff --git a/docs/advanced/create-subclasses-from-version.rst b/docs/advanced/create-subclasses-from-version.rst new file mode 100644 index 00000000..7c97ee6f --- /dev/null +++ b/docs/advanced/create-subclasses-from-version.rst @@ -0,0 +1,33 @@ +.. _sec_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:`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: + +.. literalinclude:: semverwithvprefix.py + :language: python + :lines: 4- + + +The derived class :class:`SemVerWithVPrefix` can be used like +the original class: + +.. code-block:: python + + >>> v1 = SemVerWithVPrefix.parse("v1.2.3") + >>> assert str(v1) == "v1.2.3" + >>> print(v1) + v1.2.3 + >>> v2 = SemVerWithVPrefix.parse("v2.3.4") + >>> v2 > v1 + True + >>> bad = SemVerWithVPrefix.parse("1.2.4") + Traceback (most recent call last): + ... + ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' diff --git a/docs/advanced/deal-with-invalid-versions.rst b/docs/advanced/deal-with-invalid-versions.rst new file mode 100644 index 00000000..ee5e5704 --- /dev/null +++ b/docs/advanced/deal-with-invalid-versions.rst @@ -0,0 +1,32 @@ +.. _sec_dealing_with_invalid_versions: + +Dealing with Invalid Versions +============================= + +As semver follows the semver specification, it cannot parse version +strings which are considered "invalid" by that specification. The semver +library cannot know all the possible variations so you need to help the +library a bit. + +For example, if you have a version string ``v1.2`` would be an invalid +semver version. +However, "basic" version strings consisting of major, minor, +and patch part, can be easy to convert. The following function extract this +information and returns a tuple with two items: + +.. literalinclude:: coerce.py + :language: python + + +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. + +For example: + +.. code-block:: python + + >>> coerce("v1.2") + (Version(major=1, minor=2, patch=0, prerelease=None, build=None), '') + >>> coerce("v2.5.2-bla") + (Version(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') diff --git a/docs/advanced/display-deprecation-warnings.rst b/docs/advanced/display-deprecation-warnings.rst new file mode 100644 index 00000000..825bbe76 --- /dev/null +++ b/docs/advanced/display-deprecation-warnings.rst @@ -0,0 +1,34 @@ +.. _sec_display_deprecation_warnings: + +Displaying Deprecation Warnings +=============================== + +By default, deprecation warnings are `ignored in Python `_. +This also affects semver's own warnings. + +It is recommended that you turn on deprecation warnings in your scripts. Use one of +the following methods: + +* Use the option `-Wd `_ + to enable default warnings: + + * Directly running the Python command:: + + $ python3 -Wd scriptname.py + + * Add the option in the shebang line (something like ``#!/usr/bin/python3``) + after the command:: + + #!/usr/bin/python3 -Wd + +* In your own scripts add a filter to ensure that *all* warnings are displayed: + + .. code-block:: python + + import warnings + warnings.simplefilter("default") + # Call your semver code + + For further details, see the section + `Overriding the default filter `_ + of the Python documentation. diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst new file mode 100644 index 00000000..8a45d361 --- /dev/null +++ b/docs/advanced/index.rst @@ -0,0 +1,12 @@ +Advanced topics +=============== + + +.. toctree:: + + deal-with-invalid-versions + create-subclasses-from-version + display-deprecation-warnings + combine-pydantic-and-semver + convert-pypi-to-semver + version-from-file diff --git a/docs/semverwithvprefix.py b/docs/advanced/semverwithvprefix.py similarity index 82% rename from docs/semverwithvprefix.py rename to docs/advanced/semverwithvprefix.py index 5e375031..f2a7fecd 100644 --- a/docs/semverwithvprefix.py +++ b/docs/advanced/semverwithvprefix.py @@ -17,9 +17,8 @@ def parse(cls, version: str) -> "SemVerWithVPrefix": """ if not version[0] in ("v", "V"): raise ValueError( - "{v!r}: not a valid semantic version tag. Must start with 'v' or 'V'".format( - v=version - ) + f"{version!r}: not a valid semantic version tag. " + "Must start with 'v' or 'V'" ) return super().parse(version[1:]) diff --git a/docs/advanced/version-from-file.rst b/docs/advanced/version-from-file.rst new file mode 100644 index 00000000..6dc9bb48 --- /dev/null +++ b/docs/advanced/version-from-file.rst @@ -0,0 +1,23 @@ +.. _sec_reading_versions_from_file: + +Reading versions from file +========================== + +In cases where a version is stored inside a file, one possible solution +is to use the following function: + +.. code-block:: python + + from semver.version import Version + + def get_version(path: str = "version") -> Version: + """ + Construct a Version from a file + + :param path: A text file only containing the semantic version + :return: A :class:`Version` object containing the semantic + version from the file. + """ + with open(path,"r") as fh: + version = fh.read().strip() + return Version.parse(version) diff --git a/docs/build.rst b/docs/build.rst new file mode 100644 index 00000000..ba0c84a4 --- /dev/null +++ b/docs/build.rst @@ -0,0 +1 @@ +.. include:: ../BUILDING.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index 565b0521..e1e273b4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1 +1,3 @@ +.. _change-log: + .. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py index 52a46704..ed888361 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import codecs +from datetime import date import os import re import sys @@ -24,6 +25,7 @@ SRC_DIR = os.path.abspath("../src/") sys.path.insert(0, SRC_DIR) # from semver import __version__ # noqa: E402 +YEAR = date.today().year def read(*parts): @@ -83,7 +85,7 @@ def find_version(*file_paths): # General information about the project. project = "python-semver" -copyright = "2018, Kostiantyn Rybnikov and all" +copyright = f"{YEAR}, Kostiantyn Rybnikov and all" author = "Kostiantyn Rybnikov and all" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/index.rst b/docs/index.rst index 405d9e27..2dce2a50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,9 +9,11 @@ Semver |version| -- Semantic Versioning :caption: Contents :hidden: + build install - usage - migratetosemver3 + usage/index + migration/index + advanced/index development api @@ -31,6 +33,7 @@ Semver |version| -- Semantic Versioning changelog changelog-semver2 + Indices and Tables ================== diff --git a/docs/install.rst b/docs/install.rst index b603703c..5404882f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -18,8 +18,13 @@ This line avoids surprises. You will get any updates within the major 2 release Keep in mind, as this line avoids any major version updates, you also will never get new exciting features or bug fixes. -You can add this line in your file :file:`setup.py`, :file:`requirements.txt`, or any other -file that lists your dependencies. +Same applies for semver v3, if you want to get all updates for the semver v3 +development line, but not a major update to semver v4:: + + semver>=3,<4 + +You can add this line in your file :file:`setup.py`, :file:`requirements.txt`, +:file:`pyproject.toml`, or any other file that lists your dependencies. Pip --- @@ -28,12 +33,12 @@ Pip pip3 install semver -If you want to install this specific version (for example, 2.10.0), use the command :command:`pip` +If you want to install this specific version (for example, 3.0.0), use the command :command:`pip` with an URL and its version: .. parsed-literal:: - pip3 install git+https://github.com/python-semver/python-semver.git@2.11.0 + pip3 install git+https://github.com/python-semver/python-semver.git@3.0.0 Linux Distributions @@ -116,4 +121,4 @@ Ubuntu $ sudo apt-get install python3-semver -.. _semantic versioning: http://semver.org/ +.. _semantic versioning: https://semver.org/ diff --git a/docs/migration/index.rst b/docs/migration/index.rst new file mode 100644 index 00000000..c6af7c05 --- /dev/null +++ b/docs/migration/index.rst @@ -0,0 +1,9 @@ +Migrating to semver3 +==================== + + +.. toctree:: + :maxdepth: 1 + + migratetosemver3 + replace-deprecated-functions.rst diff --git a/docs/migratetosemver3.rst b/docs/migration/migratetosemver3.rst similarity index 93% rename from docs/migratetosemver3.rst rename to docs/migration/migratetosemver3.rst index d977bc03..f869cad3 100644 --- a/docs/migratetosemver3.rst +++ b/docs/migration/migratetosemver3.rst @@ -3,7 +3,7 @@ Migrating from semver2 to semver3 ================================= -This chapter describes the visible differences for +This document describes the visible differences for users and how your code stays compatible for semver3. Although the development team tries to make the transition @@ -11,7 +11,7 @@ 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`. +to our :ref:`change-log`. Use Version instead of VersionInfo diff --git a/docs/migration/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst new file mode 100644 index 00000000..9738001c --- /dev/null +++ b/docs/migration/replace-deprecated-functions.rst @@ -0,0 +1,110 @@ +.. _sec_replace_deprecated_functions: + +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.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. + +The following list shows the deprecated functions and how you can replace +them with code which is compatible for future versions: + + +* :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` + + Replace them with the respective methods of the :class:`Version ` + class. + For example, the function :func:`semver.bump_major` is replaced by + :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.bump_major("3.4.5") + >>> s2 = str(Version.parse("3.4.5").bump_major()) + >>> s1 == s2 + True + + Likewise with the other module level functions. + +* :func:`semver.finalize_version` + + Replace it with :func:`semver.Version.finalize_version`: + + .. code-block:: python + + >>> s1 = semver.finalize_version('1.2.3-rc.5') + >>> s2 = str(semver.Version.parse('1.2.3-rc.5').finalize_version()) + >>> s1 == s2 + True + +* :func:`semver.format_version` + + Replace it with ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') + >>> s2 = str(Version(5, 4, 3, 'pre.2', 'build.1')) + >>> s1 == s2 + True + +* :func:`semver.max_ver` + + Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])``: + + .. code-block:: python + + >>> s1 = semver.max_ver("1.2.3", "1.2.4") + >>> s2 = max("1.2.3", "1.2.4", key=Version.parse) + >>> s1 == s2 + True + +* :func:`semver.min_ver` + + Replace it with ``min(version1, version2, ...)`` or ``min([version1, version2, ...])``: + + .. code-block:: python + + >>> s1 = semver.min_ver("1.2.3", "1.2.4") + >>> s2 = min("1.2.3", "1.2.4", key=Version.parse) + >>> s1 == s2 + True + +* :func:`semver.parse` + + Replace it with :func:`semver.Version.parse` and + :func:`semver.Version.to_dict`: + + .. code-block:: python + + >>> v1 = semver.parse("1.2.3") + >>> v2 = Version.parse("1.2.3").to_dict() + >>> v1 == v2 + True + +* :func:`semver.parse_version_info` + + Replace it with :func:`semver.Version.parse`: + + .. code-block:: python + + >>> v1 = semver.parse_version_info("3.4.5") + >>> v2 = Version.parse("3.4.5") + >>> v1 == v2 + True + +* :func:`semver.replace` + + Replace it with :func:`semver.Version.replace`: + + .. code-block:: python + + >>> s1 = semver.replace("1.2.3", major=2, patch=10) + >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) + >>> s1 == s2 + True diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index f6983d17..00000000 --- a/docs/usage.rst +++ /dev/null @@ -1,789 +0,0 @@ -Using semver -============ - -The :mod:`semver` module can store a version in the :class:`~semver.version.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 -are met. - - -Getting the Implemented semver.org Version ------------------------------------------- - -The semver.org page is the authoritative specification of how semantic -versioning is defined. -To know which version of semver.org is implemented in the semver library, -use the following constant:: - - >>> semver.SEMVER_SPEC_VERSION - '2.0.0' - - -Getting the Version of semver ------------------------------ - -To know the version of semver itself, use the following construct:: - - >>> semver.__version__ - '3.0.0-dev.3' - - -Creating a Version ------------------- - -.. versionchanged:: 3.0.0 - - The former :class:`~semver.version.VersionInfo` - has been renamed to :class:`~semver.version.Version`. - -The preferred way to create a new version is with the class -:class:`~semver.version.Version`. - -.. note:: - - In the previous major release semver 2 it was possible to - create a version with module level functions. - However, module level functions are marked as *deprecated* - since version 2.x.y now. - These functions will be removed in semver 3.1.0. - For details, see the sections :ref:`sec_replace_deprecated_functions` - and :ref:`sec_display_deprecation_warnings`. - -A :class:`~semver.version.Version` instance can be created in different ways: - -* From a Unicode string:: - - >>> from semver.version import Version - >>> Version.parse("3.4.5-pre.2+build.4") - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> Version.parse(u"5.3.1") - Version(major=5, minor=3, patch=1, prerelease=None, build=None) - -* From a byte string:: - - >>> 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'} - >>> 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 integers or strings: - - >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> Version(**d) - 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 ``major`` - key, others can be omitted. You get a ``TypeError`` if your - dictionary contains invalid keys. - Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` - are allowed. - -* From a tuple:: - - >>> t = (3, 5, 6) - >>> 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``:: - - >>> Version("3", "5", 6) - Version(major=3, minor=5, patch=6, prerelease=None, build=None) - -The old, deprecated module level functions are still available but -using them are discoraged. They are available to convert old code -to semver3. - -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.version.Version` class anymore. - -Depending on your use case, the following methods are available: - -* From individual version parts into a string - - In some cases you only need a string from your version data:: - - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - -* From a string into a dictionary - - To access individual parts, you can use the function :func:`semver.parse`:: - - >>> semver.parse("3.4.5-pre.2+build.4") - OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) - - If you pass an invalid version string you will get a :py:exc:`ValueError`:: - - >>> semver.parse("1.2") - Traceback (most recent call last): - ... - ValueError: 1.2 is not valid SemVer string - - -Parsing a Version String ------------------------- - -"Parsing" in this context means to identify the different parts in a string. -Use the function :func:`Version.parse `:: - - >>> Version.parse("3.4.5-pre.2+build.4") - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - - -Checking for a Valid Semver Version ------------------------------------ - -If you need to check a string if it is a valid semver version, use the -classmethod :func:`Version.isvalid `: - -.. code-block:: python - - >>> Version.isvalid("1.0.0") - True - >>> Version.isvalid("invalid") - False - - -.. _sec.properties.parts: - -Accessing Parts of a Version Through Names ------------------------------------------- - -The :class:`~semver.version.Version` class contains attributes to access the different -parts of a version: - -.. code-block:: python - - >>> v = Version.parse("3.4.5-pre.2+build.4") - >>> v.major - 3 - >>> v.minor - 4 - >>> v.patch - 5 - >>> v.prerelease - 'pre.2' - >>> v.build - 'build.4' - -However, the attributes are read-only. You cannot change any of the above attributes. -If you do, you get an :py:exc:`AttributeError`:: - - >>> v.minor = 5 - Traceback (most recent call last): - ... - AttributeError: attribute 'minor' is readonly - -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.version.Version` instance:: - - >>> for item in Version.parse("3.4.5-pre.2+build.4"): - ... print(item) - 3 - 4 - 5 - pre.2 - build.4 - >>> list(Version.parse("3.4.5-pre.2+build.4")) - [3, 4, 5, 'pre.2', 'build.4'] - - -.. _sec.getitem.parts: - -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:`~semver.version.Version` object allows to access its data through -the magic method :func:`~semver.version.Version.__getitem__`. - -For example, the ``major`` part can be accessed by index number 0 (zero). -Likewise the other parts: - -.. code-block:: python - - >>> ver = 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') - -If you need more than one part at the same time, use the slice notation: - -.. code-block:: python - - >>> ver[0:3] - (10, 3, 2) - -Or, as an alternative, you can pass a :func:`slice` object: - -.. code-block:: python - - >>> sl = slice(0,3) - >>> ver[sl] - (10, 3, 2) - -Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: - -.. code-block:: python - - >>> ver = Version.parse("10.3.2") - >>> ver[3] - Traceback (most recent call last): - ... - IndexError: Version part undefined - >>> ver[-2] - Traceback (most recent call last): - ... - IndexError: Version index cannot be negative - -.. _sec.replace.parts: - -Replacing Parts of a Version ----------------------------- - -If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`replace `: - -* From a :class:`Version ` instance:: - - >>> version = semver.Version.parse("1.4.5-pre.1+build.6") - >>> version.replace(major=2, minor=2) - Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') - -* From a version string:: - - >>> semver.replace("1.4.5-pre.1+build.6", major=2) - '2.4.5-pre.1+build.6' - -If you pass invalid keys you get an exception:: - - >>> semver.replace("1.2.3", invalidkey=2) - Traceback (most recent call last): - ... - TypeError: replace() got 1 unexpected keyword argument(s): invalidkey - >>> version = semver.Version.parse("1.4.5-pre.1+build.6") - >>> version.replace(invalidkey=2) - Traceback (most recent call last): - ... - TypeError: replace() got 1 unexpected keyword argument(s): invalidkey - - -.. _sec.convert.versions: - -Converting a Version instance into Different Types ------------------------------------------------------- - -Sometimes it is needed to convert a :class:`Version ` instance into -a different type. For example, for displaying or to access all parts. - -It is possible to convert a :class:`Version ` instance: - -* Into a string with the builtin function :func:`str`:: - - >>> str(Version.parse("3.4.5-pre.2+build.4")) - '3.4.5-pre.2+build.4' - -* Into a dictionary with :func:`to_dict `:: - - >>> v = 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:`to_tuple `:: - - >>> v = Version(major=5, minor=4, patch=2) - >>> v.to_tuple() - (5, 4, 2, None, None) - - -Raising Parts of a Version --------------------------- - -The ``semver`` module contains the following functions to raise parts of -a version: - -* :func:`Version.bump_major `: raises the major part and set all other parts to - zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. - Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and - ``build`` to ``None``. -* :func:`Version.bump_prerelease `: raises the prerelease part and set - ``build`` to ``None``. -* :func:`Version.bump_build `: raises the build part. - -.. code-block:: python - - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) - '4.0.0' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_minor()) - '3.5.0' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_patch()) - '3.4.6' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) - '3.4.5-pre.3' - >>> str(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`. - - -Increasing Parts of a Version Taking into Account Prereleases -------------------------------------------------------------- - -.. versionadded:: 2.10.0 - Added :func:`Version.next_version `. - -If you want to raise your version and take prereleases into account, -the function :func:`next_version ` -would perhaps a better fit. - - -.. code-block:: python - - >>> v = Version.parse("3.4.5-pre.2+build.4") - >>> str(v.next_version(part="prerelease")) - '3.4.5-pre.3' - >>> str(Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) - '3.4.5' - >>> str(Version.parse("3.4.5+build.4").next_version(part="patch")) - '3.4.5' - >>> str(Version.parse("0.1.4").next_version("prerelease")) - '0.1.5-rc.1' - - -Comparing Versions ------------------- - -To compare two versions depends on your type: - -* **Two strings** - - Use :func:`semver.compare`:: - - >>> 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 - - The return value is negative if ``version1 < version2``, zero if - ``version1 == version2`` and strictly positive if ``version1 > version2``. - -* **Two** :class:`Version ` **instances** - - Use the specific operator. Currently, the operators ``<``, - ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - - >>> v1 = Version.parse("3.4.5") - >>> v2 = Version.parse("3.5.1") - >>> v1 < v2 - True - >>> v1 > v2 - False - -* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` - - Use the operator as with two :class:`Version ` types:: - - >>> v = Version.parse("3.4.5") - >>> v > (1, 0) - True - >>> v < [3, 5] - True - - The opposite does also work:: - - >>> (1, 0) < v - True - >>> [3, 5] > v - True - -* **A** :class:`Version ` **type and a** :func:`str` - - You can use also raw strings to compare:: - - >>> v > "1.0.0" - True - >>> v < "3.5.0" - True - - The opposite does also work:: - - >>> "1.0.0" < v - True - >>> "3.5.0" > v - True - - However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: - - >>> v > "1.0" - Traceback (most recent call last): - ... - ValueError: 1.0 is not valid SemVer string - -* **A** :class:`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):: - - >>> v > dict(major=1) - True - - The opposite does also work:: - - >>> dict(major=1) < v - True - - If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: - - >>> v > dict(major=1, unknown=42) - Traceback (most recent call last): - ... - TypeError: ... got an unexpected keyword argument 'unknown' - - -Other types cannot be compared. - -If you need to convert some types into others, refer to :ref:`sec.convert.versions`. - -The use of these comparison operators also implies that you can use builtin -functions that leverage this capability; builtins including, but not limited to: :func:`max`, :func:`min` -(for examples, see :ref:`sec_max_min`) and :func:`sorted`. - - -Determining Version Equality ----------------------------- - -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 = Version.parse("1.2.3-rc4+1e4664d") - >>> v == "1.2.3-rc4+dedbeef" - True - -This also applies when a :class:`Version ` is a member of a set, or a -dictionary key:: - - >>> d = {} - >>> v1 = Version.parse("1.2.3-rc4+1e4664d") - >>> v2 = Version.parse("1.2.3-rc4+dedbeef") - >>> d[v1] = 1 - >>> d[v2] - 1 - >>> s = set() - >>> s.add(v1) - >>> v2 in s - True - - - -Comparing Versions through an Expression ----------------------------------------- - -If you need a more fine-grained approach of comparing two versions, -use the :func:`semver.match` function. It expects two arguments: - -1. a version string -2. a match expression - -Currently, the match expression supports the following operators: - -* ``<`` smaller than -* ``>`` greater than -* ``>=`` greater or equal than -* ``<=`` smaller or equal than -* ``==`` equal -* ``!=`` not equal - -That gives you the following possibilities to express your condition: - -.. code-block:: python - - >>> semver.match("2.0.0", ">=1.0.0") - True - >>> semver.match("1.0.0", ">1.0.0") - False - -.. _sec_max_min: - -Getting Minimum and Maximum of Multiple Versions ------------------------------------------------- -.. versionchanged:: 2.10.2 - 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:`Version ` implements -:func:`__gt__ ` and -:func:`__lt__ `, it can be used with builtins requiring: - -.. code-block:: python - - >>> max([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) - Version(major=0, minor=2, patch=0, prerelease=None, build=None) - >>> min([Version(0, 1, 0), Version(0, 2, 0), 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:`Version `). - -For example, here are the maximum and minimum versions of a list of version strings: - -.. code-block:: python - - >>> max(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) - '2.1.0' - >>> min(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) - '0.4.99' - -And the same can be done with tuples: - -.. code-block:: python - - >>> max(map(lambda v: 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: 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`. - -The "old way" with :func:`semver.max_ver` or :func:`semver.min_ver` is still available, but not recommended: - -.. code-block:: python - - >>> semver.max_ver("1.0.0", "2.0.0") - '2.0.0' - >>> semver.min_ver("1.0.0", "2.0.0") - '1.0.0' - - -.. _sec_dealing_with_invalid_versions: - -Dealing with Invalid Versions ------------------------------ - -As semver follows the semver specification, it cannot parse version -strings which are considered "invalid" by that specification. The semver -library cannot know all the possible variations so you need to help the -library a bit. - -For example, if you have a version string ``v1.2`` would be an invalid -semver version. -However, "basic" version strings consisting of major, minor, -and patch part, can be easy to convert. The following function extract this -information and returns a tuple with two items: - -.. literalinclude:: coerce.py - :language: python - - -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. - -For example: - -.. code-block:: python - - >>> coerce("v1.2") - (Version(major=1, minor=2, patch=0, prerelease=None, build=None), '') - >>> coerce("v2.5.2-bla") - (Version(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') - - -.. _sec_replace_deprecated_functions: - -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.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. - -The following list shows the deprecated functions and how you can replace -them with code which is compatible for future versions: - - -* :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` - - Replace them with the respective methods of the :class:`Version ` - class. - For example, the function :func:`semver.bump_major` is replaced by - :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: - - .. code-block:: python - - >>> s1 = semver.bump_major("3.4.5") - >>> s2 = str(Version.parse("3.4.5").bump_major()) - >>> s1 == s2 - True - - Likewise with the other module level functions. - -* :func:`semver.finalize_version` - - Replace it with :func:`semver.Version.finalize_version`: - - .. code-block:: python - - >>> s1 = semver.finalize_version('1.2.3-rc.5') - >>> s2 = str(semver.Version.parse('1.2.3-rc.5').finalize_version()) - >>> s1 == s2 - True - -* :func:`semver.format_version` - - Replace it with ``str(versionobject)``: - - .. code-block:: python - - >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') - >>> s2 = str(Version(5, 4, 3, 'pre.2', 'build.1')) - >>> s1 == s2 - True - -* :func:`semver.max_ver` - - Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])``: - - .. code-block:: python - - >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) - >>> s1 == s2 - True - -* :func:`semver.min_ver` - - Replace it with ``min(version1, version2, ...)`` or ``min([version1, version2, ...])``: - - .. code-block:: python - - >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) - >>> s1 == s2 - True - -* :func:`semver.parse` - - Replace it with :func:`semver.Version.parse` and - :func:`semver.Version.to_dict`: - - .. code-block:: python - - >>> v1 = semver.parse("1.2.3") - >>> v2 = Version.parse("1.2.3").to_dict() - >>> v1 == v2 - True - -* :func:`semver.parse_version_info` - - Replace it with :func:`semver.Version.parse`: - - .. code-block:: python - - >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = Version.parse("3.4.5") - >>> v1 == v2 - True - -* :func:`semver.replace` - - Replace it with :func:`semver.Version.replace`: - - .. code-block:: python - - >>> s1 = semver.replace("1.2.3", major=2, patch=10) - >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) - >>> s1 == s2 - True - - -.. _sec_display_deprecation_warnings: - -Displaying Deprecation Warnings -------------------------------- - -By default, deprecation warnings are `ignored in Python `_. -This also affects semver's own warnings. - -It is recommended that you turn on deprecation warnings in your scripts. Use one of -the following methods: - -* Use the option `-Wd `_ - to enable default warnings: - - * Directly running the Python command:: - - $ python3 -Wd scriptname.py - - * Add the option in the shebang line (something like ``#!/usr/bin/python3``) - after the command:: - - #!/usr/bin/python3 -Wd - -* In your own scripts add a filter to ensure that *all* warnings are displayed: - - .. code-block:: python - - import warnings - warnings.simplefilter("default") - # Call your semver code - - For further details, see the section - `Overriding the default filter `_ - of the Python documentation. - - -.. _sec_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:`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: - -.. literalinclude:: semverwithvprefix.py - :language: python - :lines: 4- - - -The derived class :class:`SemVerWithVPrefix` can be used like -the original class: - -.. code-block:: python - - >>> v1 = SemVerWithVPrefix.parse("v1.2.3") - >>> assert str(v1) == "v1.2.3" - >>> print(v1) - v1.2.3 - >>> v2 = SemVerWithVPrefix.parse("v2.3.4") - >>> v2 > v1 - True - >>> bad = SemVerWithVPrefix.parse("1.2.4") - Traceback (most recent call last): - ... - ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' diff --git a/docs/usage/access-parts-of-a-version.rst b/docs/usage/access-parts-of-a-version.rst new file mode 100644 index 00000000..4eb9274f --- /dev/null +++ b/docs/usage/access-parts-of-a-version.rst @@ -0,0 +1,43 @@ +.. _sec.properties.parts: + +Accessing Parts of a Version Through Names +========================================== + +The :class:`~semver.version.Version` class contains attributes to access the different +parts of a version: + +.. code-block:: python + + >>> v = Version.parse("3.4.5-pre.2+build.4") + >>> v.major + 3 + >>> v.minor + 4 + >>> v.patch + 5 + >>> v.prerelease + 'pre.2' + >>> v.build + 'build.4' + +However, the attributes are read-only. You cannot change any of the above attributes. +If you do, you get an :py:exc:`AttributeError`:: + + >>> v.minor = 5 + Traceback (most recent call last): + ... + AttributeError: attribute 'minor' is readonly + +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.version.Version` instance:: + + >>> for item in Version.parse("3.4.5-pre.2+build.4"): + ... print(item) + 3 + 4 + 5 + pre.2 + build.4 + >>> list(Version.parse("3.4.5-pre.2+build.4")) + [3, 4, 5, 'pre.2', 'build.4'] diff --git a/docs/usage/access-parts-through-index.rst b/docs/usage/access-parts-through-index.rst new file mode 100644 index 00000000..a261fda4 --- /dev/null +++ b/docs/usage/access-parts-through-index.rst @@ -0,0 +1,48 @@ +.. _sec.getitem.parts: + +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:`~semver.version.Version` object allows to access its data through +the magic method :func:`~semver.version.Version.__getitem__`. + +For example, the ``major`` part can be accessed by index number 0 (zero). +Likewise the other parts: + +.. code-block:: python + + >>> ver = 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') + +If you need more than one part at the same time, use the slice notation: + +.. code-block:: python + + >>> ver[0:3] + (10, 3, 2) + +Or, as an alternative, you can pass a :func:`slice` object: + +.. code-block:: python + + >>> sl = slice(0,3) + >>> ver[sl] + (10, 3, 2) + +Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: + +.. code-block:: python + + >>> ver = Version.parse("10.3.2") + >>> ver[3] + Traceback (most recent call last): + ... + IndexError: Version part undefined + >>> ver[-2] + Traceback (most recent call last): + ... + IndexError: Version index cannot be negative diff --git a/docs/usage/check-valid-semver-version.rst b/docs/usage/check-valid-semver-version.rst new file mode 100644 index 00000000..7aa9615b --- /dev/null +++ b/docs/usage/check-valid-semver-version.rst @@ -0,0 +1,12 @@ +Checking for a Valid Semver Version +=================================== + +If you need to check a string if it is a valid semver version, use the +classmethod :func:`Version.isvalid `: + +.. code-block:: python + + >>> Version.isvalid("1.0.0") + True + >>> Version.isvalid("invalid") + False diff --git a/docs/usage/compare-versions-through-expression.rst b/docs/usage/compare-versions-through-expression.rst new file mode 100644 index 00000000..28fad671 --- /dev/null +++ b/docs/usage/compare-versions-through-expression.rst @@ -0,0 +1,39 @@ +Comparing Versions through an Expression +======================================== + +If you need a more fine-grained approach of comparing two versions, +use the :func:`semver.match` function. It expects two arguments: + +1. a version string +2. a match expression + +Currently, the match expression supports the following operators: + +* ``<`` smaller than +* ``>`` greater than +* ``>=`` greater or equal than +* ``<=`` smaller or equal than +* ``==`` equal +* ``!=`` not equal + +That gives you the following possibilities to express your condition: + +.. code-block:: python + + >>> semver.match("2.0.0", ">=1.0.0") + True + >>> semver.match("1.0.0", ">1.0.0") + False + +If no operator is specified, the match expression is interpreted as a +version to be compared for equality. This allows handling the common +case of version compatibility checking through either an exact version +or a match expression very easy to implement, as the same code will +handle both cases: + +.. code-block:: python + + >>> semver.match("2.0.0", "2.0.0") + True + >>> semver.match("1.0.0", "3.5.1") + False diff --git a/docs/usage/compare-versions.rst b/docs/usage/compare-versions.rst new file mode 100644 index 00000000..b42ba1a7 --- /dev/null +++ b/docs/usage/compare-versions.rst @@ -0,0 +1,99 @@ +Comparing Versions +================== + +To compare two versions depends on your type: + +* **Two strings** + + Use :func:`semver.compare`:: + + >>> 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 + + The return value is negative if ``version1 < version2``, zero if + ``version1 == version2`` and strictly positive if ``version1 > version2``. + +* **Two** :class:`Version ` **instances** + + Use the specific operator. Currently, the operators ``<``, + ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: + + >>> v1 = Version.parse("3.4.5") + >>> v2 = Version.parse("3.5.1") + >>> v1 < v2 + True + >>> v1 > v2 + False + +* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` + + Use the operator as with two :class:`Version ` types:: + + >>> v = Version.parse("3.4.5") + >>> v > (1, 0) + True + >>> v < [3, 5] + True + + The opposite does also work:: + + >>> (1, 0) < v + True + >>> [3, 5] > v + True + +* **A** :class:`Version ` **type and a** :func:`str` + + You can use also raw strings to compare:: + + >>> v > "1.0.0" + True + >>> v < "3.5.0" + True + + The opposite does also work:: + + >>> "1.0.0" < v + True + >>> "3.5.0" > v + True + + However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: + + >>> v > "1.0" + Traceback (most recent call last): + ... + ValueError: 1.0 is not valid SemVer string + +* **A** :class:`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):: + + >>> v > dict(major=1) + True + + The opposite does also work:: + + >>> dict(major=1) < v + True + + If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: + + >>> v > dict(major=1, unknown=42) + Traceback (most recent call last): + ... + TypeError: ... got an unexpected keyword argument 'unknown' + + +Other types cannot be compared. + +If you need to convert some types into others, refer to :ref:`sec.convert.versions`. + +The use of these comparison operators also implies that you can use builtin +functions that leverage this capability; builtins including, but not limited to: :func:`max`, :func:`min` +(for examples, see :ref:`sec_max_min`) and :func:`sorted`. diff --git a/docs/usage/convert-version-into-different-types.rst b/docs/usage/convert-version-into-different-types.rst new file mode 100644 index 00000000..976283d8 --- /dev/null +++ b/docs/usage/convert-version-into-different-types.rst @@ -0,0 +1,26 @@ +.. _sec.convert.versions: + +Converting a Version instance into Different Types +================================================== + +Sometimes it is needed to convert a :class:`Version ` instance into +a different type. For example, for displaying or to access all parts. + +It is possible to convert a :class:`Version ` instance: + +* Into a string with the builtin function :func:`str`:: + + >>> str(Version.parse("3.4.5-pre.2+build.4")) + '3.4.5-pre.2+build.4' + +* Into a dictionary with :func:`to_dict `:: + + >>> v = 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:`to_tuple `:: + + >>> v = Version(major=5, minor=4, patch=2) + >>> v.to_tuple() + (5, 4, 2, None, None) diff --git a/docs/usage/create-a-version.rst b/docs/usage/create-a-version.rst new file mode 100644 index 00000000..3acb4c03 --- /dev/null +++ b/docs/usage/create-a-version.rst @@ -0,0 +1,100 @@ +Creating a Version +================== + +.. versionchanged:: 3.0.0 + + The former :class:`~semver.version.VersionInfo` + has been renamed to :class:`~semver.version.Version`. + +The preferred way to create a new version is with the class +:class:`~semver.version.Version`. + +.. note:: + + In the previous major release semver 2 it was possible to + create a version with module level functions. + However, module level functions are marked as *deprecated* + since version 2.x.y now. + These functions will be removed in semver 3.1.0. + For details, see the sections :ref:`sec_replace_deprecated_functions` + and :ref:`sec_display_deprecation_warnings`. + +A :class:`~semver.version.Version` instance can be created in different ways: + +* From a Unicode string:: + + >>> from semver.version import Version + >>> Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> Version.parse(u"5.3.1") + Version(major=5, minor=3, patch=1, prerelease=None, build=None) + +* From a byte string:: + + >>> 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'} + >>> 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 integers or strings: + + >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> Version(**d) + 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 ``major`` + key, others can be omitted. You get a ``TypeError`` if your + dictionary contains invalid keys. + Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` + are allowed. + +* From a tuple:: + + >>> t = (3, 5, 6) + >>> 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``:: + + >>> Version("3", "5", 6) + Version(major=3, minor=5, patch=6, prerelease=None, build=None) + +The old, deprecated module level functions are still available but +using them are discoraged. They are available to convert old code +to semver3. + +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.version.Version` class anymore. + +Depending on your use case, the following methods are available: + +* From individual version parts into a string + + In some cases you only need a string from your version data:: + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + +* From a string into a dictionary + + To access individual parts, you can use the function :func:`semver.parse`:: + + >>> semver.parse("3.4.5-pre.2+build.4") + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) + + If you pass an invalid version string you will get a :py:exc:`ValueError`:: + + >>> semver.parse("1.2") + Traceback (most recent call last): + ... + ValueError: 1.2 is not valid SemVer string diff --git a/docs/usage/determine-version-equality.rst b/docs/usage/determine-version-equality.rst new file mode 100644 index 00000000..211743c9 --- /dev/null +++ b/docs/usage/determine-version-equality.rst @@ -0,0 +1,25 @@ +Determining Version Equality +============================ + +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 = Version.parse("1.2.3-rc4+1e4664d") + >>> v == "1.2.3-rc4+dedbeef" + True + +This also applies when a :class:`Version ` is a member of a set, or a +dictionary key:: + + >>> d = {} + >>> v1 = Version.parse("1.2.3-rc4+1e4664d") + >>> v2 = Version.parse("1.2.3-rc4+dedbeef") + >>> d[v1] = 1 + >>> d[v2] + 1 + >>> s = set() + >>> s.add(v1) + >>> v2 in s + True + diff --git a/docs/usage/get-min-and-max-of-multiple-versions.rst b/docs/usage/get-min-and-max-of-multiple-versions.rst new file mode 100644 index 00000000..266ee50b --- /dev/null +++ b/docs/usage/get-min-and-max-of-multiple-versions.rst @@ -0,0 +1,51 @@ +.. _sec_max_min: + +Getting Minimum and Maximum of Multiple Versions +================================================ + +.. versionchanged:: 2.10.2 + 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:`Version ` implements +:func:`__gt__ ` and +:func:`__lt__ `, it can be used with builtins requiring: + +.. code-block:: python + + >>> max([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) + Version(major=0, minor=2, patch=0, prerelease=None, build=None) + >>> min([Version(0, 1, 0), Version(0, 2, 0), 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:`Version `). + +For example, here are the maximum and minimum versions of a list of version strings: + +.. code-block:: python + + >>> max(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) + '2.1.0' + >>> min(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) + '0.4.99' + +And the same can be done with tuples: + +.. code-block:: python + + >>> max(map(lambda v: 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: 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`. + +The "old way" with :func:`semver.max_ver` or :func:`semver.min_ver` is still available, but not recommended: + +.. code-block:: python + + >>> semver.max_ver("1.0.0", "2.0.0") + '2.0.0' + >>> semver.min_ver("1.0.0", "2.0.0") + '1.0.0' diff --git a/docs/usage/increase-parts-of-a-version_prereleases.rst b/docs/usage/increase-parts-of-a-version_prereleases.rst new file mode 100644 index 00000000..98283937 --- /dev/null +++ b/docs/usage/increase-parts-of-a-version_prereleases.rst @@ -0,0 +1,22 @@ +Increasing Parts of a Version Taking into Account Prereleases +============================================================= + +.. versionadded:: 2.10.0 + Added :func:`Version.next_version `. + +If you want to raise your version and take prereleases into account, +the function :func:`next_version ` +would perhaps a better fit. + + +.. code-block:: python + + >>> v = Version.parse("3.4.5-pre.2+build.4") + >>> str(v.next_version(part="prerelease")) + '3.4.5-pre.3' + >>> str(Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + '3.4.5' + >>> str(Version.parse("3.4.5+build.4").next_version(part="patch")) + '3.4.5' + >>> str(Version.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' diff --git a/docs/usage/index.rst b/docs/usage/index.rst new file mode 100644 index 00000000..ddfc2284 --- /dev/null +++ b/docs/usage/index.rst @@ -0,0 +1,20 @@ +Using semver +============ + +.. toctree:: + + semver_org-version + semver-version + create-a-version + parse-version-string + check-valid-semver-version + access-parts-of-a-version + access-parts-through-index + replace-parts-of-a-version + convert-version-into-different-types + raise-parts-of-a-version + increase-parts-of-a-version_prereleases + compare-versions + determine-version-equality + compare-versions-through-expression + get-min-and-max-of-multiple-versions diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst new file mode 100644 index 00000000..0a39c8a3 --- /dev/null +++ b/docs/usage/parse-version-string.rst @@ -0,0 +1,15 @@ +Parsing a Version String +======================== + +"Parsing" in this context means to identify the different parts in a string. +Use the function :func:`Version.parse `:: + + >>> Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + +Set the parameter ``optional_minor_and_patch=True`` to allow optional +minor and patch parts. Optional parts are set to zero. By default (False), the +version string to parse has to follow the semver specification:: + + >>> Version.parse("1.2", optional_minor_and_patch=True) + Version(major=1, minor=2, patch=0, prerelease=None, build=None) diff --git a/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst new file mode 100644 index 00000000..cc62fffb --- /dev/null +++ b/docs/usage/raise-parts-of-a-version.rst @@ -0,0 +1,30 @@ +Raising Parts of a Version +========================== + +The ``semver`` module contains the following functions to raise parts of +a version: + +* :func:`Version.bump_major `: raises the major part and set all other parts to + zero. Set ``prerelease`` and ``build`` to ``None``. +* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. + Set ``prerelease`` and ``build`` to ``None``. +* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and + ``build`` to ``None``. +* :func:`Version.bump_prerelease `: raises the prerelease part and set + ``build`` to ``None``. +* :func:`Version.bump_build `: raises the build part. + +.. code-block:: python + + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) + '4.0.0' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_minor()) + '3.5.0' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_patch()) + '3.4.6' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) + '3.4.5-pre.3' + >>> str(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`. diff --git a/docs/usage/replace-parts-of-a-version.rst b/docs/usage/replace-parts-of-a-version.rst new file mode 100644 index 00000000..b6c38865 --- /dev/null +++ b/docs/usage/replace-parts-of-a-version.rst @@ -0,0 +1,30 @@ +.. _sec.replace.parts: + +Replacing Parts of a Version +============================ + +If you want to replace different parts of a version, but leave other parts +unmodified, use the function :func:`replace `: + +* From a :class:`Version ` instance:: + + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") + >>> version.replace(major=2, minor=2) + Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') + +* From a version string:: + + >>> semver.replace("1.4.5-pre.1+build.6", major=2) + '2.4.5-pre.1+build.6' + +If you pass invalid keys you get an exception:: + + >>> semver.replace("1.2.3", invalidkey=2) + Traceback (most recent call last): + ... + TypeError: replace() got 1 unexpected keyword argument(s): invalidkey + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") + >>> version.replace(invalidkey=2) + Traceback (most recent call last): + ... + TypeError: replace() got 1 unexpected keyword argument(s): invalidkey diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst new file mode 100644 index 00000000..8eeab62f --- /dev/null +++ b/docs/usage/semver-version.rst @@ -0,0 +1,7 @@ +Getting the Version of semver +============================= + +To know the version of semver itself, use the following construct:: + + >>> semver.__version__ + '3.0.0-dev.4' diff --git a/docs/usage/semver_org-version.rst b/docs/usage/semver_org-version.rst new file mode 100644 index 00000000..b0a1ad87 --- /dev/null +++ b/docs/usage/semver_org-version.rst @@ -0,0 +1,10 @@ +Getting the Implemented semver.org Version +========================================== + +The semver.org page is the authoritative specification of how semantic +versioning is defined. +To know which version of semver.org is implemented in the semver library, +use the following constant:: + + >>> semver.SEMVER_SPEC_VERSION + '2.0.0' diff --git a/pyproject.toml b/pyproject.toml index 769b13d7..d288e68e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,31 @@ +# +# +# See also https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html +# +# General idea taken from +# https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/ + [build-system] requires = [ # sync with setup.py until we discard non-pep-517/518 - "setuptools>=40.0", + "setuptools", "setuptools-scm", "wheel", + "build", ] build-backend = "setuptools.build_meta" + + [tool.black] line-length = 88 -target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] # diff = true -extend-exclude = ''' -# A regex preceded with ^/ will apply only to files and directories -# in the root of the project. -^/*.py -''' -include = ''' -^/setup.py -''' + [tool.towncrier] package = "semver" -# package_dir = "src" +package_dir = "src" filename = "CHANGELOG.rst" directory = "changelog.d/" title_format = "Version {version}" @@ -30,42 +33,24 @@ template = "changelog.d/_template.rst" # issue_format = "`#{issue} `_" # issue_format = ":gh:`{issue}`" - # [[tool.towncrier.type]] - # directory = "breaking" - # name = "Breaking Changes" - # showcontent = true - [[tool.towncrier.type]] - directory = "deprecation" - name = "Deprecations" - showcontent = true +[tool.towncrier.fragment.breaking] +name = "Breaking Changes" - [[tool.towncrier.type]] - directory = "feature" - name = "Features" - showcontent = true +[tool.towncrier.fragment.bugfix] +name = "Bug Fixes" - # [[tool.towncrier.type]] - # directory = "improvement" - # name = "Improvements" - # showcontent = true +[tool.towncrier.fragment.deprecation] +name = "Deprecations" - [[tool.towncrier.type]] - directory = "bugfix" - name = "Bug Fixes" - showcontent = true +[tool.towncrier.fragment.doc] +name = "Improved Documentation" - [[tool.towncrier.type]] - directory = "doc" - name = "Improved Documentation" - showcontent = true +[tool.towncrier.fragment.feature] +name = "Features" - [[tool.towncrier.type]] - directory = "trivial" - name = "Trivial/Internal Changes" - showcontent = true +[tool.towncrier.fragment.removal] +name = "Removals" - [[tool.towncrier.type]] - directory = "removal" - name = "Removals" - showcontent = true +[tool.towncrier.fragment.trivial] +name = "Trivial/Internal Changes" diff --git a/release-procedure.md b/release-procedure.md index 251f23f0..d6c1701e 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -11,7 +11,8 @@ create a new release. * that all pull requests that should be included in this release are merged: . - * that continuous integration for latest build was passing: . + * that continuous integration for latest build was passing: + . 1. Create a new branch `release/`. @@ -81,6 +82,8 @@ create a new release. 1. Check if everything is okay with the wheel. Check also the web site `https://test.pypi.org/project//` +1. If everything looks fine, merge the pull request. + 1. Upload to PyPI: ```bash @@ -91,16 +94,24 @@ create a new release. 1. Go to https://pypi.org/project/semver/ to verify that new version is online and the page is rendered correctly. -1. Tag commit and push to GitHub using command line interface: +# Finish the release - ```bash - $ git tag -a x.x.x -m 'Version x.x.x' - $ git push python-semver master --tags - ``` +1. Create a tag: + + $ git tag -a x.x.x + + It's recommended to use the generated Tox output + from the Changelog. + +1. Push the tag: + + $ git push --tags 1. In [GitHub Release page](https://github.com/python-semver/python-semver/release) document the new release. - Usually it's enough to take it from a commit message or the tag description. + Select the tag from the last step and copy the + content of the tag description into the release + description. 1. Announce it in . diff --git a/setup.cfg b/setup.cfg index de2d226c..87ec3b8f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ [metadata] name = semver version = attr: semver.__about__.__version__ -description = Python helper for Semantic Versioning (http://semver.org) +description = Python helper for Semantic Versioning (https://semver.org) long_description = file: README.rst long_description_content_type = text/x-rst author = Kostiantyn Rybnikov @@ -26,11 +26,12 @@ classifiers = 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 :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Software Development :: Libraries :: Python Modules license = BSD @@ -38,7 +39,7 @@ license = BSD package_dir = =src packages = find: -python_requires = >=3.6.* +python_requires = >=3.7.* include_package_data = True [options.entry_points] @@ -54,9 +55,12 @@ semver = py.typed [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs +# pythonpath = src filterwarnings = ignore:Function 'semver.*:DeprecationWarning + # ' <- This apostroph is just to fix syntax highlighting addopts = + # --import-mode=importlib --no-cov-on-fail --cov=semver --cov-report=term-missing @@ -67,18 +71,15 @@ addopts = [flake8] max-line-length = 88 ignore = F821,W503 -exclude = - src/semver/__init__.py - .env - venv +extend-exclude = .eggs - .tox - .git - __pycache__ + .env build - dist docs + venv conftest.py + src/semver/__init__.py + tasks.py [pycodestyle] count = False diff --git a/setup.py b/setup.py deleted file mode 100644 index 88990ad8..00000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python3 -import setuptools - -setuptools.setup() # For compatibility with python 3.6 diff --git a/src/semver/__about__.py b/src/semver/__about__.py index fa448ebe..0f7150bf 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.0-dev.3" +__version__ = "3.0.0-dev.4" #: Original semver author __author__ = "Kostiantyn Rybnikov" @@ -31,7 +31,7 @@ __maintainer_email__ = "s.celles@gmail.com" #: Short description about semver -__description__ = "Python helper for Semantic Versioning (http://semver.org)" +__description__ = "Python helper for Semantic Versioning (https://semver.org)" #: Supported semver specification SEMVER_SPEC_VERSION = "2.0.0" diff --git a/src/semver/__main__.py b/src/semver/__main__.py index 7fde54d7..a6d448aa 100644 --- a/src/semver/__main__.py +++ b/src/semver/__main__.py @@ -11,12 +11,12 @@ """ import os.path import sys -from typing import List +from typing import List, Optional from semver import cli -def main(cliargs: List[str] = None) -> int: +def main(cliargs: Optional[List[str]] = None) -> int: if __package__ == "": path = os.path.dirname(os.path.dirname(__file__)) sys.path[0:0] = [path] diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 61ceae12..5f51c8f3 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -7,7 +7,7 @@ import warnings from functools import partial, wraps from types import FrameType -from typing import Type, Callable, cast +from typing import Type, Callable, Optional, cast from . import cli from .version import Version @@ -15,9 +15,9 @@ def deprecated( - func: F = None, - replace: str = None, - version: str = None, + func: Optional[F] = None, + replace: Optional[str] = None, + version: Optional[str] = None, category: Type[Warning] = DeprecationWarning, ) -> Decorator: """ diff --git a/src/semver/cli.py b/src/semver/cli.py index 65ca5187..3c573d63 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -12,7 +12,7 @@ import argparse import sys -from typing import cast, List +from typing import cast, List, Optional from .version import Version from .__about__ import __version__ @@ -152,7 +152,7 @@ def process(args: argparse.Namespace) -> str: return args.func(args) -def main(cliargs: List[str] = None) -> int: +def main(cliargs: Optional[List[str]] = None) -> int: """ Entry point for the application script. diff --git a/src/semver/version.py b/src/semver/version.py index 9e02544f..96281192 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -68,15 +68,18 @@ class Version: __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""" + #: Regex template for a semver version + _REGEX_TEMPLATE = r""" ^ (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + ){opt_patch} + ){opt_minor} (?:-(?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-]*))* @@ -86,7 +89,15 @@ class Version: (?:\.[0-9a-zA-Z-]+)* ))? $ - """, + """ + #: Regex for a semver version + _REGEX = re.compile( + _REGEX_TEMPLATE.format(opt_patch="", opt_minor=""), + re.VERBOSE, + ) + #: Regex for a semver version that might be shorter + _REGEX_OPTIONAL_MINOR_AND_PATCH = re.compile( + _REGEX_TEMPLATE.format(opt_patch="?", opt_minor="?"), re.VERBOSE, ) @@ -95,8 +106,8 @@ def __init__( major: SupportsInt, minor: SupportsInt = 0, patch: SupportsInt = 0, - prerelease: Union[String, int] = None, - build: Union[String, int] = None, + prerelease: Optional[Union[String, int]] = None, + build: Optional[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)} @@ -510,7 +521,7 @@ def match(self, match_expr: str) -> bool: """ Compare self to match a match expression. - :param match_expr: operator and version; valid operators are + :param match_expr: optional operator and version; valid operators are ``<``` smaller than ``>`` greater than ``>=`` greator or equal than @@ -523,6 +534,8 @@ def match(self, match_expr: str) -> bool: True >>> semver.Version.parse("1.0.0").match(">1.0.0") False + >>> semver.Version.parse("4.0.4").match("4.0.4") + True """ prefix = match_expr[:2] if prefix in (">=", "<=", "==", "!="): @@ -530,6 +543,9 @@ def match(self, match_expr: str) -> bool: elif prefix and prefix[0] in (">", "<"): prefix = prefix[0] match_version = match_expr[1:] + elif match_expr and match_expr[0] in "0123456789": + prefix = "==" + match_version = match_expr else: raise ValueError( "match_expr parameter should be in format , " @@ -553,15 +569,24 @@ def match(self, match_expr: str) -> bool: return cmp_res in possibilities @classmethod - def parse(cls, version: String) -> "Version": + def parse( + cls, version: String, optional_minor_and_patch: bool = False + ) -> "Version": """ Parse version string to a Version instance. .. versionchanged:: 2.11.0 Changed method from static to classmethod to allow subclasses. + .. versionchanged:: 3.0.0 + Added optional parameter optional_minor_and_patch to allow optional + minor and patch parts. :param version: version string + :param optional_minor_and_patch: if set to true, the version string to parse \ + can contain optional minor and patch parts. Optional parts are set to zero. + By default (False), the version string to parse has to follow the semver + specification. :return: a new :class:`Version` instance :raises ValueError: if version is invalid :raises TypeError: if version contains the wrong type @@ -575,11 +600,18 @@ def parse(cls, version: String) -> "Version": elif not isinstance(version, String.__args__): # type: ignore raise TypeError("not expecting type '%s'" % type(version)) - match = cls._REGEX.match(version) + if optional_minor_and_patch: + match = cls._REGEX_OPTIONAL_MINOR_AND_PATCH.match(version) + else: + match = cls._REGEX.match(version) if match is None: raise ValueError(f"{version} is not valid SemVer string") matched_version_parts: Dict[str, Any] = match.groupdict() + if not matched_version_parts["minor"]: + matched_version_parts["minor"] = 0 + if not matched_version_parts["patch"]: + matched_version_parts["patch"] = 0 return cls(**matched_version_parts) diff --git a/tests/coerce.py b/tests/coerce.py new file mode 120000 index 00000000..e79106a2 --- /dev/null +++ b/tests/coerce.py @@ -0,0 +1 @@ +../docs/advanced/coerce.py \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 0450e0ee..beecffc9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,10 +4,11 @@ import semver -sys.path.insert(0, "docs") +# sys.path.insert(0, "docs/usage") from coerce import coerce # noqa:E402 from semverwithvprefix import SemVerWithVPrefix # noqa:E402 +import packaging.version @pytest.fixture(autouse=True) @@ -16,6 +17,7 @@ def add_semver(doctest_namespace): doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix + doctest_namespace["PyPIVersion"] = packaging.version.Version @pytest.fixture diff --git a/tests/semverwithvprefix.py b/tests/semverwithvprefix.py new file mode 120000 index 00000000..d1a8d995 --- /dev/null +++ b/tests/semverwithvprefix.py @@ -0,0 +1 @@ +../docs/advanced/semverwithvprefix.py \ No newline at end of file diff --git a/tests/test_match.py b/tests/test_match.py index b4cc50cc..e2685cae 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -23,6 +23,18 @@ def test_should_match_not_equal(left, right, expected): assert match(left, right) is expected +@pytest.mark.parametrize( + "left,right,expected", + [ + ("2.3.7", "2.3.7", True), + ("2.3.6", "2.3.6", True), + ("2.3.7", "4.3.7", False), + ], +) +def test_should_match_equal_by_default(left, right, expected): + assert match(left, right) is expected + + @pytest.mark.parametrize( "left,right,expected", [ @@ -48,9 +60,7 @@ def test_should_raise_value_error_for_unexpected_match_expression(left, right): match(left, right) -@pytest.mark.parametrize( - "left,right", [("1.0.0", ""), ("1.0.0", "!"), ("1.0.0", "1.0.0")] -) +@pytest.mark.parametrize("left,right", [("1.0.0", ""), ("1.0.0", "!")]) def test_should_raise_value_error_for_invalid_match_expression(left, right): with pytest.raises(ValueError): match(left, right) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 25c55c74..ddf52196 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -53,6 +53,56 @@ def test_should_parse_version(version, expected): assert result == expected +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2-alpha.1.2+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 0, + "prerelease": "alpha.1.2", + "build": "build.11.e0f985a", + }, + ), + # no. 2 + ( + "1-alpha-1+build.11.e0f985a", + { + "major": 1, + "minor": 0, + "patch": 0, + "prerelease": "alpha-1", + "build": "build.11.e0f985a", + }, + ), + ( + "0.1-0f", + {"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None}, + ), + ( + "0-0foo.1", + {"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None}, + ), + ( + "0-0foo.1+build.1", + { + "major": 0, + "minor": 0, + "patch": 0, + "prerelease": "0foo.1", + "build": "build.1", + }, + ), + ], +) +def test_should_parse_version_with_optional_minor_and_patch(version, expected): + result = Version.parse(version, optional_minor_and_patch=True) + assert result == expected + + def test_parse_version_info_str_hash(): s_version = "1.2.3-alpha.1.2+build.11.e0f985a" v = parse_version_info(s_version) diff --git a/tox.ini b/tox.ini index 8c7eb5e5..8ca917b8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,19 @@ [tox] envlist = checks - py{36,37,38,39,310} + py3{7,8,9,10,11,12} isolated_build = True +skip_missing_interpreters = True [gh-actions] python = - 3.6: py36 - 3.7: py37 + # setuptools >=62 needs Python >=3.7 + 3.7: py37,check 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 [testenv] @@ -20,12 +23,16 @@ commands = pytest {posargs:} deps = pytest pytest-cov + setuptools>=62.0 + setuptools-scm setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 + [testenv:black] description = Check for formatting changes basepython = python3 +skip_install = true deps = black commands = black --check {posargs:.} @@ -93,8 +100,11 @@ basepython = python3 deps = wheel twine + # PEP 517 build frontend + build commands = - python3 setup.py sdist bdist_wheel + # Same as python3 -m build + pyproject-build twine check dist/*