Skip to content

Commit 54a1af6

Browse files
authored
Merge pull request python-semver#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy