Skip to content

Commit e3bc6fa

Browse files
committed
Merge pull request #1470 from ceridwen/features
Escape both bytes and unicode strings for "ids" in Metafunc.parametrize
2 parents 909d72b + 23a8e2b commit e3bc6fa

File tree

6 files changed

+80
-45
lines changed

6 files changed

+80
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ env/
3232
.coverage
3333
.ropeproject
3434
.idea
35+
.hypothesis

CHANGELOG.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727

2828
**Changes**
2929

30+
* Fix (`#1351`_):
31+
explicitly passed parametrize ids do not get escaped to ascii.
32+
Thanks `@ceridwen`_ for the PR.
33+
3034
* parametrize ids can accept None as specific test id. The
3135
automatically generated id for that argument will be used.
3236
Thanks `@palaviv`_ for the complete PR (`#1468`_).
@@ -41,19 +45,20 @@
4145
.. _@novas0x2a: https://github.com/novas0x2a
4246
.. _@kalekundert: https://github.com/kalekundert
4347
.. _@tareqalayan: https://github.com/tareqalayan
48+
.. _@ceridwen: https://github.com/ceridwen
4449
.. _@palaviv: https://github.com/palaviv
4550
.. _@omarkohl: https://github.com/omarkohl
4651

4752
.. _#1428: https://github.com/pytest-dev/pytest/pull/1428
4853
.. _#1444: https://github.com/pytest-dev/pytest/pull/1444
4954
.. _#1441: https://github.com/pytest-dev/pytest/pull/1441
5055
.. _#1454: https://github.com/pytest-dev/pytest/pull/1454
56+
.. _#1351: https://github.com/pytest-dev/pytest/issues/1351
5157
.. _#1468: https://github.com/pytest-dev/pytest/pull/1468
5258
.. _#1474: https://github.com/pytest-dev/pytest/pull/1474
5359
.. _#1502: https://github.com/pytest-dev/pytest/pull/1502
5460
.. _#372: https://github.com/pytest-dev/pytest/issues/372
5561

56-
5762
2.9.2.dev1
5863
==========
5964

_pytest/python.py

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,67 +1079,76 @@ def addcall(self, funcargs=None, id=_notexists, param=_notexists):
10791079
self._calls.append(cs)
10801080

10811081

1082+
10821083
if _PY3:
10831084
import codecs
10841085

