Skip to content

Commit 54a1af6

Browse files
authored
Merge pull request #352 from tomschr/feature/335-pypi-to-semver
Describe conversion between PyPI and semver
2 parents bafd212 + 0c4985c commit 54a1af6

File tree

7 files changed

+227
-8
lines changed

7 files changed

+227
-8
lines changed

changelog.d/335.doc.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add new section "Converting versions between PyPI and semver" the limitations
2+
and possible use cases to convert from one into the other versioning scheme.
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
Converting versions between PyPI and semver
2+
===========================================
3+
4+
.. Link
5+
https://packaging.pypa.io/en/latest/_modules/packaging/version.html#InvalidVersion
6+
7+
When packaging for PyPI, your versions are defined through `PEP 440`_.
8+
This is the standard version scheme for Python packages and
9+
implemented by the :class:`packaging.version.Version` class.
10+
11+
However, these versions are different from semver versions
12+
(cited from `PEP 440`_):
13+
14+
* The "Major.Minor.Patch" (described in this PEP as "major.minor.micro")
15+
aspects of semantic versioning (clauses 1-8 in the 2.0.0
16+
specification) are fully compatible with the version scheme defined
17+
in this PEP, and abiding by these aspects is encouraged.
18+
19+
* Semantic versions containing a hyphen (pre-releases - clause 10)
20+
or a plus sign (builds - clause 11) are *not* compatible with this PEP
21+
and are not permitted in the public version field.
22+
23+
In other words, it's not always possible to convert between these different
24+
versioning schemes without information loss. It depends on what parts are
25+
used. The following table gives a mapping between these two versioning
26+
schemes:
27+
28+
+--------------+----------------+
29+
| PyPI Version | Semver version |
30+
+==============+================+
31+
| ``epoch`` | n/a |
32+
+--------------+----------------+
33+
| ``major`` | ``major`` |
34+
+--------------+----------------+
35+
| ``minor`` | ``minor`` |
36+
+--------------+----------------+
37+
| ``micro`` | ``patch`` |
38+
+--------------+----------------+
39+
| ``pre`` | ``prerelease`` |
40+
+--------------+----------------+
41+
| ``dev`` | ``build`` |
42+
+--------------+----------------+
43+
| ``post`` | n/a |
44+
+--------------+----------------+
45+
46+
47+
.. _convert_pypi_to_semver:
48+
49+
From PyPI to semver
50+
-------------------
51+
52+
We distinguish between the following use cases:
53+
54+
55+
* **"Incomplete" versions**
56+
57+
If you only have a major part, this shouldn't be a problem.
58+
The initializer of :class:`semver.Version <semver.version.Version>` takes
59+
care to fill missing parts with zeros (except for major).
60+
61+
.. code-block:: python
62+
63+
>>> from packaging.version import Version as PyPIVersion
64+
>>> from semver import Version
65+
66+
>>> p = PyPIVersion("3.2")
67+
>>> p.release
68+
(3, 2)
69+
>>> Version(*p.release)
70+
Version(major=3, minor=2, patch=0, prerelease=None, build=None)
71+
72+
* **Major, minor, and patch**
73+
74+
This is the simplest and most compatible approch. Both versioning
75+
schemes are compatible without information loss.
76+
77+
.. code-block:: python
78+
79+
>>> p = PyPIVersion("3.0.0")
80+
>>> p.base_version
81+
'3.0.0'
82+
>>> p.release
83+
(3, 0, 0)
84+
>>> Version(*p.release)
85+
Version(major=3, minor=0, patch=0, prerelease=None, build=None)
86+
87+
* **With** ``pre`` **part only**
88+
89+
A prerelease exists in both versioning schemes. As such, both are
90+
a natural candidate. A prelease in PyPI version terms is the same
91+
as a "release candidate", or "rc".
92+
93+
.. code-block:: python
94+
95+
>>> p = PyPIVersion("2.1.6.pre5")
96+
>>> p.base_version
97+
'2.1.6'
98+
>>> p.pre
99+
('rc', 5)
100+
>>> pre = "".join([str(i) for i in p.pre])
101+
>>> Version(*p.release, pre)
102+
Version(major=2, minor=1, patch=6, prerelease='rc5', build=None)
103+
104+
* **With only development version**
105+
106+
Semver doesn't have a "development" version.
107+
However, we could use Semver's ``build`` part:
108+
109+
.. code-block:: python
110+
111+
>>> p = PyPIVersion("3.0.0.dev2")
112+
>>> p.base_version
113+
'3.0.0'
114+
>>> p.dev
115+
2
116+
>>> Version(*p.release, build=f"dev{p.dev}")
117+
Version(major=3, minor=0, patch=0, prerelease=None, build='dev2')
118+
119+
* **With a** ``post`` **version**
120+
121+
Semver doesn't know the concept of a post version. As such, there
122+
is currently no way to convert it reliably.
123+
124+
* **Any combination**
125+
126+
There is currently no way to convert a PyPI version which consists
127+
of, for example, development *and* post parts.
128+
129+
130+
You can use the following function to convert a PyPI version into
131+
semver:
132+
133+
.. code-block:: python
134+
135+
def convert2semver(ver: packaging.version.Version) -> semver.Version:
136+
"""Converts a PyPI version into a semver version
137+
138+
:param packaging.version.Version ver: the PyPI version
139+
:return: a semver version
140+
:raises ValueError: if epoch or post parts are used
141+
"""
142+
if not ver.epoch:
143+
raise ValueError("Can't convert an epoch to semver")
144+
if not ver.post:
145+
raise ValueError("Can't convert a post part to semver")
146+
147+
pre = None if not ver.pre else "".join([str(i) for i in ver.pre])
148+
semver.Version(*ver.release, prerelease=pre, build=ver.dev)
149+
150+
151+
.. _convert_semver_to_pypi:
152+
153+
From semver to PyPI
154+
-------------------
155+
156+
We distinguish between the following use cases:
157+
158+
159+
* **Major, minor, and patch**
160+
161+
.. code-block:: python
162+
163+
>>> from packaging.version import Version as PyPIVersion
164+
>>> from semver import Version
165+
166+
>>> v = Version(1, 2, 3)
167+
>>> PyPIVersion(str(v.finalize_version()))
168+
<Version('1.2.3')>
169+
170+
* **With** ``pre`` **part only**
171+
172+
.. code-block:: python
173+
174+
>>> v = Version(2, 1, 4, prerelease="rc1")
175+
>>> PyPIVersion(str(v))
176+
<Version('2.1.4rc1')>
177+
178+
* **With only development version**
179+
180+
.. code-block:: python
181+
182+
>>> v = Version(3, 2, 8, build="dev4")
183+
>>> PyPIVersion(f"{v.finalize_version()}{v.build}")
184+
<Version('3.2.8.dev4')>
185+
186+
If you are unsure about the parts of the version, the following
187+
function helps to convert the different parts:
188+
189+
.. code-block:: python
190+
191+
def convert2pypi(ver: semver.Version) -> packaging.version.Version:
192+
"""Converts a semver version into a version from PyPI
193+
194+
A semver prerelease will be converted into a
195+
prerelease of PyPI.
196+
A semver build will be converted into a development
197+
part of PyPI
198+
:param semver.Version ver: the semver version
199+
:return: a PyPI version
200+
"""
201+
v = ver.finalize_version()
202+
prerelease = ver.prerelease if ver.prerelease else ""
203+
build = ver.build if ver.build else ""
204+
return PyPIVersion(f"{v}{prerelease}{build}")
205+
206+
207+
.. _PEP 440: https://www.python.org/dev/peps/pep-0440/

