Skip to content

Commit 1587fa0

Browse files
committed
Fix #303: Fix Version.__init__ method
* Allow different variants to call Version * Adapt the documentation and README * Adapt and amend tests * Add changelog entries TODO: Fix typing errors
1 parent 6bb8ca6 commit 1587fa0

File tree

7 files changed

+146
-39
lines changed

7 files changed

+146
-39
lines changed

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ different parts, use the ``semver.Version.parse`` function:
5050

5151
.. code-block:: python
5252
53-
>>> ver = semver.Version.parse('1.2.3-pre.2+build.4')
53+
>>> ver = semver.Version('1.2.3-pre.2+build.4')
5454
>>> ver.major
5555
1
5656
>>> ver.minor
@@ -68,7 +68,7 @@ returns a new ``semver.Version`` instance with the raised major part:
6868

6969
.. code-block:: python
7070
71-
>>> ver = semver.Version.parse("3.4.5")
71+
>>> ver = semver.Version("3.4.5")
7272
>>> ver.bump_major()
7373
Version(major=4, minor=0, patch=0, prerelease=None, build=None)
7474

changelog.d/303.doc.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Prefer :meth:`Version.__init__` over :meth:`Version.parse`
2+
and change examples accordingly.

changelog.d/303.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Extend :meth:`Version.__init__` initializer. It allows
2+
now to have positional and keyword arguments. The keyword
3+
arguments overwrites any positional arguments.

docs/usage.rst

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,12 @@ A :class:`~semver.version.Version` instance can be created in different ways:
5757
* From a Unicode string::
5858

5959
>>> from semver.version import Version
60-
>>> Version.parse("3.4.5-pre.2+build.4")
60+
>>> Version("3.4.5-pre.2+build.4")
6161
Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4')
62-
>>> Version.parse(u"5.3.1")
63-
Version(major=5, minor=3, patch=1, prerelease=None, build=None)
6462

6563
* From a byte string::
6664

67-
>>> Version.parse(b"2.3.4")
65+
>>> Version(b"2.3.4")
6866
Version(major=2, minor=3, patch=4, prerelease=None, build=None)
6967

7068
* From individual parts by a dictionary::
@@ -100,6 +98,22 @@ A :class:`~semver.version.Version` instance can be created in different ways:
10098
>>> Version("3", "5", 6)
10199
Version(major=3, minor=5, patch=6, prerelease=None, build=None)
102100

101+
It is possible to combine, positional and keyword arguments. In
102+
some use cases you have a fixed version string, but would like to
103+
replace parts of them. For example::
104+
105+
>>> Version(1, 2, 3, major=2, build="b2")
106+
Version(major=2, minor=2, patch=3, prerelease=None, build='b2')
107+
108+
In some cases it could be helpful to pass nothing to :class:`Version`::
109+
110+
>>> Version()
111+
Version(major=0, minor=0, patch=0, prerelease=None, build=None)
112+
113+
114+
Using Deprecated Functions to Create a Version
115+
----------------------------------------------
116+
103117
The old, deprecated module level functions are still available but
104118
using them are discoraged. They are available to convert old code
105119
to semver3.
@@ -133,16 +147,6 @@ Depending on your use case, the following methods are available:
133147
ValueError: 1.2 is not valid SemVer string
134148

135149

136-
Parsing a Version String
137-
------------------------
138-
139-
"Parsing" in this context means to identify the different parts in a string.
140-
Use the function :func:`Version.parse <semver.version.Version.parse>`::
141-
142-
>>> Version.parse("3.4.5-pre.2+build.4")
143-
Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4')
144-
145-
146150
Checking for a Valid Semver Version
147151
-----------------------------------
148152

@@ -167,7 +171,7 @@ parts of a version:
167171

168172
.. code-block:: python
169173
170-
>>> v = Version.parse("3.4.5-pre.2+build.4")
174+
>>> v = Version("3.4.5-pre.2+build.4")
171175
>>> v.major
172176
3
173177
>>> v.minor

src/semver/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
VersionDict = Dict[str, VersionPart]
88
VersionIterator = Iterable[VersionPart]
99
String = Union[str, bytes]
10+
StringOrInt = Union[String, int]
1011
F = TypeVar("F", bound=Callable)

src/semver/version.py

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
)
1818

1919
from ._types import (
20-
VersionTuple,
20+
String,
21+
StringOrInt,
2122
VersionDict,
2223
VersionIterator,
23-
String,
24+
VersionTuple,
2425
VersionPart,
2526
)
2627

