Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 20e5c4f

Browse filesBrowse files
tomschreli-darkly
andauthored
Fix #274: String Types Py2 vs. Py3 compatibility (#275)
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) * Update Changelog * Update CONTRIBUTORS * Document creating a version from a byte string Co-authored-by: Eli Bishop <eli-darkly@users.noreply.github.com>
1 parent db870f2 commit 20e5c4f
Copy full SHA for 20e5c4f

File tree

6 files changed

+168
-5
lines changed
Filter options

6 files changed

+168
-5
lines changed

‎CHANGELOG.rst

Copy file name to clipboardExpand all lines: CHANGELOG.rst
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Bug Fixes
2323
---------
2424

2525
* :gh:`276` (:pr:`277`): VersionInfo.parse should be a class method
26+
* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError
2627

2728

2829
Additions

‎CONTRIBUTORS

Copy file name to clipboardExpand all lines: CONTRIBUTORS
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Significant contributors
2828
* Carles Barrobés <carles@barrobes.com>
2929
* Craig Blaszczyk <masterjakul@gmail.com>
3030
* Damien Nadé <anvil@users.noreply.github.com>
31+
* Eli Bishop <eli-darkly@users.noreply.github.com>
3132
* George Sakkis <gsakkis@users.noreply.github.com>
3233
* Jan Pieter Waagmeester <jieter@jieter.nl>
3334
* Jelo Agnasin <jelo@icannhas.com>

‎docs/usage.rst

Copy file name to clipboardExpand all lines: docs/usage.rst
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,17 @@ creating a version:
4545

4646
A :class:`semver.VersionInfo` instance can be created in different ways:
4747

48-
* From a string::
48+
* From a string (a Unicode string in Python 2)::
4949

5050
>>> semver.VersionInfo.parse("3.4.5-pre.2+build.4")
5151
VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4')
52+
>>> semver.VersionInfo.parse(u"5.3.1")
53+
VersionInfo(major=5, minor=3, patch=1, prerelease=None, build=None)
54+
55+
* From a byte string::
56+
57+
>>> semver.VersionInfo.parse(b"2.3.4")
58+
VersionInfo(major=2, minor=3, patch=4, prerelease=None, build=None)
5259

5360
* From individual parts by a dictionary::
5461

‎semver.py

Copy file name to clipboardExpand all lines: semver.py
+55-4Lines 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(cls, version):
651702
VersionInfo(major=3, minor=4, patch=5, \
652703
prerelease='pre.2', build='build.4')
653704
"""
654-
match = cls._REGEX.match(version)
705+
match = cls._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

Copy file name to clipboardExpand all lines: setup.cfg
+1Lines 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_typeerror-274.py

Copy file name to clipboard
+102Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.