Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/features' into RonnyPfannschmi…
Browse files Browse the repository at this point in the history
…dt/introduce-attrs
  • Loading branch information
nicoddemus committed Nov 4, 2017
2 parents e58e8fa + b18a9de commit e351976
Show file tree
Hide file tree
Showing 23 changed files with 279 additions and 81 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -47,6 +47,7 @@ Dave Hunt
David Díaz-Barquero
David Mohr
David Vierra
Daw-Ran Liou
Denis Kirisov
Diego Russo
Dmitry Dygalo
Expand Down
16 changes: 11 additions & 5 deletions _pytest/assertion/rewrite.py
Expand Up @@ -591,23 +591,26 @@ def run(self, mod):
# docstrings and __future__ imports.
aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
ast.alias("_pytest.assertion.rewrite", "@pytest_ar")]
expect_docstring = True
doc = getattr(mod, "docstring", None)
expect_docstring = doc is None
if doc is not None and self.is_rewrite_disabled(doc):
return
pos = 0
lineno = 0
lineno = 1
for item in mod.body:
if (expect_docstring and isinstance(item, ast.Expr) and
isinstance(item.value, ast.Str)):
doc = item.value.s
if "PYTEST_DONT_REWRITE" in doc:
# The module has disabled assertion rewriting.
if self.is_rewrite_disabled(doc):
return
lineno += len(doc) - 1
expect_docstring = False
elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or
item.module != "__future__"):
lineno = item.lineno
break
pos += 1
else:
lineno = item.lineno
imports = [ast.Import([alias], lineno=lineno, col_offset=0)
for alias in aliases]
mod.body[pos:pos] = imports
Expand All @@ -633,6 +636,9 @@ def run(self, mod):
not isinstance(field, ast.expr)):
nodes.append(field)

def is_rewrite_disabled(self, docstring):
return "PYTEST_DONT_REWRITE" in docstring

def variable(self):
"""Get a new variable."""
# Use a character invalid in python identifiers to avoid clashing.
Expand Down
5 changes: 5 additions & 0 deletions _pytest/deprecated.py
Expand Up @@ -40,3 +40,8 @@ class RemovedInPytest4Warning(DeprecationWarning):
" please use pytest.param(..., marks=...) instead.\n"
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
)

COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
"pycollector makeitem was removed "
"as it is an accidentially leaked internal api"
)
2 changes: 1 addition & 1 deletion _pytest/doctest.py
Expand Up @@ -127,7 +127,7 @@ def repr_failure(self, excinfo):
lines = ["%03d %s" % (i + test.lineno + 1, x)
for (i, x) in enumerate(lines)]
# trim docstring error lines to 10
lines = lines[example.lineno - 9:example.lineno + 1]
lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
else:
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
indent = '>>>'
Expand Down
30 changes: 28 additions & 2 deletions _pytest/mark.py
Expand Up @@ -8,6 +8,7 @@
from operator import attrgetter
from six.moves import map
from .deprecated import MARK_PARAMETERSET_UNPACKING
from .compat import NOTSET, getfslineno


def alias(name, warning=None):
Expand Down Expand Up @@ -68,6 +69,30 @@ def extract_from(cls, parameterset, legacy_force_tuple=False):

return cls(argval, marks=newmarks, id=None)

@classmethod
def _for_parameterize(cls, argnames, argvalues, function):
if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
else:
force_tuple = False
parameters = [
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
for x in argvalues]
del argvalues

if not parameters:
fs, lineno = getfslineno(function)
reason = "got empty parameter set %r, function %s at %s:%d" % (
argnames, function.__name__, fs, lineno)
mark = MARK_GEN.skip(reason=reason)
parameters.append(ParameterSet(
values=(NOTSET,) * len(argnames),
marks=[mark],
id=None,
))
return argnames, parameters


class MarkerError(Exception):

