Merge pull request #2869 from nicoddemus/merge-master-into-features

Merge master into features
This commit is contained in:
Ronny Pfannschmidt 2017-10-25 09:08:08 +02:00 committed by GitHub
commit def471b975
30 changed files with 193 additions and 57 deletions

View File

@ -12,4 +12,4 @@ Here's a quick checklist that should be present in PRs:
Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please: Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please:
- [ ] Add yourself to `AUTHORS`; - [ ] Add yourself to `AUTHORS`, in alphabetical order;

View File

@ -171,6 +171,7 @@ Tareq Alayan
Ted Xiao Ted Xiao
Thomas Grainger Thomas Grainger
Thomas Hisch Thomas Hisch
Tom Dalton
Tom Viner Tom Viner
Trevor Bekolay Trevor Bekolay
Tyler Goodlet Tyler Goodlet

View File

@ -177,7 +177,8 @@ Short version
#. Write a ``changelog`` entry: ``changelog/2574.bugfix``, use issue id number #. Write a ``changelog`` entry: ``changelog/2574.bugfix``, use issue id number
and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or
``trivial`` for the issue type. ``trivial`` for the issue type.
#. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order. #. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please
add yourself to the ``AUTHORS`` file, in alphabetical order;
Long version Long version

View File

@ -14,7 +14,6 @@ import py
import _pytest import _pytest
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
try: try:
import enum import enum
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@ -110,10 +109,9 @@ def getfuncargnames(function, is_method=False, cls=None):
# ordered mapping of parameter names to Parameter instances. This # ordered mapping of parameter names to Parameter instances. This
# creates a tuple of the names of the parameters that don't have # creates a tuple of the names of the parameters that don't have
# defaults. # defaults.
arg_names = tuple( arg_names = tuple(p.name for p in signature(function).parameters.values()
p.name for p in signature(function).parameters.values() if (p.kind is Parameter.POSITIONAL_OR_KEYWORD or
if (p.kind is Parameter.POSITIONAL_OR_KEYWORD p.kind is Parameter.KEYWORD_ONLY) and
or p.kind is Parameter.KEYWORD_ONLY) and
p.default is Parameter.empty) p.default is Parameter.empty)
# If this function should be treated as a bound method even though # If this function should be treated as a bound method even though
# it's passed as an unbound method or function, remove the first # it's passed as an unbound method or function, remove the first
@ -129,8 +127,6 @@ def getfuncargnames(function, is_method=False, cls=None):
if _PY3: if _PY3:
imap = map
izip = zip
STRING_TYPES = bytes, str STRING_TYPES = bytes, str
UNICODE_TYPES = str, UNICODE_TYPES = str,
@ -173,8 +169,6 @@ else:
STRING_TYPES = bytes, str, unicode STRING_TYPES = bytes, str, unicode
UNICODE_TYPES = unicode, UNICODE_TYPES = unicode,
from itertools import imap, izip # NOQA
def ascii_escaped(val): def ascii_escaped(val):
"""In py2 bytes and str are the same type, so return if it's a bytes """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, object, return it unchanged if it is a full ascii string,

View File

@ -1,13 +1,14 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
from py._code.code import FormattedExcinfo
import py
import warnings
import inspect import inspect
import sys
import warnings
import py
from py._code.code import FormattedExcinfo
import _pytest import _pytest
from _pytest import nodes
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest.compat import ( from _pytest.compat import (
NOTSET, exc_clear, _format_args, NOTSET, exc_clear, _format_args,
@ -15,9 +16,10 @@ from _pytest.compat import (
is_generator, isclass, getimfunc, is_generator, isclass, getimfunc,
getlocation, getfuncargnames, getlocation, getfuncargnames,
safe_getattr, safe_getattr,
FuncargnamesCompatAttr,
) )
from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.outcomes import fail, TEST_OUTCOME
from _pytest.compat import FuncargnamesCompatAttr
from collections import OrderedDict from collections import OrderedDict
@ -977,8 +979,8 @@ class FixtureManager:
# by their test id) # by their test id)
if p.basename.startswith("conftest.py"): if p.basename.startswith("conftest.py"):
nodeid = p.dirpath().relto(self.config.rootdir) nodeid = p.dirpath().relto(self.config.rootdir)
if p.sep != "/": if p.sep != nodes.SEP:
nodeid = nodeid.replace(p.sep, "/") nodeid = nodeid.replace(p.sep, nodes.SEP)
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid): def _getautousenames(self, nodeid):
@ -1033,8 +1035,13 @@ class FixtureManager:
if faclist: if faclist:
fixturedef = faclist[-1] fixturedef = faclist[-1]
if fixturedef.params is not None: if fixturedef.params is not None:
func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]]) parametrize_func = getattr(metafunc.function, 'parametrize', None)
func_params = getattr(parametrize_func, 'args', [[None]])
func_kwargs = getattr(parametrize_func, 'kwargs', {})
# skip directly parametrized arguments # skip directly parametrized arguments
if "argnames" in func_kwargs:
argnames = parametrize_func.kwargs["argnames"]
else:
argnames = func_params[0] argnames = func_params[0]
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
@ -1123,5 +1130,5 @@ class FixtureManager:
def _matchfactories(self, fixturedefs, nodeid): def _matchfactories(self, fixturedefs, nodeid):
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if nodeid.startswith(fixturedef.baseid): if nodes.ischildnode(fixturedef.baseid, nodeid):
yield fixturedef yield fixturedef

