Repository: python-semver/python-semver (python)

Issue:
Cannot compare with list or string
# Situation

A `VersionInfo` instance has the ability to compare with another `VersionInfo` instance, a tuple, or a dict :

```python
>>> import semver
>>> v1 = semver.VersionInfo.parse("1.2.3")
>>> v1 < (1, 2, 4)
True
```

However, it is not possible for a list or a string:

```python
>>> v1 < [1, 2, 4]
Traceback (most recent call last):
...
TypeError: other type <class 'list'> must be in (<class 'semver.VersionInfo'>, <class 'dict'>, <class 'tuple'>)
>>> v1 < "1.2.4"
Traceback (most recent call last):
...
TypeError: other type <class 'str'> must be in (<class 'semver.VersionInfo'>, <class 'dict'>, <class 'tuple'>)
```

# Situation

After analyzing the situation above, I found the `comparator` which allows `VersionInfo` instance, a tuple, or a dict :


```python
def comparator(operator):
    """Wrap a VersionInfo binary op method in a type-check."""

    @wraps(operator)
    def wrapper(self, other):
        comparable_types = (VersionInfo, dict, tuple)  # bug!
        if not isinstance(other, comparable_types):
            raise TypeError(
                "other type %r must be in %r" % (type(other), comparable_types)
            )
        return operator(self, other)

    return wrapper
```

We should add a list and a string to the allowed types. 

Would that be ok @python-semver/reviewers?

Pull request description:
Fix #244: Allow list & str for comparison
This PR fixes #244 and contains the following changes:

The `VersionInfo.compare` method allows already `VersionInfo`, dict, tuple/list and str.

* Extend list of allowed types in `comparator` with list and str.
* Rewrite case `test_should_not_allow_to_compare_version_with_string` ->
  `test_should_compare_version_string`
* Add new test cases:
  - `test_should_compare_version_list`
  - `test_should_not_allow_to_compare_invalid_versionstring`
* Use `pytest.mark.parametrize` for `test_should_compare_version_tuples`
* Update `usage.rst` to document all possibilities
* Update `CHANGELOG`

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ddefadc..930f81d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -24,6 +24,7 @@ Bug Fixes
 
 * :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``,
   ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)``
+* :gh:`244` (:pr:`245`): Allow comparison with ``VersionInfo``, tuple/list, dict, and string.
 
 
 Additions
diff --git a/docs/usage.rst b/docs/usage.rst
index 91e4d06..0f1f21d 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -295,7 +295,7 @@ To compare two versions depends on your type:
   The return value is negative if ``version1 < version2``, zero if
   ``version1 == version2`` and strictly positive if ``version1 > version2``.
 
-* **Two** :class:`semver.VersionInfo` **types**
+* **Two** :class:`semver.VersionInfo` **instances**
 
   Use the specific operator. Currently, the operators ``<``,
   ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported::
@@ -307,24 +307,68 @@ To compare two versions depends on your type:
     >>> v1 > v2
     False
 
-* **A** :class:`semver.VersionInfo` **type and a** ``tuple``
+* **A** :class:`semver.VersionInfo` **type and a** :func:`tuple` **or** :func:`list`
 
   Use the operator as with two :class:`semver.VersionInfo` types::
 
     >>> v = semver.VersionInfo.parse("3.4.5")
     >>> v > (1, 0)
     True
-    >>> v < (3, 5)
+    >>> v < [3, 5]
     True
 
   The opposite does also work::
 
     >>> (1, 0) < v
     True
-    >>> (3, 5) > v
+    >>> [3, 5] > v
     True
 
-Other types cannot be compared (like dictionaries, lists etc).
+* **A** :class:`semver.VersionInfo` **type and a** :func:`str`
+
+  You can use also raw strings to compare::
+
+    >>> v > "1.0.0"
+    True
+    >>> v < "3.5.0"
+    True
+
+  The opposite does also work::
+
+    >>> "1.0.0" < v
+    True
+    >>> "3.5.0" > v
+    True
+
+  However, if you compare incomplete strings, you get a :class:`ValueError` exception::
+
+    >>> v > "1.0"
+    Traceback (most recent call last):
+    ...
+    ValueError: 1.0 is not valid SemVer string
+
+* **A** :class:`semver.VersionInfo` **type and a** :func:`dict`
+
+  You can also use a dictionary. In contrast to strings, you can have an "incomplete"
+  version (as the other parts are set to zero)::
+
+   >>> v > dict(major=1)
+   True
+
+  The opposite does also work::
+
+   >>> dict(major=1) < v
+   True
+
+  If the dictionary contains unknown keys, you get a :class:`TypeError` exception::
+
+    >>> v > dict(major=1, unknown=42)
+    Traceback (most recent call last):
+    ...
+    TypeError: __init__() got an unexpected keyword argument 'unknown'
+
+
+Other types cannot be compared.
 
 If you need to convert some types into other, refer to :ref:`sec.convert.versions`.
 