Expand Down Expand Up @@ -273,8 +298,9 @@ def _check(self, name):
pass
self._markers = l = set()
for line in self._config.getini("markers"):
beginning = line.split(":", 1)
x = beginning[0].split("(", 1)[0]
marker, _ = line.split(":", 1)
marker = marker.rstrip()
x = marker.split("(", 1)[0]
l.add(x)
if name not in self._markers:
raise AttributeError("%r not a registered marker" % (name,))
Expand Down
33 changes: 11 additions & 22 deletions _pytest/python.py
Expand Up @@ -6,9 +6,11 @@
import sys
import os
import collections
import warnings
from textwrap import dedent
from itertools import count


import py
import six
from _pytest.mark import MarkerError
Expand All @@ -18,6 +20,7 @@
import pluggy
from _pytest import fixtures
from _pytest import main
from _pytest import deprecated
from _pytest.compat import (
isclass, isfunction, is_generator, ascii_escaped,
REGEX_TYPE, STRING_TYPES, NoneType, NOTSET,
Expand Down Expand Up @@ -328,7 +331,7 @@ def collect(self):
if name in seen:
continue
seen[name] = True
res = self.makeitem(name, obj)
res = self._makeitem(name, obj)
if res is None:
continue
if not isinstance(res, list):
Expand All @@ -338,6 +341,10 @@ def collect(self):
return l

def makeitem(self, name, obj):
warnings.warn(deprecated.COLLECTOR_MAKEITEM, stacklevel=2)
self._makeitem(name, obj)

def _makeitem(self, name, obj):
# assert self.ihook.fspath == self.fspath, self
return self.ihook.pytest_pycollect_makeitem(
collector=self, name=name, obj=obj)
Expand Down Expand Up @@ -769,30 +776,12 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None,
to set a dynamic scope using test context or configuration.
"""
from _pytest.fixtures import scope2index
from _pytest.mark import MARK_GEN, ParameterSet
from _pytest.mark import ParameterSet
from py.io import saferepr

if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
else:
force_tuple = False
parameters = [
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
for x in argvalues]
argnames, parameters = ParameterSet._for_parameterize(
argnames, argvalues, self.function)
del argvalues

if not parameters:
fs, lineno = getfslineno(self.function)
reason = "got empty parameter set %r, function %s at %s:%d" % (
argnames, self.function.__name__, fs, lineno)
mark = MARK_GEN.skip(reason=reason)
parameters.append(ParameterSet(
values=(NOTSET,) * len(argnames),
marks=[mark],
id=None,
))

if scope is None:
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)

Expand Down
1 change: 1 addition & 0 deletions changelog/1505.doc
@@ -0,0 +1 @@
Introduce a dedicated section about conftest.py.
1 change: 1 addition & 0 deletions changelog/2658.doc
@@ -0,0 +1 @@
Append example for pytest.param in the example/parametrize document.
1 change: 1 addition & 0 deletions changelog/2856.bugfix
@@ -0,0 +1 @@
Strip whitespace from marker names when reading them from INI config.
1 change: 1 addition & 0 deletions changelog/2877.trivial
@@ -0,0 +1 @@
Internal move of the parameterset extraction to a more maintainable place.
1 change: 1 addition & 0 deletions changelog/2882.bugfix
@@ -0,0 +1 @@
Show full context of doctest source in the pytest output, if the lineno of failed example in the docstring is < 9.
4 changes: 2 additions & 2 deletions doc/en/assert.rst
Expand Up @@ -209,8 +209,8 @@ the ``pytest_assertrepr_compare`` hook.
.. autofunction:: _pytest.hookspec.pytest_assertrepr_compare
:noindex:

As an example consider adding the following hook in a conftest.py which
provides an alternative explanation for ``Foo`` objects::
As an example consider adding the following hook in a :ref:`conftest.py <conftest.py>`
file which provides an alternative explanation for ``Foo`` objects::

# content of conftest.py
from test_foocompare import Foo
Expand Down
50 changes: 50 additions & 0 deletions doc/en/example/parametrize.rst
Expand Up @@ -485,4 +485,54 @@ of our ``test_func1`` was skipped. A few notes:
values as well.


Set marks or test ID for individual parametrized test
--------------------------------------------------------------------

Use ``pytest.param`` to apply marks or set test ID to individual parametrized test.
For example::

# content of test_pytest_param_example.py
import pytest
@pytest.mark.parametrize('test_input,expected', [
('3+5', 8),
pytest.param('1+7', 8,
marks=pytest.mark.basic),
pytest.param('2+4', 6,
marks=pytest.mark.basic,
id='basic_2+4'),
pytest.param('6*9', 42,
marks=[pytest.mark.basic, pytest.mark.xfail],
id='basic_6*9'),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
In this example, we have 4 parametrized tests. Except for the first test,
we mark the rest three parametrized tests with the custom marker ``basic``,
and for the fourth test we also use the built-in mark ``xfail`` to indicate this
test is expected to fail. For explicitness, we set test ids for some tests.

Then run ``pytest`` with verbose mode and with only the ``basic`` marker::

pytest -v -m basic
============================================ test session starts =============================================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
collected 4 items

test_pytest_param_example.py::test_eval[1+7-8] PASSED
test_pytest_param_example.py::test_eval[basic_2+4] PASSED
test_pytest_param_example.py::test_eval[basic_6*9] xfail
========================================== short test summary info ===========================================
XFAIL test_pytest_param_example.py::test_eval[basic_6*9]

============================================= 1 tests deselected =============================================

As the result:

- Four tests were collected
- One test was deselected because it doesn't have the ``basic`` mark.
- Three tests with the ``basic`` mark was selected.
- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing.
- The test ``test_eval[basic_2+4]`` passed.
- The test ``test_eval[basic_6*9]`` was expected to fail and did fail.
30 changes: 16 additions & 14 deletions doc/en/example/pythoncollection.rst
Expand Up @@ -175,21 +175,23 @@ You can always peek at the collection tree without running tests like this::
======= no tests ran in 0.12 seconds ========

customizing test collection to find all .py files
---------------------------------------------------------
.. _customizing-test-collection:

.. regendoc:wipe
Customizing test collection
---------------------------

You can easily instruct ``pytest`` to discover tests from every python file::
.. regendoc:wipe
You can easily instruct ``pytest`` to discover tests from every Python file::

# content of pytest.ini
[pytest]
python_files = *.py

However, many projects will have a ``setup.py`` which they don't want to be imported. Moreover, there may files only importable by a specific python version.
For such cases you can dynamically define files to be ignored by listing
them in a ``conftest.py`` file::
However, many projects will have a ``setup.py`` which they don't want to be
imported. Moreover, there may files only importable by a specific python
version. For such cases you can dynamically define files to be ignored by
listing them in a ``conftest.py`` file::

# content of conftest.py
import sys
Expand All @@ -198,7 +200,7 @@ them in a ``conftest.py`` file::
if sys.version_info[0] > 2:
collect_ignore.append("pkg/module_py2.py")

And then if you have a module file like this::
and then if you have a module file like this::

# content of pkg/module_py2.py
def test_only_on_python2():
Expand All @@ -207,13 +209,13 @@ And then if you have a module file like this::
except Exception, e:
pass

and a setup.py dummy file like this::
and a ``setup.py`` dummy file like this::

# content of setup.py
0/0 # will raise exception if imported

then a pytest run on Python2 will find the one test and will leave out the
setup.py file::
If you run with a Python 2 interpreter then you will find the one test and will
leave out the ``setup.py`` file::

#$ pytest --collect-only
====== test session starts ======
Expand All @@ -225,13 +227,13 @@ setup.py file::

====== no tests ran in 0.04 seconds ======

If you run with a Python3 interpreter both the one test and the setup.py file
will be left out::
If you run with a Python 3 interpreter both the one test and the ``setup.py``
file will be left out::

$ pytest --collect-only
======= test session starts ========
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
collected 0 items

======= no tests ran in 0.12 seconds ========

0 comments on commit e351976

Please sign in to comment.