Skip to content

Commit b103a4b

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 dc1e110 commit b103a4b

File tree

7 files changed

+131
-33
lines changed

7 files changed

+131
-33
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
@@ -55,14 +55,12 @@ A :class:`~semver.version.Version` instance can be created in different ways:
5555
* From a Unicode string::
5656

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

6361
* From a byte string::
6462

65-
>>> Version.parse(b"2.3.4")
63+
>>> Version(b"2.3.4")
6664
Version(major=2, minor=3, patch=4, prerelease=None, build=None)
6765

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

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

133147

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

@@ -165,7 +169,7 @@ parts of a version:
165169

166170
.. code-block:: python
167171
168-
>>> v = Version.parse("3.4.5-pre.2+build.4")
172+
>>> v = Version("3.4.5-pre.2+build.4")
169173
>>> v.major
170174
3
171175
>>> v.minor

src/semver/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
VersionPart = Union[int, Optional[str]]
66
VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]]
77
VersionDict = Dict[str, VersionPart]
8+
VersionTupleString = Tuple[str]
89
VersionIterator = Iterable[VersionPart]
910
String = Union[str, bytes]
11+
StringOrInt = Union[String, int]
1012
F = TypeVar("F", bound=Callable)

src/semver/version.py

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
)
1818

1919
from ._types import (
20-
VersionTuple,
20+
String,
21+
StringOrInt,
2122
VersionDict,
2223
VersionIterator,
23-
String,
24+
# VersionTupleString,
25+
VersionTuple,
2426
VersionPart,
2527
)
2628

@@ -109,12 +111,28 @@ class Version:
109111
"""
110112
A semver compatible version class.
111113
114+
:param args: a tuple with version information. It can consist of:
115+
116+
* a maximum length of 5 items that comprehend the major,
117+
minor, patch, prerelease, or build.
118+
* a str or bytes string that contains a valid semver
119+
version string.
112120
:param major: version when you make incompatible API changes.
113121
:param minor: version when you add functionality in
114122
a backwards-compatible manner.
115123
:param patch: version when you make backwards-compatible bug fixes.
116124
:param prerelease: an optional prerelease string
117125
:param build: an optional build string
126+
127+
This gives you some options to call the :class:`Version` class.
128+
Precedence has the keyword arguments over the positional arguments.
129+
130+
>>> Version(1, 2, 3)
131+
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
132+
>>> Version("2.3.4-pre.2")
133+
Version(major=2, minor=3, patch=4, prerelease="pre.2", build=None)
134+
>>> Version(major=2, minor=3, patch=4, build="build.2")
135+
Version(major=2, minor=3, patch=4, prerelease=None, build="build.2")
118136
"""
119137

120138
__slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build")
@@ -144,27 +162,92 @@ class Version:
144162

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

156209
for name, value in version_parts.items():
157210
if value < 0:
158211
raise ValueError(
159212
"{!r} is negative. A version can only be positive.".format(name)
160213
)
161214

215+
prerelease = prerelease or verlist[3]
216+
build = build or verlist[4]
217+
162218
self._major = version_parts["major"]
163219
self._minor = version_parts["minor"]
164220
self._patch = version_parts["patch"]
165221
self._prerelease = None if prerelease is None else str(prerelease)
166222
self._build = None if build is None else str(build)
167223

224+
@classmethod
225+
def _parse(cls, version: String) -> Dict:
226+
"""
227+
Parse version string to a Version instance.
228+
229+
.. versionchanged:: 2.11.0
230+
Changed method from static to classmethod to
231+
allow subclasses.
232+
233+
:param version: version string
234+
:return: a new :class:`Version` instance
235+
:raises ValueError: if version is invalid
236+
237+
>>> semver.Version.parse('3.4.5-pre.2+build.4')
238+
Version(major=3, minor=4, patch=5, \
239+
prerelease='pre.2', build='build.4')
240+
"""
241+
if isinstance(version, bytes):
242+
version: str = version.decode("UTF-8") # type: ignore
243+
elif not isinstance(version, String.__args__): # type: ignore
244+
raise TypeError(f"not expecting type {type(version)!r}")
245+
match = cls._REGEX.match(version)
246+
if match is None:
247+
raise ValueError(f"{version} is not valid SemVer string") # type: ignore
248+
249+
return cast(dict, match.groupdict())
250+
168251
@property
169252
def major(self) -> int:
170253
"""The major part of a version (read-only)."""
@@ -513,11 +596,11 @@ def __repr__(self) -> str:
513596
return "%s(%s)" % (type(self).__name__, s)
514597

515598
def __str__(self) -> str:
516-
version = "%d.%d.%d" % (self.major, self.minor, self.patch)
599+
version = f"{self.major:d}.{self.minor:d}.{self.patch:d}"
517600
if self.prerelease:
518-
version += "-%s" % self.prerelease
601+
version += f"-{self.prerelease}"
519602
if self.build:
520-
version += "+%s" % self.build
603+
version += f"+{self.build}"
521604
return version
522605

523606
def __hash__(self) -> int:
@@ -598,13 +681,7 @@ def parse(cls, version: String) -> "Version":
598681
Version(major=3, minor=4, patch=5, \
599682
prerelease='pre.2', build='build.4')
600683
"""
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-
684+
matched_version_parts: Dict[str, Any] = cls._parse(version)
608685
return cls(**matched_version_parts)
609686

610687
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