Skip to content

Commit

Permalink
Merge branch 'python-semver:master' into feature/303-refactor-__init__
Browse files Browse the repository at this point in the history
  • Loading branch information
tomschr committed Jan 31, 2022
2 parents ff470fa + 54a1af6 commit 282e9c7
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 8 deletions.
2 changes: 2 additions & 0 deletions changelog.d/335.doc.rst
Original file line number Diff line number Diff line change
@@ -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.
207 changes: 207 additions & 0 deletions docs/advanced/convert-pypi-to-semver.rst
Original file line number Diff line number Diff line change
@@ -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 <semver.version.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()))
<Version('1.2.3')>
* **With** ``pre`` **part only**

.. code-block:: python
>>> v = Version(2, 1, 4, prerelease="rc1")
>>> PyPIVersion(str(v))
<Version('2.1.4rc1')>
* **With only development version**

.. code-block:: python
>>> v = Version(3, 2, 8, build="dev4")
>>> PyPIVersion(f"{v.finalize_version()}{v.build}")
<Version('3.2.8.dev4')>
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/
3 changes: 2 additions & 1 deletion docs/advanced/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Advanced topics
deal-with-invalid-versions
create-subclasses-from-version
display-deprecation-warnings
combine-pydantic-and-semver
combine-pydantic-and-semver
convert-pypi-to-semver
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
# 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

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):
Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/migration/replace-deprecated-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from coerce import coerce # noqa:E402
from semverwithvprefix import SemVerWithVPrefix # noqa:E402
import packaging.version


@pytest.fixture(autouse=True)
Expand All @@ -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
Expand Down

0 comments on commit 282e9c7

Please sign in to comment.