View File

@ -17,6 +17,7 @@ import re
import sys import sys
import time import time
import pytest import pytest
from _pytest import nodes
from _pytest.config import filename_arg from _pytest.config import filename_arg
# Python 2.X and 3.X compatibility # Python 2.X and 3.X compatibility
@ -252,7 +253,7 @@ def mangle_test_address(address):
except ValueError: except ValueError:
pass pass
# convert file path to dotted path # convert file path to dotted path
names[0] = names[0].replace("/", '.') names[0] = names[0].replace(nodes.SEP, '.')
names[0] = _py_ext_re.sub("", names[0]) names[0] = _py_ext_re.sub("", names[0])
# put any params back # put any params back
names[-1] += possible_open_bracket + params names[-1] += possible_open_bracket + params

View File

@ -7,6 +7,7 @@ import six
import sys import sys
import _pytest import _pytest
from _pytest import nodes
import _pytest._code import _pytest._code
import py import py
try: try:
@ -15,8 +16,8 @@ except ImportError:
from UserDict import DictMixin as MappingMixin from UserDict import DictMixin as MappingMixin
from _pytest.config import directory_arg, UsageError, hookimpl from _pytest.config import directory_arg, UsageError, hookimpl
from _pytest.runner import collect_one_node
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.runner import collect_one_node
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@ -494,14 +495,14 @@ class FSCollector(Collector):
rel = fspath.relto(parent.fspath) rel = fspath.relto(parent.fspath)
if rel: if rel:
name = rel name = rel
name = name.replace(os.sep, "/") name = name.replace(os.sep, nodes.SEP)
super(FSCollector, self).__init__(name, parent, config, session) super(FSCollector, self).__init__(name, parent, config, session)
self.fspath = fspath self.fspath = fspath
def _makeid(self): def _makeid(self):
relpath = self.fspath.relto(self.config.rootdir) relpath = self.fspath.relto(self.config.rootdir)
if os.sep != "/": if os.sep != nodes.SEP:
relpath = relpath.replace(os.sep, "/") relpath = relpath.replace(os.sep, nodes.SEP)
return relpath return relpath

View File