@@ -109,12 +110,28 @@ class Version:
109110
"""
110111
A semver compatible version class.
111112
113+
:param args: a tuple with version information. It can consist of:
114+
115+
* a maximum length of 5 items that comprehend the major,
116+
minor, patch, prerelease, or build.
117+
* a str or bytes string that contains a valid semver
118+
version string.
112119
:param major: version when you make incompatible API changes.
113120
:param minor: version when you add functionality in
114121
a backwards-compatible manner.
115122
:param patch: version when you make backwards-compatible bug fixes.
116123
:param prerelease: an optional prerelease string
117124
:param build: an optional build string
125+
126+
This gives you some options to call the :class:`Version` class.
127+
Precedence has the keyword arguments over the positional arguments.
128+
129+
>>> Version(1, 2, 3)
130+
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
131+
>>> Version("2.3.4-pre.2")
132+
Version(major=2, minor=3, patch=4, prerelease="pre.2", build=None)
133+
>>> Version(major=2, minor=3, patch=4, build="build.2")
134+
Version(major=2, minor=3, patch=4, prerelease=None, build="build.2")
118135
"""
119136

120137
__slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build")
@@ -144,27 +161,92 @@ class Version:
144161

145162
def __init__(
146163
self,
147-
major: SupportsInt,
164+
*args: Tuple[
165+
Union[str, bytes, int],
166+
Optional[int],
167+
Optional[int],
168+
Optional[str],
169+
Optional[str],
170+
],
171+
major: SupportsInt = 0,
148172
minor: SupportsInt = 0,
149173
patch: SupportsInt = 0,
150-
prerelease: Union[String, int] = None,
151-
build: Union[String, int] = None,
174+
prerelease: StringOrInt = None,
175+
build: StringOrInt = None,
152176
):
177+
verlist = [None, None, None, None, None]
178+
179+
if args and "." in str(args[0]):
180+
# we have a version string as first argument
181+
cls = self.__class__
182+
v = cast(dict, cls._parse(args[0])) # type: ignore
183+
self._major = int(v["major"])
184+
self._minor = int(v["minor"])
185+
self._patch = int(v["patch"])
186+
self._prerelease = v["prerelease"]
187+
self._build = v["build"]
188+
return
189+
if args and len(args) > 5:
190+
raise ValueError("You cannot pass more than 5 arguments to Version")
191+
192+
for index, item in enumerate(args):
193+
verlist[index] = args[index] # type: ignore
194+
153195
# Build a dictionary of the arguments except prerelease and build
154-
version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)}
196+
try:
197+
version_parts = {
198+
# Prefer major, minor, and patch over args
199+
"major": int(major or verlist[0] or 0),
200+
"minor": int(minor or verlist[1] or 0),
201+
"patch": int(patch or verlist[2] or 0),
202+
}
203+
except ValueError:
204+
raise ValueError(
205+
"Expected integer or integer string for major, " "minor, or patch"
206+
)
155207

156208
for name, value in version_parts.items():
157209
if value < 0:
158210
raise ValueError(
159211
"{!r} is negative. A version can only be positive.".format(name)
160212
)
161213

214+
prerelease = prerelease or verlist[3]
215+
build = build or verlist[4]
216+
162217
self._major = version_parts["major"]
163218
self._minor = version_parts["minor"]
164219
self._patch = version_parts["patch"]
165220
self._prerelease = None if prerelease is None else str(prerelease)
166221
self._build = None if build is None else str(build)
167222

