Skip to content

Escape both bytes and unicode strings for "ids" in Metafunc.parametrize #1470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ env/
.coverage
.ropeproject
.idea
.hypothesis
7 changes: 6 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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`_).
Expand All @@ -37,16 +41,17 @@
.. _@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

.. _#1428: https://github.com/pytest-dev/pytest/pull/1428
.. _#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


2.9.2.dev1
==========

Expand Down
75 changes: 42 additions & 33 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -1079,67 +1079,76 @@ def addcall(self, funcargs=None, id=_notexists, param=_notexists):
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):
"""
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.
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.

"""
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):
if 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):
Expand All @@ -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)
Expand Down
34 changes: 25 additions & 9 deletions testing/python/metafunc.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -121,20 +128,29 @@ class A:
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
Expand Down
4 changes: 2 additions & 2 deletions testing/test_junitxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,14 +610,14 @@ def test_pass():
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):
Expand Down
4 changes: 4 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ envlist=
commands= py.test --lsof -rfsxX {posargs:testing}
passenv = USER USERNAME
deps=
hypothesis
nose
mock
requests

[testenv:py26]
commands= py.test --lsof -rfsxX {posargs:testing}
deps=
hypothesis<3.0
nose
mock<1.1 # last supported version for py26

Expand All @@ -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}

Expand All @@ -67,6 +70,7 @@ commands=

[testenv:py27-nobyte]
deps=pytest-xdist>=1.13
hypothesis
distribute=true
setenv=
PYTHONDONTWRITEBYTECODE=1
Expand Down