Skip to content

Commit 20e5c4f

Browse 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

File tree

6 files changed

+168
-5
lines changed

6 files changed

+168
-5
lines changed

CHANGELOG.rst

Lines 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

Lines 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

Lines 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

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(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

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_typeerror-274.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)
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