Repository: python-semver/python-semver
Pull request #277 (2020-07-21 16:32:44)

Turn VersionInfo.parse into classmethod to allow subclasses
Suggested solution for #276 


Patch:
diff --git a/docs/development.rst b/docs/development.rst
index dad1464..3f5e9b6 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -154,8 +154,9 @@ documentation includes:
 
   * **A docstring**
 
-    Each docstring contains a summary line, a linebreak, the description
-    of its arguments in `Sphinx style`_, and an optional doctest.
+    Each docstring contains a summary line, a linebreak, an optional
+    directive (see next item), the description of its arguments in
+    `Sphinx style`_, and an optional doctest.
     The docstring is extracted and reused in the :ref:`api` section.
     An appropriate docstring should look like this::
 
@@ -177,6 +178,43 @@ documentation includes:
 
             """
 
+  * **An optional directive**
+
+    If you introduce a new feature, change a function/method, or remove something,
+    it is a good practice to introduce Sphinx directives into the docstring.
+    This gives the reader an idea what version is affected by this change.
+
+    The first required argument, ``VERSION``, defines the version when this change
+    was introduced. You can choose from:
+
+    * ``.. versionadded:: VERSION``
+
+      Use this directive to describe a new feature.
+
+    * ``.. versionchanged:: VERSION``
+
+      Use this directive to describe when something has changed, for example,
+      new parameters were added, changed side effects, different return values, etc.
+
+    * ``.. deprecated:: VERSION``
+
+      Use this directive when a feature is deprecated. Describe what should
+      be used instead, if appropriate.
+
+
+    Add such a directive *after* the summary line, if needed.
+    An appropriate directive could look like this::
+
+        def to_tuple(self):
+            """
+            Convert the VersionInfo object to a tuple.
+
+            .. versionadded:: 2.10.0
+               Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to
+               make this function available in the public API.
+            [...]
+            """
+
   * **The documentation**
 
     A docstring is good, but in most cases it's too dense. Describe how
diff --git a/semver.py b/semver.py
index 85811fe..f6e5bff 100644
--- a/semver.py
+++ b/semver.py
@@ -637,8 +637,8 @@ build='build.10')
 
         return cmp_res in possibilities
 
-    @staticmethod
-    def parse(version):
+    @classmethod
+    def parse(cls, version):
         """
         Parse version string to a VersionInfo instance.
 
@@ -651,7 +651,7 @@ build='build.10')
         VersionInfo(major=3, minor=4, patch=5, \
 prerelease='pre.2', build='build.4')
         """
-        match = VersionInfo._REGEX.match(version)
+        match = cls._REGEX.match(version)
         if match is None:
             raise ValueError("%s is not valid SemVer string" % version)
 
@@ -661,7 +661,7 @@ prerelease='pre.2', build='build.4')
         version_parts["minor"] = int(version_parts["minor"])
         version_parts["patch"] = int(version_parts["patch"])
 
-        return VersionInfo(**version_parts)
+        return cls(**version_parts)
 
     def replace(self, **parts):
         """


Test patch:
diff --git a/test_semver.py b/test_semver.py
index b23576d..0aeedbd 100644
--- a/test_semver.py
+++ b/test_semver.py
@@ -1128,3 +1128,21 @@ def test_next_version_with_versioninfo(version, part, expected):
 )
 def test_repr(version, expected):
     assert repr(version) == expected
+
+
+def test_subclass_from_versioninfo():
+    class SemVerWithVPrefix(VersionInfo):
+        @classmethod
+        def parse(cls, version):
+            if not version.startswith("v"):
+                raise ValueError(
+                    "{v}: not a valid semantic version tag".format(v=version)
+                )
+            return super(SemVerWithVPrefix, cls).parse(version[1:])
+
+        def __str__(self):
+            # Reconstruct the tag.
+            return "v" + super(SemVerWithVPrefix, self).__str__()
+
+    v = SemVerWithVPrefix.parse("v1.2.3")
+    assert str(v) == "v1.2.3"