223+
@classmethod
224+
def _parse(cls, version: String) -> Dict:
225+
"""
226+
Parse version string to a Version instance.
227+
228+
.. versionchanged:: 2.11.0
229+
Changed method from static to classmethod to
230+
allow subclasses.
231+
232+
:param version: version string
233+
:return: a new :class:`Version` instance
234+
:raises ValueError: if version is invalid
235+
236+
>>> semver.Version.parse('3.4.5-pre.2+build.4')
237+
Version(major=3, minor=4, patch=5, \
238+
prerelease='pre.2', build='build.4')
239+
"""
240+
if isinstance(version, bytes):
241+
version: str = version.decode("UTF-8") # type: ignore
242+
elif not isinstance(version, String.__args__): # type: ignore
243+
raise TypeError(f"not expecting type {type(version)!r}")
244+
match = cls._REGEX.match(cast(str, version))
245+
if match is None:
246+
raise ValueError(f"{version} is not valid SemVer string") # type: ignore
247+
248+
return cast(dict, match.groupdict())
249+
168250
@property
169251
def major(self) -> int:
170252
"""The major part of a version (read-only)."""
@@ -285,7 +367,7 @@ def bump_major(self) -> "Version":
285367
Version(major=4, minor=0, patch=0, prerelease=None, build=None)
286368
"""
287369
cls = type(self)
288-
return cls(self._major + 1)
370+
return cls(major=self._major + 1)
289371

290372
def bump_minor(self) -> "Version":
291373
"""
@@ -299,7 +381,7 @@ def bump_minor(self) -> "Version":
299381
Version(major=3, minor=5, patch=0, prerelease=None, build=None)
300382
"""
301383
cls = type(self)
302-
return cls(self._major, self._minor + 1)
384+
return cls(major=self._major, minor=self._minor + 1)
303385

304386
def bump_patch(self) -> "Version":
305387
"""
@@ -313,7 +395,7 @@ def bump_patch(self) -> "Version":
313395
Version(major=3, minor=4, patch=6, prerelease=None, build=None)
314396
"""
315397
cls = type(self)
316-
return cls(self._major, self._minor, self._patch + 1)
398+
return cls(major=self._major, minor=self._minor, patch=self._patch + 1)
317399

318400
def bump_prerelease(self, token: str = "rc") -> "Version":
319401
"""
@@ -330,7 +412,12 @@ def bump_prerelease(self, token: str = "rc") -> "Version":
330412
"""
331413
cls = type(self)
332414
prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0")
333-
return cls(self._major, self._minor, self._patch, prerelease)
415+
return cls(
416+
major=self._major,
417+
minor=self._minor,
418+
patch=self._patch,
419+
prerelease=prerelease,
420+
)
334421

335422
def bump_build(self, token: str = "build") -> "Version":
336423
"""
@@ -347,7 +434,13 @@ def bump_build(self, token: str = "build") -> "Version":
347434
"""
348435
cls = type(self)
349436
build = cls._increment_string(self._build or (token or "build") + ".0")
350-
return cls(self._major, self._minor, self._patch, self._prerelease, build)
437+
return cls(
438+
major=self._major,
439+
minor=self._minor,
440+
patch=self._patch,
441+
prerelease=self._prerelease,
442+
build=build,
443+
)
351444

352445
def compare(self, other: Comparable) -> int:
353446
"""
@@ -513,11 +606,11 @@ def __repr__(self) -> str:
513606
return "%s(%s)" % (type(self).__name__, s)
514607

515608
def __str__(self) -> str:
516-
version = "%d.%d.%d" % (self.major, self.minor, self.patch)
609+
version = f"{self.major:d}.{self.minor:d}.{self.patch:d}"
517610
if self.prerelease:
518-
version += "-%s" % self.prerelease
611+
version += f"-{self.prerelease}"
519612
if self.build:
520-
version += "+%s" % self.build
613+
version += f"+{self.build}"
521614
return version
522615

523616
def __hash__(self) -> int:
@@ -533,7 +626,7 @@ def finalize_version(self) -> "Version":
533626
'1.2.3'
534627
"""
535628
cls = type(self)
536-
return cls(self.major, self.minor, self.patch)
629+
return cls(major=self.major, minor=self.minor, patch=self.patch)
537630

538631
def match(self, match_expr: str) -> bool:
539632
"""
@@ -598,13 +691,7 @@ def parse(cls, version: String) -> "Version":
598691
Version(major=3, minor=4, patch=5, \
599692
prerelease='pre.2', build='build.4')
600693
"""
601-
version_str = ensure_str(version)
602-
match = cls._REGEX.match(version_str)
603-
if match is None:
604-
raise ValueError(f"{version_str} is not valid SemVer string")
605-
606-
matched_version_parts: Dict[str, Any] = match.groupdict()
607-
694+
matched_version_parts: Dict[str, Any] = cls._parse(version)
608695
return cls(**matched_version_parts)
609696

610697
def replace(self, **parts: Union[int, Optional[str]]) -> "Version":

tests/test_semver.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,13 @@ def test_should_versioninfo_isvalid():
8080
def test_versioninfo_compare_should_raise_when_passed_invalid_value():
8181
with pytest.raises(TypeError):
8282
Version(1, 2, 3).compare(4)
83+
84+
85+
def test_should_raise_when_too_many_arguments():
86+
with pytest.raises(ValueError, match=".* more than 5 arguments .*"):
87+
Version(1, 2, 3, 4, 5, 6)
88+
89+
90+
def test_should_raise_when_incompatible_type():
91+
with pytest.raises(TypeError, match="not expecting type .*"):
92+
Version.parse(complex(42))

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