docs/advanced/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ Advanced topics
77
deal-with-invalid-versions
88
create-subclasses-from-version
99
display-deprecation-warnings
10-
combine-pydantic-and-semver
10+
combine-pydantic-and-semver
11+
convert-pypi-to-semver

docs/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
# documentation root, use os.path.abspath to make it absolute, like shown here.
1818
#
1919
import codecs
20+
from datetime import date
2021
import os
2122
import re
2223
import sys
2324

2425
SRC_DIR = os.path.abspath("../src/")
2526
sys.path.insert(0, SRC_DIR)
2627
# from semver import __version__ # noqa: E402
28+
YEAR = date.today().year
2729

2830

2931
def read(*parts):
@@ -83,7 +85,7 @@ def find_version(*file_paths):
8385

8486
# General information about the project.
8587
project = "python-semver"
86-
copyright = "2018, Kostiantyn Rybnikov and all"
88+
copyright = f"{YEAR}, Kostiantyn Rybnikov and all"
8789
author = "Kostiantyn Rybnikov and all"
8890

8991
# The version info for the project you're documenting, acts as replacement for

docs/install.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ This line avoids surprises. You will get any updates within the major 2 release
1818
Keep in mind, as this line avoids any major version updates, you also will never
1919
get new exciting features or bug fixes.
2020

21-
You can add this line in your file :file:`setup.py`, :file:`requirements.txt`, or any other
22-
file that lists your dependencies.
21+
Same applies for semver v3, if you want to get all updates for the semver v3
22+
development line, but not a major update to semver v4::
23+
24+
semver>=3,<4
25+
26+
You can add this line in your file :file:`setup.py`, :file:`requirements.txt`,
27+
:file:`pyproject.toml`, or any other file that lists your dependencies.
2328

2429
Pip
2530
---
@@ -28,12 +33,12 @@ Pip
2833
2934
pip3 install semver
3035
31-
If you want to install this specific version (for example, 2.10.0), use the command :command:`pip`
36+
If you want to install this specific version (for example, 3.0.0), use the command :command:`pip`
3237
with an URL and its version:
3338

3439
.. parsed-literal::
3540
36-
pip3 install git+https://github.com/python-semver/python-semver.git@2.11.0
41+
pip3 install git+https://github.com/python-semver/python-semver.git@3.0.0
3742
3843
3944
Linux Distributions

docs/migration/replace-deprecated-functions.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ them with code which is compatible for future versions:
6060
.. code-block:: python
6161
6262
>>> s1 = semver.max_ver("1.2.3", "1.2.4")
63-
>>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4"))))
63+
>>> s2 = max("1.2.3", "1.2.4", key=Version.parse)
6464
>>> s1 == s2
6565
True
6666
@@ -71,7 +71,7 @@ them with code which is compatible for future versions:
7171
.. code-block:: python
7272
7373
>>> s1 = semver.min_ver("1.2.3", "1.2.4")
74-
>>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4"))))
74+
>>> s2 = min("1.2.3", "1.2.4", key=Version.parse)
7575
>>> s1 == s2
7676
True
7777

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from coerce import coerce # noqa:E402
1010
from semverwithvprefix import SemVerWithVPrefix # noqa:E402
11+
import packaging.version
1112

1213

1314
@pytest.fixture(autouse=True)
@@ -16,6 +17,7 @@ def add_semver(doctest_namespace):
1617
doctest_namespace["semver"] = semver
1718
doctest_namespace["coerce"] = coerce
1819
doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix
20+
doctest_namespace["PyPIVersion"] = packaging.version.Version
1921

2022

2123
@pytest.fixture

0 commit comments

Comments
 (0)