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

Issue:
Py2 vs. Py3 incompatibility: TypeError
# Situation

I'm confused by the statement in the Readme that seems to say 2.10.0 _does_ still support Python 2.7. That's not what I'm seeing. I have a project that uses this package and runs in Py2, and as soon as it started using the 2.10 release, I started getting errors like this which look to me like what you'd get from a Py2 vs. Py3 incompatibility:

    TypeError("Expected str or VersionInfo instance, but got <type 'unicode'>"

When I pin the dependency to `<2.10`, the error goes away.

_Originally posted by @eli-darkly in https://github.com/python-semver/python-semver/issues/161#issuecomment-654455352_

Pull request:
Fix #274:  String Types Py2 vs. Py3 compatibility
This fixes #274 and contains:

fixes problems between different string types. In Python2 `str` vs. `unicode` and in Python3 `str` vs. `bytes`.

---

@eli-darkly I've tried to recycle some code from the six project. Maybe there is an easier way, maybe we can improve some ugly code. Would you like to have a look at? Thank you! :heart: 


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c79012b..e5736f3 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -23,6 +23,7 @@ Bug Fixes
 ---------
 
 * :gh:`276` (:pr:`277`): VersionInfo.parse should be a class method
+* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError
 
 
 Additions
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 4460512..a4b61fa 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -28,6 +28,7 @@ Significant contributors
 * Carles Barrobés <carles@barrobes.com>
 * Craig Blaszczyk <masterjakul@gmail.com>
 * Damien Nadé <anvil@users.noreply.github.com>
+* Eli Bishop <eli-darkly@users.noreply.github.com>
 * George Sakkis <gsakkis@users.noreply.github.com>
 * Jan Pieter Waagmeester <jieter@jieter.nl>
 * Jelo Agnasin <jelo@icannhas.com>
diff --git a/docs/usage.rst b/docs/usage.rst
index 2f23e57..010f1c0 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -45,10 +45,17 @@ creating a version:
 
 A :class:`semver.VersionInfo` instance can be created in different ways:
 
-* From a string::
+* From a string (a Unicode string in Python 2)::
 
     >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4")
     VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4')
+    >>> semver.VersionInfo.parse(u"5.3.1")
+    VersionInfo(major=5, minor=3, patch=1, prerelease=None, build=None)
+
+* From a byte string::
+
+    >>> semver.VersionInfo.parse(b"2.3.4")
+    VersionInfo(major=2, minor=3, patch=4, prerelease=None, build=None)
 
 * From individual parts by a dictionary::
 
diff --git a/semver.py b/semver.py
index f6e5bff..4797d61 100644
--- a/semver.py
+++ b/semver.py
@@ -10,6 +10,10 @@ import sys
 import warnings
 
 
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+
 __version__ = "2.10.2"
 __author__ = "Kostiantyn Rybnikov"
 __author_email__ = "k-bx@k-bx.com"
@@ -60,6 +64,53 @@ if not hasattr(__builtins__, "cmp"):
         return (a > b) - (a < b)
 
 
+if PY3:  # pragma: no cover
+    string_types = str, bytes
+    text_type = str
+    binary_type = bytes
+
+    def b(s):
+        return s.encode("latin-1")
+
+    def u(s):
+        return s
+
+
+else:  # pragma: no cover
+    string_types = unicode, str
+    text_type = unicode
+    binary_type = str
+
+    def b(s):
+        return s
+
+    # Workaround for standalone backslash
+    def u(s):
+        return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape")
+
+
+def ensure_str(s, encoding="utf-8", errors="strict"):
+    # Taken from six project
+    """
+    Coerce *s* to `str`.
+
+    For Python 2:
+      - `unicode` -> encoded to `str`
+      - `str` -> `str`
+
+    For Python 3:
+      - `str` -> `str`
+      - `bytes` -> decoded to `str`
+    """
+    if not isinstance(s, (text_type, binary_type)):
+        raise TypeError("not expecting type '%s'" % type(s))
+    if PY2 and isinstance(s, text_type):
+        s = s.encode(encoding, errors)
+    elif PY3 and isinstance(s, binary_type):
+        s = s.decode(encoding, errors)
+    return s
+
+
 def deprecated(func=None, replace=None, version=None, category=DeprecationWarning):
     """
     Decorates a function to output a deprecation warning.
@@ -144,7 +195,7 @@ def comparator(operator):
 
     @wraps(operator)
     def wrapper(self, other):
-        comparable_types = (VersionInfo, dict, tuple, list, str)
+        comparable_types = (VersionInfo, dict, tuple, list, text_type, binary_type)
         if not isinstance(other, comparable_types):
             raise TypeError(
                 "other type %r must be in %r" % (type(other), comparable_types)
@@ -423,7 +474,7 @@ build='build.10')
         0
         """
         cls = type(self)
-        if isinstance(other, str):
+        if isinstance(other, string_types):
             other = cls.parse(other)
         elif isinstance(other, dict):
             other = cls(**other)
@@ -651,7 +702,7 @@ build='build.10')
         VersionInfo(major=3, minor=4, patch=5, \
 prerelease='pre.2', build='build.4')
         """
-        match = cls._REGEX.match(version)
+        match = cls._REGEX.match(ensure_str(version))
         if match is None:
             raise ValueError("%s is not valid SemVer string" % version)
 
@@ -825,7 +876,7 @@ def max_ver(ver1, ver2):
     >>> semver.max_ver("1.0.0", "2.0.0")
     '2.0.0'
     """
