Merge pull request #1470 from ceridwen/features

Escape both bytes and unicode strings for "ids" in Metafunc.parametrize
This commit is contained in:
Bruno Oliveira 2016-04-03 13:48:30 -03:00
commit e3bc6faa2b
6 changed files with 80 additions and 45 deletions

1
.gitignore vendored
View File

@ -32,3 +32,4 @@ env/
.coverage
.ropeproject
.idea
.hypothesis

View File

@ -27,6 +27,10 @@
**Changes**
* Fix (`#1351`_):
explicitly passed parametrize ids do not get escaped to ascii.
Thanks `@ceridwen`_ for the PR.
* parametrize ids can accept None as specific test id. The
automatically generated id for that argument will be used.
Thanks `@palaviv`_ for the complete PR (`#1468`_).
@ -41,6 +45,7 @@
.. _@novas0x2a: https://github.com/novas0x2a
.. _@kalekundert: https://github.com/kalekundert
.. _@tareqalayan: https://github.com/tareqalayan
.. _@ceridwen: https://github.com/ceridwen
.. _@palaviv: https://github.com/palaviv
.. _@omarkohl: https://github.com/omarkohl
@ -48,12 +53,12 @@
.. _#1444: https://github.com/pytest-dev/pytest/pull/1444
.. _#1441: https://github.com/pytest-dev/pytest/pull/1441
.. _#1454: https://github.com/pytest-dev/pytest/pull/1454
.. _#1351: https://github.com/pytest-dev/pytest/issues/1351
.. _#1468: https://github.com/pytest-dev/pytest/pull/1468
.. _#1474: https://github.com/pytest-dev/pytest/pull/1474
.. _#1502: https://github.com/pytest-dev/pytest/pull/1502
.. _#372: https://github.com/pytest-dev/pytest/issues/372
2.9.2.dev1
==========

View File

