From 0c4985c91d5ae9d0085d27a9484397ac47a3b437 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 25 Jan 2022 21:40:45 +0100 Subject: [PATCH] Describe conversion between PyPI and semver Add new section "Converting versions between PyPI and semver" the limitations and possible use cases to convert from one into the other versioning scheme. --- changelog.d/335.doc.rst | 2 + docs/advanced/convert-pypi-to-semver.rst | 207 ++++++++++++++++++ docs/advanced/index.rst | 3 +- docs/conf.py | 4 +- docs/install.rst | 13 +- .../replace-deprecated-functions.rst | 4 +- tests/conftest.py | 2 + 7 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 changelog.d/335.doc.rst create mode 100644 docs/advanced/convert-pypi-to-semver.rst diff --git a/changelog.d/335.doc.rst b/changelog.d/335.doc.rst new file mode 100644 index 00000000..1d29fb87 --- /dev/null +++ b/changelog.d/335.doc.rst @@ -0,0 +1,2 @@ +Add new section "Converting versions between PyPI and semver" the limitations +and possible use cases to convert from one into the other versioning scheme. 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/index.rst b/docs/advanced/index.rst index f45a2f9e..d82da1a1 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -7,4 +7,5 @@ Advanced topics deal-with-invalid-versions create-subclasses-from-version display-deprecation-warnings - combine-pydantic-and-semver \ No newline at end of file + combine-pydantic-and-semver + convert-pypi-to-semver 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/install.rst b/docs/install.rst index b603703c..f23186a2 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 diff --git a/docs/migration/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst index a8f2f8f6..9738001c 100644 --- a/docs/migration/replace-deprecated-functions.rst +++ b/docs/migration/replace-deprecated-functions.rst @@ -60,7 +60,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = max("1.2.3", "1.2.4", key=Version.parse) >>> s1 == s2 True @@ -71,7 +71,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = min("1.2.3", "1.2.4", key=Version.parse) >>> s1 == s2 True diff --git a/tests/conftest.py b/tests/conftest.py index 40b56ab1..beecffc9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ 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