@ -5,7 +5,7 @@ import inspect
import warnings import warnings
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from .compat import imap from six.moves import map
from .deprecated import MARK_PARAMETERSET_UNPACKING from .deprecated import MARK_PARAMETERSET_UNPACKING
@ -379,7 +379,7 @@ def store_mark(obj, mark):
""" """
assert isinstance(mark, Mark), mark assert isinstance(mark, Mark), mark
# always reassign name to avoid updating pytestmark # always reassign name to avoid updating pytestmark
# in a referene that was only borrowed # in a reference that was only borrowed
obj.pytestmark = get_unpacked_marks(obj) + [mark] obj.pytestmark = get_unpacked_marks(obj) + [mark]
@ -427,7 +427,7 @@ class MarkInfo(object):
def __iter__(self): def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """ """ yield MarkInfo objects each relating to a marking-call. """
return imap(MarkInfo, self._marks) return map(MarkInfo, self._marks)
MARK_GEN = MarkGenerator() MARK_GEN = MarkGenerator()

37
_pytest/nodes.py Normal file
View File

@ -0,0 +1,37 @@
SEP = "/"
def _splitnode(nodeid):
"""Split a nodeid into constituent 'parts'.
Node IDs are strings, and can be things like:
''
'testing/code'
'testing/code/test_excinfo.py'
'testing/code/test_excinfo.py::TestFormattedExcinfo::()'
Return values are lists e.g.
[]
['testing', 'code']
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
"""
if nodeid == '':
# If there is no root node at all, return an empty list so the caller's logic can remain sane
return []
parts = nodeid.split(SEP)
# Replace single last element 'test_foo.py::Bar::()' with multiple elements 'test_foo.py', 'Bar', '()'
parts[-1:] = parts[-1].split("::")
return parts
def ischildnode(baseid, nodeid):
"""Return True if the nodeid is a child node of the baseid.
E.g. 'foo/bar::Baz::()' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
"""
base_parts = _splitnode(baseid)
node_parts = _splitnode(nodeid)
if len(node_parts) < len(base_parts):
return False
return node_parts[:len(base_parts)] == base_parts

View File

@ -23,9 +23,7 @@ from _pytest.main import Session, EXIT_OK
from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import AssertionRewritingHook
PYTEST_FULLPATH = os.path.abspath( PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py")
pytest.__file__.rstrip("oc")
).replace("$py.class", ".py")
def pytest_addoption(parser): def pytest_addoption(parser):

View File

@ -2,8 +2,9 @@ import math
import sys import sys
import py import py
from six.moves import zip
from _pytest.compat import isclass, izip from _pytest.compat import isclass
from _pytest.outcomes import fail from _pytest.outcomes import fail
import _pytest._code import _pytest._code
@ -145,7 +146,7 @@ class ApproxSequence(ApproxBase):
return ApproxBase.__eq__(self, actual) return ApproxBase.__eq__(self, actual)
def _yield_comparisons(self, actual): def _yield_comparisons(self, actual):
return izip(actual, self.expected) return zip(actual, self.expected)
class ApproxScalar(ApproxBase): class ApproxScalar(ApproxBase):
@ -217,7 +218,8 @@ class ApproxScalar(ApproxBase):
absolute tolerance or a relative tolerance, depending on what the user absolute tolerance or a relative tolerance, depending on what the user
specified or which would be larger. specified or which would be larger.
""" """
def set_default(x, default): return x if x is not None else default def set_default(x, default):
return x if x is not None else default
# Figure out what the absolute tolerance should be. ``self.abs`` is # Figure out what the absolute tolerance should be. ``self.abs`` is
# either None or a value specified by the user. # either None or a value specified by the user.

View File

@ -232,7 +232,5 @@ class WarningsChecker(WarningsRecorder):
else: else:
fail("DID NOT WARN. No warnings of type {0} matching" fail("DID NOT WARN. No warnings of type {0} matching"
" ('{1}') was emitted. The list of emitted warnings" " ('{1}') was emitted. The list of emitted warnings"
" is: {2}.".format( " is: {2}.".format(self.expected_warning, self.match_expr,
self.expected_warning,
self.match_expr,
[each.message for each in self])) [each.message for each in self]))

View File

@ -10,11 +10,12 @@ import sys
import time import time
import warnings import warnings
import pluggy
import py import py
import six import six
import pluggy
import pytest import pytest
from _pytest import nodes
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
@ -466,7 +467,7 @@ class TerminalReporter:
if fspath: if fspath:
res = mkrel(nodeid).replace("::()", "") # parens-normalization res = mkrel(nodeid).replace("::()", "") # parens-normalization
if nodeid.split("::")[0] != fspath.replace("\\", "/"): if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
res += " <- " + self.startdir.bestrelpath(fspath) res += " <- " + self.startdir.bestrelpath(fspath)
else: else:
res = "[location]" res = "[location]"

1
changelog/1997.doc Normal file
View File

@ -0,0 +1 @@
Explicitly mention ``xpass`` in the documentation of ``xfail``.

1
changelog/2819.bugfix Normal file
View File

@ -0,0 +1 @@
Fix issue with @pytest.parametrize if argnames was specified as kwarg.

1
changelog/2836.bug Normal file
View File

@ -0,0 +1 @@
Match fixture paths against actual path segments in order to avoid matching folders which share a prefix.

1
changelog/538.doc Normal file
View File

@ -0,0 +1 @@
Clarify the documentation of available fixture scopes.

1
changelog/911.doc Normal file
View File

@ -0,0 +1 @@
Add documentation about the ``python -m pytest`` invocation adding the current directory to sys.path.

View File

@ -350,7 +350,7 @@ Parametrizing test methods through per-class configuration
.. _`unittest parametrizer`: https://github.com/testing-cabal/unittest-ext/blob/master/params.py .. _`unittest parametrizer`: https://github.com/testing-cabal/unittest-ext/blob/master/params.py
Here is an example ``pytest_generate_function`` function implementing a Here is an example ``pytest_generate_tests`` function implementing a
parametrization scheme similar to Michael Foord's `unittest parametrization scheme similar to Michael Foord's `unittest
parametrizer`_ but in a lot less code:: parametrizer`_ but in a lot less code::

View File

@ -27,7 +27,7 @@ functions:
* fixture management scales from simple unit to complex * fixture management scales from simple unit to complex
functional testing, allowing to parametrize fixtures and tests according functional testing, allowing to parametrize fixtures and tests according
to configuration and component options, or to re-use fixtures to configuration and component options, or to re-use fixtures
across class, module or whole test session scopes. across function, class, module or whole test session scopes.
In addition, pytest continues to support :ref:`xunitsetup`. You can mix In addition, pytest continues to support :ref:`xunitsetup`. You can mix
both styles, moving incrementally from classic to new style, as you both styles, moving incrementally from classic to new style, as you
@ -129,8 +129,8 @@ functions take the role of the *injector* and test functions are the
.. _smtpshared: .. _smtpshared:
Sharing a fixture across tests in a module (or class/session) Scope: Sharing a fixture across tests in a class, module or session
----------------------------------------------------------------- -------------------------------------------------------------------
.. regendoc:wipe .. regendoc:wipe
@ -139,10 +139,12 @@ usually time-expensive to create. Extending the previous example, we
can add a ``scope='module'`` parameter to the can add a ``scope='module'`` parameter to the
:py:func:`@pytest.fixture <_pytest.python.fixture>` invocation :py:func:`@pytest.fixture <_pytest.python.fixture>` invocation
to cause the decorated ``smtp`` fixture function to only be invoked once to cause the decorated ``smtp`` fixture function to only be invoked once
per test module. Multiple test functions in a test module will thus per test *module* (the default is to invoke once per test *function*).
each receive the same ``smtp`` fixture instance. The next example puts Multiple test functions in a test module will thus
the fixture function into a separate ``conftest.py`` file so each receive the same ``smtp`` fixture instance, thus saving time.
that tests from multiple test modules in the directory can
The next example puts the fixture function into a separate ``conftest.py`` file
so that tests from multiple test modules in the directory can
access the fixture function:: access the fixture function::
# content of conftest.py # content of conftest.py
@ -223,6 +225,8 @@ instance, you can simply declare it:
# the returned fixture value will be shared for # the returned fixture value will be shared for
# all tests needing it # all tests needing it
Finally, the ``class`` scope will invoke the fixture once per test *class*.
.. _`finalization`: .. _`finalization`:
Fixture finalization / executing teardown code Fixture finalization / executing teardown code
@ -858,7 +862,7 @@ into a conftest.py file **without** using ``autouse``::
# content of conftest.py # content of conftest.py
@pytest.fixture @pytest.fixture
def transact(self, request, db): def transact(request, db):
db.begin() db.begin()
yield yield
db.rollback() db.rollback()

View File

@ -68,4 +68,9 @@ imported in the global import namespace.
This is also discussed in details in :ref:`test discovery`. This is also discussed in details in :ref:`test discovery`.
Invoking ``pytest`` versus ``python -m pytest``
-----------------------------------------------
Running pytest with ``python -m pytest [...]`` instead of ``pytest [...]`` yields nearly
equivalent behaviour, except that the former call will add the current directory to ``sys.path``.
See also :ref:`cmdline`.

View File

@ -16,13 +16,17 @@ resource which is not available at the moment (for example a database).
A **xfail** means that you expect a test to fail for some reason. A **xfail** means that you expect a test to fail for some reason.
A common example is a test for a feature not yet implemented, or a bug not yet fixed. A common example is a test for a feature not yet implemented, or a bug not yet fixed.
When a test passes despite being expected to fail (marked with ``pytest.mark.xfail``),
it's an **xpass** and will be reported in the test summary.
``pytest`` counts and lists *skip* and *xfail* tests separately. Detailed ``pytest`` counts and lists *skip* and *xfail* tests separately. Detailed
information about skipped/xfailed tests is not shown by default to avoid information about skipped/xfailed tests is not shown by default to avoid
cluttering the output. You can use the ``-r`` option to see details cluttering the output. You can use the ``-r`` option to see details
corresponding to the "short" letters shown in the test progress:: corresponding to the "short" letters shown in the test progress::
pytest -rxs # show extra info on skips and xfails pytest -rxXs # show extra info on xfailed, xpassed, and skipped tests
More details on the ``-r`` option can be found by running ``pytest -h``.
(See :ref:`how to change command line options defaults`) (See :ref:`how to change command line options defaults`)

View File

@ -233,3 +233,13 @@ was executed ahead of the ``test_method``.
overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to
to overwrite ``debug`` in the same way (this is also true for standard to overwrite ``debug`` in the same way (this is also true for standard
unittest). unittest).
.. note::
Due to architectural differences between the two frameworks, setup and
teardown for ``unittest``-based tests is performed during the ``call`` phase
of testing instead of in ``pytest``'s standard ``setup`` and ``teardown``
stages. This can be important to understand in some situations, particularly
when reasoning about errors. For example, if a ``unittest``-based suite
exhibits errors during setup, ``pytest`` will report no errors during its
``setup`` phase and will instead raise the error during ``call``.

View File

@ -17,7 +17,7 @@ You can invoke testing through the Python interpreter from the command line::
python -m pytest [...] python -m pytest [...]
This is almost equivalent to invoking the command line script ``pytest [...]`` This is almost equivalent to invoking the command line script ``pytest [...]``
directly, except that Python will also add the current directory to ``sys.path``. directly, except that calling via ``python`` will also add the current directory to ``sys.path``.
Possible exit codes Possible exit codes
-------------------------------------------------------------- --------------------------------------------------------------

View File

@ -109,7 +109,7 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
.. code-block:: python .. code-block:: python
# turns all warnings into errors for this module # turns all warnings into errors for this module
pytestmark = @pytest.mark.filterwarnings('error') pytestmark = pytest.mark.filterwarnings('error')
.. note:: .. note::

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function
import pytest import pytest
import py import py
import _pytest._code
from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv
@ -829,3 +830,28 @@ def test_continue_on_collection_errors_maxfail(testdir):
"collected 2 items / 2 errors", "collected 2 items / 2 errors",
"*1 failed, 2 error*", "*1 failed, 2 error*",
]) ])
def test_fixture_scope_sibling_conftests(testdir):
"""Regression test case for https://github.com/pytest-dev/pytest/issues/2836"""
foo_path = testdir.mkpydir("foo")
foo_path.join("conftest.py").write(_pytest._code.Source("""
import pytest
@pytest.fixture
def fix():
return 1
"""))
foo_path.join("test_foo.py").write("def test_foo(fix): assert fix == 1")
# Tests in `food/` should not see the conftest fixture from `foo/`
food_path = testdir.mkpydir("food")
food_path.join("test_food.py").write("def test_food(fix): assert fix == 1")
res = testdir.runpytest()
assert res.ret == 1
res.stdout.fnmatch_lines([
"*ERROR at setup of test_food*",
"E*fixture 'fix' not found",
"*1 passed, 1 error*",
])

