Skip to content

Commit 9d30056

Browse files
committed
Fix #274: Py2 vs. Py3 incompatibility
This fixes problems between different string types. In Python2 str vs. unicode and in Python3 str vs. bytes. * Add some code from six project * Suppress two flake8 issues (false positives)
1 parent e2532b2 commit 9d30056

File tree

3 files changed

+158
-4
lines changed

3 files changed

+158
-4
lines changed

semver.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import warnings
1111

1212

13+
PY2 = sys.version_info[0] == 2
14+
PY3 = sys.version_info[0] == 3
15+
16+
1317
__version__ = "2.10.2"
1418
__author__ = "Kostiantyn Rybnikov"
1519
__author_email__ = "k-bx@k-bx.com"
@@ -60,6 +64,53 @@ def cmp(a, b):
6064
return (a > b) - (a < b)
6165

6266

67+
if PY3: # pragma: no cover
68+
string_types = str, bytes
69+
text_type = str
70+
binary_type = bytes
71+
72+
def b(s):
73+
return s.encode("latin-1")
74+
75+
def u(s):
76+
return s
77+
78+
79+
else: # pragma: no cover
80+
string_types = unicode, str
81+
text_type = unicode
82+
binary_type = str
83+
84+
def b(s):
85+
return s
86+
87+
# Workaround for standalone backslash
88+
def u(s):
89+
return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape")
90+
91+
92+
def ensure_str(s, encoding="utf-8", errors="strict"):
93+
# Taken from six project
94+
"""
95+
Coerce *s* to `str`.
96+
97+
For Python 2:
98+
- `unicode` -> encoded to `str`
99+
- `str` -> `str`
100+
101+
For Python 3:
102+
- `str` -> `str`
103+
- `bytes` -> decoded to `str`
104+
"""
105+
if not isinstance(s, (text_type, binary_type)):
106+
raise TypeError("not expecting type '%s'" % type(s))
107+
if PY2 and isinstance(s, text_type):
108+
s = s.encode(encoding, errors)
109+
elif PY3 and isinstance(s, binary_type):
110+
s = s.decode(encoding, errors)
111+
return s
112+
113+
63114
def deprecated(func=None, replace=None, version=None, category=DeprecationWarning):
64115
"""
65116
Decorates a function to output a deprecation warning.
@@ -144,7 +195,7 @@ def comparator(operator):
144195

145196
@wraps(operator)
146197
def wrapper(self, other):
147-
comparable_types = (VersionInfo, dict, tuple, list, str)
198+
comparable_types = (VersionInfo, dict, tuple, list, text_type, binary_type)
148199
if not isinstance(other, comparable_types):
149200
raise TypeError(
150201
"other type %r must be in %r" % (type(other), comparable_types)
@@ -423,7 +474,7 @@ def compare(self, other):
423474
0
424475
"""
425476
cls = type(self)
426-
if isinstance(other, str):
477+
if isinstance(other, string_types):
427478
other = cls.parse(other)
428479
elif isinstance(other, dict):
429480
other = cls(**other)
@@ -651,7 +702,7 @@ def parse(version):
651702
VersionInfo(major=3, minor=4, patch=5, \
652703
prerelease='pre.2', build='build.4')
653704
"""
654-
match = VersionInfo._REGEX.match(version)
705+
match = VersionInfo._REGEX.match(ensure_str(version))
655706
if match is None:
656707
raise ValueError("%s is not valid SemVer string" % version)
657708

@@ -825,7 +876,7 @@ def max_ver(ver1, ver2):
825876
>>> semver.max_ver("1.0.0", "2.0.0")
826877
'2.0.0'
827878
"""
828-
if isinstance(ver1, str):
879+
if isinstance(ver1, string_types):
829880
ver1 = VersionInfo.parse(ver1)
830881
elif not isinstance(ver1, VersionInfo):
831882
raise TypeError()

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ addopts =
1313
1414
[flake8]
1515
max-line-length = 88
16+
ignore = F821,W503
1617
exclude =
1718
.env,
1819
.eggs,

