Skip to content

Commit 282d185

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 282d185

File tree

3 files changed

+185
-4
lines changed

3 files changed

+185
-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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
if PY3:
12+
string_types = str, bytes
13+
text_type = str
14+
binary_type = bytes
15+
16+
# From six project:
17+
def b(s):
18+
return s.encode("utf-8") # latin-1
19+
20+
def u(s):
21+
return s
22+
23+
24+
else:
25+
string_types = unicode, str
26+
text_type = unicode
27+
binary_type = str
28+
29+
# From six project:
30+
def b(s):
31+
return s
32+
33+
# Workaround for standalone backslash
34+
def u(s):
35+
return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape")
36+
37+
38+
def ensure_binary(s, encoding="utf-8", errors="strict"):
39+
"""Coerce **s** to six.binary_type.
40+
41+
For Python 2:
42+
- `unicode` -> encoded to `str`
43+
- `str` -> `str`
44+
45+
For Python 3:
46+
- `str` -> encoded to `bytes`
47+
- `bytes` -> `bytes`
48+
"""
49+
if isinstance(s, text_type):
50+
return s.encode(encoding, errors)
51+
elif isinstance(s, binary_type):
52+
return s
53+
else:
54+
raise TypeError("not expecting type '%s'" % type(s))
55+
56+
57+
def test_should_work_with_string_and_unicode():
58+
result = semver.compare(u("1.1.0"), b("1.2.2"))
59+
assert result == -1
60+
result = semver.compare(b("1.1.0"), u("1.2.2"))
61+
assert result == -1
62+
63+
64+
class TestEnsure:
65+
# From six project
66+
# grinning face emoji
67+
UNICODE_EMOJI = u("\U0001F600")
68+
BINARY_EMOJI = b"\xf0\x9f\x98\x80"
69+
70+
def test_ensure_binary_raise_type_error(self):
71+
with pytest.raises(TypeError):
72+
semver.ensure_str(8)
73+
74+
def test_errors_and_encoding(self):
75+
ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore")
76+
with pytest.raises(UnicodeEncodeError):
77+
ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict")
78+
79+
def test_ensure_binary_raise(self):
80+
converted_unicode = ensure_binary(
81+
self.UNICODE_EMOJI, encoding="utf-8", errors="strict"
82+
)
83+
converted_binary = ensure_binary(
84+
self.BINARY_EMOJI, encoding="utf-8", errors="strict"
85+
)
86+
if semver.PY2:
87+
# PY2: unicode -> str
88+
assert converted_unicode == self.BINARY_EMOJI and isinstance(
89+
converted_unicode, str
90+
)
91+
# PY2: str -> str
92+
assert converted_binary == self.BINARY_EMOJI and isinstance(
93+
converted_binary, str
94+
)
95+
else:
96+
# PY3: str -> bytes
97+
assert converted_unicode == self.BINARY_EMOJI and isinstance(
98+
converted_unicode, bytes
99+
)
100+
# PY3: bytes -> bytes
101+
assert converted_binary == self.BINARY_EMOJI and isinstance(
102+
converted_binary, bytes
103+
)
104+
105+
def test_ensure_str(self):
106+
converted_unicode = semver.ensure_str(
107+
self.UNICODE_EMOJI, encoding="utf-8", errors="strict"
108+
)
109+
converted_binary = semver.ensure_str(
110+
self.BINARY_EMOJI, encoding="utf-8", errors="strict"
111+
)
112+
if PY2:
113+
# PY2: unicode -> str
114+
assert converted_unicode == self.BINARY_EMOJI and isinstance(
115+
converted_unicode, str
116+
)
117+
# PY2: str -> str
118+
assert converted_binary == self.BINARY_EMOJI and isinstance(
119+
converted_binary, str
120+
)
121+
else:
122+
# PY3: str -> str
123+
assert converted_unicode == self.UNICODE_EMOJI and isinstance(
124+
converted_unicode, str
125+
)
126+
# PY3: bytes -> str
127+
assert converted_binary == self.UNICODE_EMOJI and isinstance(
128+
converted_unicode, str
129+
)

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