@ -1079,38 +1079,55 @@ class Metafunc(FuncargnamesCompatAttr):
self._calls.append(cs)
if _PY3:
import codecs
def _escape_bytes(val):
"""
If val is pure ascii, returns it as a str(), otherwise escapes
into a sequence of escaped bytes:
def _escape_strings(val):
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
bytes objects into a sequence of escaped bytes:
b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6'
and escapes unicode objects into a sequence of escaped unicode
ids, e.g.:
'4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944'
note:
the obvious "v.decode('unicode-escape')" will return
valid utf-8 unicode if it finds them in the string, but we
valid utf-8 unicode if it finds them in bytes, but we
want to return escaped bytes for any byte, even if they match
a utf-8 string.
"""
if val:
# source: http://goo.gl/bGsnwC
encoded_bytes, _ = codecs.escape_encode(val)
return encoded_bytes.decode('ascii')
if isinstance(val, bytes):
if val:
# source: http://goo.gl/bGsnwC
encoded_bytes, _ = codecs.escape_encode(val)
return encoded_bytes.decode('ascii')
else:
# empty bytes crashes codecs.escape_encode (#1087)
return ''
else:
# empty bytes crashes codecs.escape_encode (#1087)
return ''
return val.encode('unicode_escape').decode('ascii')
else:
def _escape_bytes(val):
def _escape_strings(val):
"""In py2 bytes and str are the same type, so return if it's a bytes
object, return it unchanged if it is a full ascii string,
otherwise escape it into its binary form.
If it's a unicode string, change the unicode characters into
unicode escapes.
"""
In py2 bytes and str are the same type, so return it unchanged if it
is a full ascii string, otherwise escape it into its binary form.
"""
try:
return val.decode('ascii')
except UnicodeDecodeError:
return val.encode('string-escape')
if isinstance(val, bytes):
try:
return val.encode('ascii')
except UnicodeDecodeError:
return val.encode('string-escape')
else:
return val.encode('unicode-escape')
def _idval(val, argname, idx, idfn):
@ -1118,28 +1135,20 @@ def _idval(val, argname, idx, idfn):
try:
s = idfn(val)
if s:
return s
return _escape_strings(s)
except Exception:
pass
if isinstance(val, bytes):
return _escape_bytes(val)
elif isinstance(val, (float, int, str, bool, NoneType)):
if isinstance(val, (bytes, str)) or (_PY2 and isinstance(val, unicode)):
return _escape_strings(val)
elif isinstance(val, (float, int, bool, NoneType)):
return str(val)
elif isinstance(val, REGEX_TYPE):
return _escape_bytes(val.pattern) if isinstance(val.pattern, bytes) else val.pattern
return _escape_strings(val.pattern)
elif enum is not None and isinstance(val, enum.Enum):
return str(val)
elif isclass(val) and hasattr(val, '__name__'):
return val.__name__
elif _PY2 and isinstance(val, unicode):
# special case for python 2: if a unicode string is
# convertible to ascii, return it as an str() object instead
try:
return str(val)
except UnicodeError:
# fallthrough
pass
return str(argname)+str(idx)
def _idvalset(idx, valset, argnames, idfn, ids):
@ -1148,7 +1157,7 @@ def _idvalset(idx, valset, argnames, idfn, ids):
for val, argname in zip(valset, argnames)]
return "-".join(this_id)
else:
return ids[idx]
return _escape_strings(ids[idx])
def idmaker(argnames, argvalues, idfn=None, ids=None):
ids = [_idvalset(valindex, valset, argnames, idfn, ids)

View File

@ -1,11 +1,18 @@
# -*- coding: utf-8 -*-
import re
import sys
import _pytest._code
import py
import pytest
from _pytest import python as funcargs
import hypothesis
from hypothesis import strategies
PY3 = sys.version_info >= (3, 0)
class TestMetafunc:
def Metafunc(self, func):
# the unit tests of this class check if things work correctly
@ -121,20 +128,29 @@ class TestMetafunc:
assert metafunc._calls[2].id == "x1-a"
assert metafunc._calls[3].id == "x1-b"
@pytest.mark.skipif('sys.version_info[0] >= 3')
def test_unicode_idval_python2(self):
"""unittest for the expected behavior to obtain ids for parametrized
unicode values in Python 2: if convertible to ascii, they should appear
as ascii values, otherwise fallback to hide the value behind the name
of the parametrized variable name. #1086
@hypothesis.given(strategies.text() | strategies.binary())
def test_idval_hypothesis(self, value):
from _pytest.python import _idval
escaped = _idval(value, 'a', 6, None)
assert isinstance(escaped, str)
if PY3:
escaped.encode('ascii')
else:
escaped.decode('ascii')
def test_unicode_idval(self):
"""This tests that Unicode strings outside the ASCII character set get
escaped, using byte escapes if they're in that range or unicode
escapes if they're not.
"""
from _pytest.python import _idval
values = [
(u'', ''),
(u'ascii', 'ascii'),
(u'ação', 'a6'),
(u'josé@blah.com', 'a6'),
(u'δοκ.ιμή@παράδειγμα.δοκιμή', 'a6'),
(u'ação', 'a\\xe7\\xe3o'),
(u'josé@blah.com', 'jos\\xe9@blah.com'),
(u'δοκ.ιμή@παράδειγμα.δοκιμή', '\\u03b4\\u03bf\\u03ba.\\u03b9\\u03bc\\u03ae@\\u03c0\\u03b1\\u03c1\\u03ac\\u03b4\\u03b5\\u03b9\\u03b3\\u03bc\\u03b1.\\u03b4\\u03bf\\u03ba\\u03b9\\u03bc\\u03ae'),
]
for val, expected in values:
assert _idval(val, 'a', 6, None) == expected

View File

@ -610,14 +610,14 @@ def test_logxml_makedir(testdir):
def test_escaped_parametrized_names_xml(testdir):
testdir.makepyfile("""
import pytest
@pytest.mark.parametrize('char', ["\\x00"])
@pytest.mark.parametrize('char', [u"\\x00"])
def test_func(char):
assert char
""")
result, dom = runandparse(testdir)
assert result.ret == 0
node = dom.find_first_by_tag("testcase")
node.assert_attr(name="test_func[#x00]")
node.assert_attr(name="test_func[\\x00]")
def test_double_colon_split_function_issue469(testdir):

View File

@ -10,6 +10,7 @@ envlist=
commands= py.test --lsof -rfsxX {posargs:testing}
passenv = USER USERNAME
deps=
hypothesis
nose
mock
requests
@ -17,6 +18,7 @@ deps=
[testenv:py26]
commands= py.test --lsof -rfsxX {posargs:testing}
deps=
hypothesis<3.0
nose
mock<1.1 # last supported version for py26
@ -43,6 +45,7 @@ commands = flake8 pytest.py _pytest testing
deps=pytest-xdist>=1.13
mock
nose
hypothesis
commands=
py.test -n1 -rfsxX {posargs:testing}
@ -67,6 +70,7 @@ commands=
[testenv:py27-nobyte]
deps=pytest-xdist>=1.13
hypothesis
distribute=true
setenv=
PYTHONDONTWRITEBYTECODE=1