diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ad3fea61e..bf9fc199f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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: -- [ ] Add yourself to `AUTHORS`; +- [ ] Add yourself to `AUTHORS`, in alphabetical order; diff --git a/AUTHORS b/AUTHORS index 0e75026ad..aeef6dd9b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -171,6 +171,7 @@ Tareq Alayan Ted Xiao Thomas Grainger Thomas Hisch +Tom Dalton Tom Viner Trevor Bekolay Tyler Goodlet diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 917ce3c33..68db81398 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -177,7 +177,8 @@ Short version #. Write a ``changelog`` entry: ``changelog/2574.bugfix``, use issue id number and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``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 diff --git a/_pytest/compat.py b/_pytest/compat.py index 8499e8882..7560fbec3 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -14,7 +14,6 @@ import py import _pytest from _pytest.outcomes import TEST_OUTCOME - try: import enum except ImportError: # pragma: no cover @@ -110,11 +109,10 @@ def getfuncargnames(function, is_method=False, cls=None): # ordered mapping of parameter names to Parameter instances. This # creates a tuple of the names of the parameters that don't have # defaults. - arg_names = tuple( - p.name for p in signature(function).parameters.values() - if (p.kind is Parameter.POSITIONAL_OR_KEYWORD - or p.kind is Parameter.KEYWORD_ONLY) and - p.default is Parameter.empty) + arg_names = tuple(p.name for p in signature(function).parameters.values() + if (p.kind is Parameter.POSITIONAL_OR_KEYWORD or + p.kind is Parameter.KEYWORD_ONLY) and + p.default is Parameter.empty) # If this function should be treated as a bound method even though # it's passed as an unbound method or function, remove the first # parameter name. @@ -129,8 +127,6 @@ def getfuncargnames(function, is_method=False, cls=None): if _PY3: - imap = map - izip = zip STRING_TYPES = bytes, str UNICODE_TYPES = str, @@ -173,8 +169,6 @@ else: STRING_TYPES = bytes, str, unicode UNICODE_TYPES = unicode, - from itertools import imap, izip # NOQA - def ascii_escaped(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, diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 5ac93b1a9..596354ee3 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -1,13 +1,14 @@ from __future__ import absolute_import, division, print_function -import sys - -from py._code.code import FormattedExcinfo - -import py -import warnings import inspect +import sys +import warnings + +import py +from py._code.code import FormattedExcinfo + import _pytest +from _pytest import nodes from _pytest._code.code import TerminalRepr from _pytest.compat import ( NOTSET, exc_clear, _format_args, @@ -15,9 +16,10 @@ from _pytest.compat import ( is_generator, isclass, getimfunc, getlocation, getfuncargnames, safe_getattr, + FuncargnamesCompatAttr, ) from _pytest.outcomes import fail, TEST_OUTCOME -from _pytest.compat import FuncargnamesCompatAttr + from collections import OrderedDict @@ -977,8 +979,8 @@ class FixtureManager: # by their test id) if p.basename.startswith("conftest.py"): nodeid = p.dirpath().relto(self.config.rootdir) - if p.sep != "/": - nodeid = nodeid.replace(p.sep, "/") + if p.sep != nodes.SEP: + nodeid = nodeid.replace(p.sep, nodes.SEP) self.parsefactories(plugin, nodeid) def _getautousenames(self, nodeid): @@ -1033,9 +1035,14 @@ class FixtureManager: if faclist: fixturedef = faclist[-1] 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 - argnames = func_params[0] + if "argnames" in func_kwargs: + argnames = parametrize_func.kwargs["argnames"] + else: + argnames = func_params[0] if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] if argname not in func_params and argname not in argnames: @@ -1123,5 +1130,5 @@ class FixtureManager: def _matchfactories(self, fixturedefs, nodeid): for fixturedef in fixturedefs: - if nodeid.startswith(fixturedef.baseid): + if nodes.ischildnode(fixturedef.baseid, nodeid): yield fixturedef diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index ed3ba2e9a..7fb40dc35 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -17,6 +17,7 @@ import re import sys import time import pytest +from _pytest import nodes from _pytest.config import filename_arg # Python 2.X and 3.X compatibility @@ -252,7 +253,7 @@ def mangle_test_address(address): except ValueError: pass # 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]) # put any params back names[-1] += possible_open_bracket + params diff --git a/_pytest/main.py b/_pytest/main.py index f7c3fe480..a2d61b442 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -7,6 +7,7 @@ import six import sys import _pytest +from _pytest import nodes import _pytest._code import py try: @@ -15,8 +16,8 @@ except ImportError: from UserDict import DictMixin as MappingMixin from _pytest.config import directory_arg, UsageError, hookimpl -from _pytest.runner import collect_one_node from _pytest.outcomes import exit +from _pytest.runner import collect_one_node tracebackcutdir = py.path.local(_pytest.__file__).dirpath() @@ -494,14 +495,14 @@ class FSCollector(Collector): rel = fspath.relto(parent.fspath) if rel: name = rel - name = name.replace(os.sep, "/") + name = name.replace(os.sep, nodes.SEP) super(FSCollector, self).__init__(name, parent, config, session) self.fspath = fspath def _makeid(self): relpath = self.fspath.relto(self.config.rootdir) - if os.sep != "/": - relpath = relpath.replace(os.sep, "/") + if os.sep != nodes.SEP: + relpath = relpath.replace(os.sep, nodes.SEP) return relpath diff --git a/_pytest/mark.py b/_pytest/mark.py index 547a94e4b..03b058d95 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -5,7 +5,7 @@ import inspect import warnings from collections import namedtuple from operator import attrgetter -from .compat import imap +from six.moves import map from .deprecated import MARK_PARAMETERSET_UNPACKING @@ -379,7 +379,7 @@ def store_mark(obj, mark): """ assert isinstance(mark, Mark), mark # 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] @@ -427,7 +427,7 @@ class MarkInfo(object): def __iter__(self): """ yield MarkInfo objects each relating to a marking-call. """ - return imap(MarkInfo, self._marks) + return map(MarkInfo, self._marks) MARK_GEN = MarkGenerator() diff --git a/_pytest/nodes.py b/_pytest/nodes.py new file mode 100644 index 000000000..ad3af2ce6 --- /dev/null +++ b/_pytest/nodes.py @@ -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 diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 345a1acd0..a65e3f027 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -23,9 +23,7 @@ from _pytest.main import Session, EXIT_OK from _pytest.assertion.rewrite import AssertionRewritingHook -PYTEST_FULLPATH = os.path.abspath( - pytest.__file__.rstrip("oc") - ).replace("$py.class", ".py") +PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py") def pytest_addoption(parser): diff --git a/_pytest/python_api.py b/_pytest/python_api.py index b52f68810..bf1cd147e 100644 --- a/_pytest/python_api.py +++ b/_pytest/python_api.py @@ -2,8 +2,9 @@ import math import sys import py +from six.moves import zip -from _pytest.compat import isclass, izip +from _pytest.compat import isclass from _pytest.outcomes import fail import _pytest._code @@ -145,7 +146,7 @@ class ApproxSequence(ApproxBase): return ApproxBase.__eq__(self, actual) def _yield_comparisons(self, actual): - return izip(actual, self.expected) + return zip(actual, self.expected) class ApproxScalar(ApproxBase): @@ -217,7 +218,8 @@ class ApproxScalar(ApproxBase): absolute tolerance or a relative tolerance, depending on what the user 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 # either None or a value specified by the user. diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index c9f86a483..4fceb10a7 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -232,7 +232,5 @@ class WarningsChecker(WarningsRecorder): else: fail("DID NOT WARN. No warnings of type {0} matching" " ('{1}') was emitted. The list of emitted warnings" - " is: {2}.".format( - self.expected_warning, - self.match_expr, - [each.message for each in self])) + " is: {2}.".format(self.expected_warning, self.match_expr, + [each.message for each in self])) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 92f319766..cb4823ff3 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -10,11 +10,12 @@ import sys import time import warnings +import pluggy import py import six -import pluggy import pytest +from _pytest import nodes from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED @@ -466,7 +467,7 @@ class TerminalReporter: if fspath: 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) else: res = "[location]" diff --git a/changelog/1997.doc b/changelog/1997.doc new file mode 100644 index 000000000..0fa110dc9 --- /dev/null +++ b/changelog/1997.doc @@ -0,0 +1 @@ +Explicitly mention ``xpass`` in the documentation of ``xfail``. diff --git a/changelog/2819.bugfix b/changelog/2819.bugfix new file mode 100644 index 000000000..303903cf7 --- /dev/null +++ b/changelog/2819.bugfix @@ -0,0 +1 @@ +Fix issue with @pytest.parametrize if argnames was specified as kwarg. \ No newline at end of file diff --git a/changelog/2836.bug b/changelog/2836.bug new file mode 100644 index 000000000..afa1961d7 --- /dev/null +++ b/changelog/2836.bug @@ -0,0 +1 @@ +Match fixture paths against actual path segments in order to avoid matching folders which share a prefix. diff --git a/changelog/538.doc b/changelog/538.doc new file mode 100644 index 000000000..bc5fb712f --- /dev/null +++ b/changelog/538.doc @@ -0,0 +1 @@ +Clarify the documentation of available fixture scopes. diff --git a/changelog/911.doc b/changelog/911.doc new file mode 100644 index 000000000..e9d94f21c --- /dev/null +++ b/changelog/911.doc @@ -0,0 +1 @@ +Add documentation about the ``python -m pytest`` invocation adding the current directory to sys.path. diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index b72e8e6de..ffeb5a951 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -350,7 +350,7 @@ Parametrizing test methods through per-class configuration .. _`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 parametrizer`_ but in a lot less code:: diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index db7ef0ca2..dace0514e 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -27,7 +27,7 @@ functions: * fixture management scales from simple unit to complex functional testing, allowing to parametrize fixtures and tests according 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 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: -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 @@ -139,10 +139,12 @@ usually time-expensive to create. Extending the previous example, we can add a ``scope='module'`` parameter to the :py:func:`@pytest.fixture <_pytest.python.fixture>` invocation to cause the decorated ``smtp`` fixture function to only be invoked once -per test module. Multiple test functions in a test module will thus -each receive the same ``smtp`` fixture instance. The next example puts -the fixture function into a separate ``conftest.py`` file so -that tests from multiple test modules in the directory can +per test *module* (the default is to invoke once per test *function*). +Multiple test functions in a test module will thus +each receive the same ``smtp`` fixture instance, thus saving time. + +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:: # content of conftest.py @@ -223,6 +225,8 @@ instance, you can simply declare it: # the returned fixture value will be shared for # all tests needing it +Finally, the ``class`` scope will invoke the fixture once per test *class*. + .. _`finalization`: Fixture finalization / executing teardown code @@ -858,7 +862,7 @@ into a conftest.py file **without** using ``autouse``:: # content of conftest.py @pytest.fixture - def transact(self, request, db): + def transact(request, db): db.begin() yield db.rollback() diff --git a/doc/en/pythonpath.rst b/doc/en/pythonpath.rst index 67de7f5d2..b64742768 100644 --- a/doc/en/pythonpath.rst +++ b/doc/en/pythonpath.rst @@ -68,4 +68,9 @@ imported in the global import namespace. 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`. diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 79c4385ca..dbe9c7f8d 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -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 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 information about skipped/xfailed tests is not shown by default to avoid cluttering the output. You can use the ``-r`` option to see details 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`) diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 92b9e653e..db1692029 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -233,3 +233,13 @@ was executed ahead of the ``test_method``. overwrite ``unittest.TestCase`` ``__call__`` or ``run``, they need to to overwrite ``debug`` in the same way (this is also true for standard 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``. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 1cb64ec87..6091db8be 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -17,7 +17,7 @@ You can invoke testing through the Python interpreter from the command line:: python -m 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 -------------------------------------------------------------- diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index ac26068c4..87faeb7bd 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -109,7 +109,7 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable: .. code-block:: python # turns all warnings into errors for this module - pytestmark = @pytest.mark.filterwarnings('error') + pytestmark = pytest.mark.filterwarnings('error') .. note:: diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index b5bee4233..c27b31137 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -3,7 +3,7 @@ import logging logger = logging.getLogger(__name__) -sublogger = logging.getLogger(__name__+'.baz') +sublogger = logging.getLogger(__name__ + '.baz') def test_fixture_help(testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index eb2814527..016c3e61b 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function import pytest import py +import _pytest._code 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", "*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*", + ]) diff --git a/testing/test_mark.py b/testing/test_mark.py index ae070f3a0..dc51bbac0 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -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): def test_mark_per_function(self, testdir): diff --git a/testing/test_nodes.py b/testing/test_nodes.py new file mode 100644 index 000000000..6f4540f99 --- /dev/null +++ b/testing/test_nodes.py @@ -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 diff --git a/tox.ini b/tox.ini index b774cbda5..aaf39026b 100644 --- a/tox.ini +++ b/tox.ini @@ -216,3 +216,8 @@ filterwarnings = [flake8] max-line-length = 120 +ignore= + # do not use bare except' + E722 + # ambiguous variable name 'l' + E741