1085-
def _escape_bytes(val):
1086-
"""
1087-
If val is pure ascii, returns it as a str(), otherwise escapes
1088-
into a sequence of escaped bytes:
1086+
def _escape_strings(val):
1087+
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
1088+
bytes objects into a sequence of escaped bytes:
1089+
10891090
b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6'
10901091
1092+
and escapes unicode objects into a sequence of escaped unicode
1093+
ids, e.g.:
1094+
1095+
'4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944'
1096+
10911097
note:
10921098
the obvious "v.decode('unicode-escape')" will return
1093-
valid utf-8 unicode if it finds them in the string, but we
1099+
valid utf-8 unicode if it finds them in bytes, but we
10941100
want to return escaped bytes for any byte, even if they match
10951101
a utf-8 string.
1102+
10961103
"""
1097-
if val:
1098-
# source: http://goo.gl/bGsnwC
1099-
encoded_bytes, _ = codecs.escape_encode(val)
1100-
return encoded_bytes.decode('ascii')
1104+
if isinstance(val, bytes):
1105+
if val:
1106+
# source: http://goo.gl/bGsnwC
1107+
encoded_bytes, _ = codecs.escape_encode(val)
1108+
return encoded_bytes.decode('ascii')
1109+
else:
1110+
# empty bytes crashes codecs.escape_encode (#1087)
1111+
return ''
11011112
else:
1102-
# empty bytes crashes codecs.escape_encode (#1087)
1103-
return ''
1113+
return val.encode('unicode_escape').decode('ascii')
11041114
else:
1105-
def _escape_bytes(val):
1106-
"""
1107-
In py2 bytes and str are the same type, so return it unchanged if it
1108-
is a full ascii string, otherwise escape it into its binary form.
1115+
def _escape_strings(val):
1116+
"""In py2 bytes and str are the same type, so return if it's a bytes
1117+
object, return it unchanged if it is a full ascii string,
1118+
otherwise escape it into its binary form.
1119+
1120+
If it's a unicode string, change the unicode characters into
1121+
unicode escapes.
1122+
11091123
"""
1110-
try:
1111-
return val.decode('ascii')
1112-
except UnicodeDecodeError:
1113-
return val.encode('string-escape')
1124+
if isinstance(val, bytes):
1125+
try:
1126+
return val.encode('ascii')
1127+
except UnicodeDecodeError:
1128+
return val.encode('string-escape')
1129+
else:
1130+
return val.encode('unicode-escape')
11141131

11151132

11161133
def _idval(val, argname, idx, idfn):
11171134
if idfn:
11181135
try:
11191136
s = idfn(val)
11201137
if s:
1121-
return s
1138+
return _escape_strings(s)
11221139
except Exception:
11231140
pass
11241141

1125-
if isinstance(val, bytes):
1126-
return _escape_bytes(val)
1127-
elif isinstance(val, (float, int, str, bool, NoneType)):
1142+
if isinstance(val, (bytes, str)) or (_PY2 and isinstance(val, unicode)):
1143+
return _escape_strings(val)
1144+
elif isinstance(val, (float, int, bool, NoneType)):
11281145
return str(val)
11291146
elif isinstance(val, REGEX_TYPE):
1130-
return _escape_bytes(val.pattern) if isinstance(val.pattern, bytes) else val.pattern
1147+
return _escape_strings(val.pattern)
11311148
elif enum is not None and isinstance(val, enum.Enum):
11321149
return str(val)
11331150
elif isclass(val) and hasattr(val, '__name__'):
11341151
return val.__name__
1135-
elif _PY2 and isinstance(val, unicode):
1136-
# special case for python 2: if a unicode string is
1137-
# convertible to ascii, return it as an str() object instead
1138-
try:
1139-
return str(val)
1140-
except UnicodeError:
1141-
# fallthrough
1142-
pass
11431152
return str(argname)+str(idx)
11441153

11451154
def _idvalset(idx, valset, argnames, idfn, ids):
@@ -1148,7 +1157,7 @@ def _idvalset(idx, valset, argnames, idfn, ids):
11481157
for val, argname in zip(valset, argnames)]
11491158
return "-".join(this_id)
11501159
else:
1151-
return ids[idx]
1160+
return _escape_strings(ids[idx])
11521161

11531162
def idmaker(argnames, argvalues, idfn=None, ids=None):
11541163
ids = [_idvalset(valindex, valset, argnames, idfn, ids)

testing/python/metafunc.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
# -*- coding: utf-8 -*-
22
import re
3+
import sys
34

45
import _pytest._code
56
import py
67
import pytest
78
from _pytest import python as funcargs
89

10+
import hypothesis
11+
from hypothesis import strategies
12+
13+
PY3 = sys.version_info >= (3, 0)
14+
15+
916
class TestMetafunc:
1017
def Metafunc(self, func):
1118
# the unit tests of this class check if things work correctly
@@ -121,20 +128,29 @@ class A:
121128
assert metafunc._calls[2].id == "x1-a"
122129
assert metafunc._calls[3].id == "x1-b"
123130

124-
@pytest.mark.skipif('sys.version_info[0] >= 3')
125-
def test_unicode_idval_python2(self):
126-
"""unittest for the expected behavior to obtain ids for parametrized
127-
unicode values in Python 2: if convertible to ascii, they should appear
128-
as ascii values, otherwise fallback to hide the value behind the name
129-
of the parametrized variable name. #1086
131+
@hypothesis.given(strategies.text() | strategies.binary())
132+
def test_idval_hypothesis(self, value):
133+
from _pytest.python import _idval
134+
escaped = _idval(value, 'a', 6, None)
135+
assert isinstance(escaped, str)
136+
if PY3:
137+
escaped.encode('ascii')
138+
else:
139+
escaped.decode('ascii')
140+
141+
def test_unicode_idval(self):
142+
"""This tests that Unicode strings outside the ASCII character set get
143+
escaped, using byte escapes if they're in that range or unicode
144+
escapes if they're not.
145+
130146
"""
131147
from _pytest.python import _idval
132148
values = [
133149
(u'', ''),
134150
(u'ascii', 'ascii'),
135-
(u'ação', 'a6'),
136-
(u'josé@blah.com', 'a6'),
137-
(u'δοκ.ιμή@παράδειγμα.δοκιμή', 'a6'),
151+
(u'ação', 'a\\xe7\\xe3o'),
152+
(u'josé@blah.com', 'jos\\[email protected]'),
153+
(u'δοκ.ιμή@παράδειγμα.δοκιμή', '\\u03b4\\u03bf\\u03ba.\\u03b9\\u03bc\\u03ae@\\u03c0\\u03b1\\u03c1\\u03ac\\u03b4\\u03b5\\u03b9\\u03b3\\u03bc\\u03b1.\\u03b4\\u03bf\\u03ba\\u03b9\\u03bc\\u03ae'),
138154
]
139155
for val, expected in values:
140156
assert _idval(val, 'a', 6, None) == expected

testing/test_junitxml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -610,14 +610,14 @@ def test_pass():
610610
def test_escaped_parametrized_names_xml(testdir):
611611
testdir.makepyfile("""
612612
import pytest
613-
@pytest.mark.parametrize('char', ["\\x00"])
613+
@pytest.mark.parametrize('char', [u"\\x00"])
614614
def test_func(char):
615615
assert char
616616
""")
617617
result, dom = runandparse(testdir)
618618
assert result.ret == 0
619619
node = dom.find_first_by_tag("testcase")
620-
node.assert_attr(name="test_func[#x00]")
620+
node.assert_attr(name="test_func[\\x00]")
621621

622622

623623
def test_double_colon_split_function_issue469(testdir):

tox.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ envlist=
1010
commands= py.test --lsof -rfsxX {posargs:testing}
1111
passenv = USER USERNAME
1212
deps=
13+
hypothesis
1314
nose
1415
mock
1516
requests
1617

1718
[testenv:py26]
1819
commands= py.test --lsof -rfsxX {posargs:testing}
1920
deps=
21+
hypothesis<3.0
2022
nose
2123
mock<1.1 # last supported version for py26
2224

@@ -43,6 +45,7 @@ commands = flake8 pytest.py _pytest testing
4345
deps=pytest-xdist>=1.13
4446
mock
4547
nose
48+
hypothesis
4649
commands=
4750
py.test -n1 -rfsxX {posargs:testing}
4851

@@ -67,6 +70,7 @@ commands=
6770

6871
[testenv:py27-nobyte]
6972
deps=pytest-xdist>=1.13
73+
hypothesis
7074
distribute=true
7175
setenv=
7276
PYTHONDONTWRITEBYTECODE=1

0 commit comments

Comments
 (0)