test_py2.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import pytest
2+
import sys
3+
4+
import semver
5+
6+
7+
PY2 = sys.version_info[0] == 2
8+
PY3 = sys.version_info[0] == 3
9+
10+
11+
def ensure_binary(s, encoding="utf-8", errors="strict"):
12+
"""Coerce **s** to six.binary_type.
13+
14+
For Python 2:
15+
- `unicode` -> encoded to `str`
16+
- `str` -> `str`
17+
18+
For Python 3:
19+
- `str` -> encoded to `bytes`
20+
- `bytes` -> `bytes`
21+
"""
22+
if isinstance(s, semver.text_type):
23+
return s.encode(encoding, errors)
24+
elif isinstance(s, semver.binary_type):
25+
return s
26+
else:
27+
raise TypeError("not expecting type '%s'" % type(s))
28+
29+
30+
def test_should_work_with_string_and_unicode():
31+
result = semver.compare(semver.u("1.1.0"), semver.b("1.2.2"))
32+
assert result == -1
33+
result = semver.compare(semver.b("1.1.0"), semver.u("1.2.2"))
34+
assert result == -1
35+
36+
37+
class TestEnsure:
38+
# From six project
39+
# grinning face emoji
40+
UNICODE_EMOJI = semver.u("\U0001F600")
41+
BINARY_EMOJI = b"\xf0\x9f\x98\x80"
42+
43+
def test_ensure_binary_raise_type_error(self):
44+
with pytest.raises(TypeError):
45+
semver.ensure_str(8)
46+
47+
def test_errors_and_encoding(self):
48+
ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore")
49+
with pytest.raises(UnicodeEncodeError):
50+
ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict")
51+
52+
def test_ensure_binary_raise(self):
53+
converted_unicode = ensure_binary(
54+
self.UNICODE_EMOJI, encoding="utf-8", errors="strict"
55+
)
56+
converted_binary = ensure_binary(
57+
self.BINARY_EMOJI, encoding="utf-8", errors="strict"
58+
)
59+
if semver.PY2:
60+
# PY2: unicode -> str
61+
assert converted_unicode == self.BINARY_EMOJI and isinstance(
62+
converted_unicode, str
63+
)
64+
# PY2: str -> str
65+
assert converted_binary == self.BINARY_EMOJI and isinstance(
66+
converted_binary, str
67+
)
68+
else:
69+
# PY3: str -> bytes
70+
assert converted_unicode == self.BINARY_EMOJI and isinstance(
71+
converted_unicode, bytes
72+
)
73+
# PY3: bytes -> bytes
74+
assert converted_binary == self.BINARY_EMOJI and isinstance(
75+
converted_binary, bytes
76+
)
77+
78+
def test_ensure_str(self):
79+
converted_unicode = semver.ensure_str(
80+
self.UNICODE_EMOJI, encoding="utf-8", errors="strict"
81+
)
82+
converted_binary = semver.ensure_str(
83+
self.BINARY_EMOJI, encoding="utf-8", errors="strict"
84+
)
85+
if PY2:
86+
# PY2: unicode -> str
87+
assert converted_unicode == self.BINARY_EMOJI and isinstance(
88+
converted_unicode, str
89+
)
90+
# PY2: str -> str
91+
assert converted_binary == self.BINARY_EMOJI and isinstance(
92+
converted_binary, str
93+
)
94+
else:
95+
# PY3: str -> str
96+
assert converted_unicode == self.UNICODE_EMOJI and isinstance(
97+
converted_unicode, str
98+
)
99+
# PY3: bytes -> str
100+
assert converted_binary == self.UNICODE_EMOJI and isinstance(
101+
converted_unicode, str
102+
)

0 commit comments

Comments
 (0)