diff --git a/semver.py b/semver.py
index a5739b1..a512a43 100644
--- a/semver.py
+++ b/semver.py
@@ -112,7 +112,7 @@ def comparator(operator):
 
     @wraps(operator)
     def wrapper(self, other):
-        comparable_types = (VersionInfo, dict, tuple)
+        comparable_types = (VersionInfo, dict, tuple, list, str)
         if not isinstance(other, comparable_types):
             raise TypeError(
                 "other type %r must be in %r" % (type(other), comparable_types)


Test patch:
diff --git a/test_semver.py b/test_semver.py
index 5daf3f1..3737bbc 100644
--- a/test_semver.py
+++ b/test_semver.py
@@ -557,37 +557,100 @@ def test_should_compare_version_dictionaries():
     assert not (v1 == v4)
 
 
-def test_should_compare_version_tuples():
-    v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4")
-    v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4")
-    for t in (
+@pytest.mark.parametrize(
+    "t",  # fmt: off
+    (
         (1, 0, 0),
         (1, 0),
         (1,),
         (1, 0, 0, "pre.2"),
         (1, 0, 0, "pre.2", "build.4"),
-    ):
-        assert v0 < t
-        assert v0 <= t
-        assert v0 != t
-        assert not v0 == t
-        assert v1 > t
-        assert v1 >= t
-        # Symmetric
-        assert t > v0
-        assert t >= v0
-        assert t < v1
-        assert t <= v1
-        assert t != v0
-        assert not t == v0
-
-
-def test_should_not_allow_to_compare_version_with_string():
+    ),  # fmt: on
+)
+def test_should_compare_version_tuples(t):
+    v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4")
     v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4")
-    with pytest.raises(TypeError):
-        v1 > "1.0.0"
-    with pytest.raises(TypeError):
-        "1.0.0" > v1
+
+    assert v0 < t
+    assert v0 <= t
+    assert v0 != t
+    assert not v0 == t
+    assert v1 > t
+    assert v1 >= t
+    # Symmetric
+    assert t > v0
+    assert t >= v0
+    assert t < v1
+    assert t <= v1
+    assert t != v0
+    assert not t == v0
+
+
+@pytest.mark.parametrize(
+    "lst",  # fmt: off
+    (
+        [1, 0, 0],
+        [1, 0],
+        [1],
+        [1, 0, 0, "pre.2"],
+        [1, 0, 0, "pre.2", "build.4"],
+    ),  # fmt: on
+)
+def test_should_compare_version_list(lst):
+    v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4")
+    v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4")
+
+    assert v0 < lst
+    assert v0 <= lst
+    assert v0 != lst
+    assert not v0 == lst
+    assert v1 > lst
+    assert v1 >= lst
+    # Symmetric
+    assert lst > v0
+    assert lst >= v0
+    assert lst < v1
+    assert lst <= v1
+    assert lst != v0
+    assert not lst == v0
+
+
+@pytest.mark.parametrize(
+    "s",  # fmt: off
+    (
+        "1.0.0",
+        # "1.0",
+        # "1",
+        "1.0.0-pre.2",
+        "1.0.0-pre.2+build.4",
+    ),  # fmt: on
+)
+def test_should_compare_version_string(s):
+    v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4")
+    v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4")
+
+    assert v0 < s
+    assert v0 <= s
+    assert v0 != s
+    assert not v0 == s
+    assert v1 > s
+    assert v1 >= s
+    # Symmetric
+    assert s > v0
+    assert s >= v0
+    assert s < v1
+    assert s <= v1
+    assert s != v0
+    assert not s == v0
+
+
+@pytest.mark.parametrize("s", ("1", "1.0", "1.0.x"))
+def test_should_not_allow_to_compare_invalid_versionstring(s):
+    v = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4")
+    with pytest.raises(ValueError):
+        v < s
+    with pytest.raises(ValueError):
+        s > v
 
 
 def test_should_not_allow_to_compare_version_with_int():