View File

@ -342,6 +342,24 @@ def test_parametrized_collect_with_wrong_args(testdir):
]) ])
def test_parametrized_with_kwargs(testdir):
"""Test collect parametrized func with wrong number of args."""
py_file = testdir.makepyfile("""
import pytest
@pytest.fixture(params=[1,2])
def a(request):
return request.param
@pytest.mark.parametrize(argnames='b', argvalues=[1, 2])
def test_func(a, b):
pass
""")
result = testdir.runpytest(py_file)
assert(result.ret == 0)
class TestFunctional(object): class TestFunctional(object):
def test_mark_per_function(self, testdir): def test_mark_per_function(self, testdir):

18
testing/test_nodes.py Normal file
View File

@ -0,0 +1,18 @@
import pytest
from _pytest import nodes
@pytest.mark.parametrize("baseid, nodeid, expected", (
('', '', True),
('', 'foo', True),
('', 'foo/bar', True),
('', 'foo/bar::TestBaz::()', True),
('foo', 'food', False),
('foo/bar::TestBaz::()', 'foo/bar', False),
('foo/bar::TestBaz::()', 'foo/bar::TestBop::()', False),
('foo/bar', 'foo/bar::TestBop::()', True),
))
def test_ischildnode(baseid, nodeid, expected):
result = nodes.ischildnode(baseid, nodeid)
assert result is expected

View File

@ -216,3 +216,8 @@ filterwarnings =
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
ignore=
# do not use bare except'
E722
# ambiguous variable name 'l'
E741