-    if isinstance(ver1, str):
+    if isinstance(ver1, string_types):
         ver1 = VersionInfo.parse(ver1)
     elif not isinstance(ver1, VersionInfo):
         raise TypeError()
diff --git a/setup.cfg b/setup.cfg
index 1cefc4b..a22f03a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,6 +13,7 @@ addopts =
 
 [flake8]
 max-line-length = 88
+ignore = F821,W503
 exclude =
     .env,
     .eggs,


Test patch:
diff --git a/test_typeerror-274.py b/test_typeerror-274.py
new file mode 100644
index 0000000..2ed03d6
--- /dev/null
+++ b/test_typeerror-274.py
@@ -0,0 +1,102 @@
+import pytest
+import sys
+
+import semver
+
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+
+def ensure_binary(s, encoding="utf-8", errors="strict"):
+    """Coerce **s** to six.binary_type.
+
+    For Python 2:
+      - `unicode` -> encoded to `str`
+      - `str` -> `str`
+
+    For Python 3:
+      - `str` -> encoded to `bytes`
+      - `bytes` -> `bytes`
+    """
+    if isinstance(s, semver.text_type):
+        return s.encode(encoding, errors)
+    elif isinstance(s, semver.binary_type):
+        return s
+    else:
+        raise TypeError("not expecting type '%s'" % type(s))
+
+
+def test_should_work_with_string_and_unicode():
+    result = semver.compare(semver.u("1.1.0"), semver.b("1.2.2"))
+    assert result == -1
+    result = semver.compare(semver.b("1.1.0"), semver.u("1.2.2"))
+    assert result == -1
+
+
+class TestEnsure:
+    # From six project
+    # grinning face emoji
+    UNICODE_EMOJI = semver.u("\U0001F600")
+    BINARY_EMOJI = b"\xf0\x9f\x98\x80"
+
+    def test_ensure_binary_raise_type_error(self):
+        with pytest.raises(TypeError):
+            semver.ensure_str(8)
+
+    def test_errors_and_encoding(self):
+        ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore")
+        with pytest.raises(UnicodeEncodeError):
+            ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict")
+
+    def test_ensure_binary_raise(self):
+        converted_unicode = ensure_binary(
+            self.UNICODE_EMOJI, encoding="utf-8", errors="strict"
+        )
+        converted_binary = ensure_binary(
+            self.BINARY_EMOJI, encoding="utf-8", errors="strict"
+        )
+        if semver.PY2:
+            # PY2: unicode -> str
+            assert converted_unicode == self.BINARY_EMOJI and isinstance(
+                converted_unicode, str
+            )
+            # PY2: str -> str
+            assert converted_binary == self.BINARY_EMOJI and isinstance(
+                converted_binary, str
+            )
+        else:
+            # PY3: str -> bytes
+            assert converted_unicode == self.BINARY_EMOJI and isinstance(
+                converted_unicode, bytes
+            )
+            # PY3: bytes -> bytes
+            assert converted_binary == self.BINARY_EMOJI and isinstance(
+                converted_binary, bytes
+            )
+
+    def test_ensure_str(self):
+        converted_unicode = semver.ensure_str(
+            self.UNICODE_EMOJI, encoding="utf-8", errors="strict"
+        )
+        converted_binary = semver.ensure_str(
+            self.BINARY_EMOJI, encoding="utf-8", errors="strict"
+        )
+        if PY2:
+            # PY2: unicode -> str
+            assert converted_unicode == self.BINARY_EMOJI and isinstance(
+                converted_unicode, str
+            )
+            # PY2: str -> str
+            assert converted_binary == self.BINARY_EMOJI and isinstance(
+                converted_binary, str
+            )
+        else:
+            # PY3: str -> str
+            assert converted_unicode == self.UNICODE_EMOJI and isinstance(
+                converted_unicode, str
+            )
+            # PY3: bytes -> str
+            assert converted_binary == self.UNICODE_EMOJI and isinstance(
+                converted_unicode, str
+            )