From e04d9ff80be855164799bb315d23ca611c4b10f4 Mon Sep 17 00:00:00 2001 From: aostr Date: Sat, 25 Jun 2016 18:16:13 +0200 Subject: [PATCH 01/34] * now showing pytest warnings summary by default. * added ``--disable-pytest-warnings` flag to let users disable the warnings summary. * extended/changed unit tests for the changes in the pytest core. --- AUTHORS | 1 + CHANGELOG.rst | 4 ++++ _pytest/terminal.py | 13 +++++++++++-- testing/test_config.py | 2 +- testing/test_terminal.py | 14 ++++++++++++-- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index c35e66587..462dc71e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,7 @@ Alexei Kozlenok Anatoly Bubenkoff Andreas Zeidler Andy Freeland +Andrzej Ostrowski Anthon van der Neut Armin Rigo Aron Curzon diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e7371a021..af7c6db95 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,10 @@ Thanks `@bagerard`_ for reporting (`#1503`_). Thanks to `@davehunt`_ and `@tomviner`_ for PR. +* Whitelisted pytest warnings to show up warnings summary by default. Added a new + flag ``--disable-pytest-warnings`` to explicitly disable the warnings summary. + This change resolves the (`#1668`_). + * Renamed the pytest ``pdb`` module (plugin) into ``debugging``. * diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 825f553ef..3344203d9 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -20,10 +20,15 @@ def pytest_addoption(parser): group._addoption('-q', '--quiet', action="count", dest="quiet", default=0, help="decrease verbosity."), group._addoption('-r', - action="store", dest="reportchars", default=None, metavar="chars", + action="store", dest="reportchars", default='', metavar="chars", help="show extra test summary info as specified by chars (f)ailed, " "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings " - "(p)passed, (P)passed with output, (a)all except pP.") + "(p)passed, (P)passed with output, (a)all except pP. " + "The pytest warnings are displayed at all times except when " + "--disable-pytest-warnings is set") + group._addoption('--disable-pytest-warnings', default=False, + dest='disablepytestwarnings', action='store_true', + help='disable warnings summary, overrides -r w flag') group._addoption('-l', '--showlocals', action="store_true", dest="showlocals", default=False, help="show locals in tracebacks (disabled by default).") @@ -66,6 +71,10 @@ def getreportopt(config): elif setting == "xfailed": reportopts += "x" reportchars = config.option.reportchars + if not config.option.disablepytestwarnings and 'w' not in reportchars: + reportchars += 'w' + elif config.option.disablepytestwarnings and 'w' in reportchars: + reportchars = reportchars.replace('w', '') if reportchars: for char in reportchars: if char not in reportopts and char != 'a': diff --git a/testing/test_config.py b/testing/test_config.py index fe0654017..c90555756 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -519,7 +519,7 @@ class TestWarning: """) result = testdir.runpytest() assert result.parseoutcomes()["pytest-warnings"] > 0 - assert "hello" not in result.stdout.str() + assert "hello" in result.stdout.str() result = testdir.runpytest("-rw") result.stdout.fnmatch_lines(""" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index be5749c96..bda5e858c 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -591,6 +591,7 @@ def test_getreportopt(): class config: class option: reportchars = "" + disablepytestwarnings = True config.option.report = "xfailed" assert getreportopt(config) == "x" @@ -601,12 +602,21 @@ def test_getreportopt(): assert getreportopt(config) == "sx" config.option.report = "skipped" - config.option.reportchars = "sf" + config.option.reportchars = "sfw" assert getreportopt(config) == "sf" - config.option.reportchars = "sfx" + config.option.reportchars = "sfxw" assert getreportopt(config) == "sfx" + config.option.reportchars = "sfx" + config.option.disablepytestwarnings = False + assert getreportopt(config) == "sfxw" + + config.option.reportchars = "sfxw" + config.option.disablepytestwarnings = False + assert getreportopt(config) == "sfxw" + + def test_terminalreporter_reportopt_addopts(testdir): testdir.makeini("[pytest]\naddopts=-rs") testdir.makepyfile(""" From 1ce467006262a1369474919ca13b0c5831652cdc Mon Sep 17 00:00:00 2001 From: aostr Date: Sun, 26 Jun 2016 06:46:09 +0200 Subject: [PATCH 02/34] * removed tailing whitespaces --- _pytest/terminal.py | 2 +- testing/test_terminal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 3344203d9..0eb3696c4 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -26,7 +26,7 @@ def pytest_addoption(parser): "(p)passed, (P)passed with output, (a)all except pP. " "The pytest warnings are displayed at all times except when " "--disable-pytest-warnings is set") - group._addoption('--disable-pytest-warnings', default=False, + group._addoption('--disable-pytest-warnings', default=False, dest='disablepytestwarnings', action='store_true', help='disable warnings summary, overrides -r w flag') group._addoption('-l', '--showlocals', diff --git a/testing/test_terminal.py b/testing/test_terminal.py index bda5e858c..96ed80e08 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -613,7 +613,7 @@ def test_getreportopt(): assert getreportopt(config) == "sfxw" config.option.reportchars = "sfxw" - config.option.disablepytestwarnings = False + config.option.disablepytestwarnings = False assert getreportopt(config) == "sfxw" From b4e0fabf93adf533a987b1fc3b567030fce30414 Mon Sep 17 00:00:00 2001 From: aostr Date: Sun, 26 Jun 2016 06:52:36 +0200 Subject: [PATCH 03/34] * added missing link to the referenced issue --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index af7c6db95..eeb6ca66c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -53,6 +53,7 @@ .. _#1553: https://github.com/pytest-dev/pytest/issues/1553 .. _#1626: https://github.com/pytest-dev/pytest/pull/1626 .. _#1503: https://github.com/pytest-dev/pytest/issues/1503 +.. _#1668: https://github.com/pytest-dev/pytest/issues/1668 .. _@graingert: https://github.com/graingert .. _@taschini: https://github.com/taschini From 61e605f60ba430fc557edbbe908f4ddadd995be6 Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Tue, 5 Jul 2016 09:56:54 +0100 Subject: [PATCH 04/34] Making conftest import failures easier to debug --- AUTHORS | 1 + CHANGELOG.rst | 2 +- _pytest/config.py | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index fd543b145..c1dec8150 100644 --- a/AUTHORS +++ b/AUTHORS @@ -59,6 +59,7 @@ Jaap Broekhuizen Jan Balster Janne Vanhala Jason R. Coombs +Javier Domingo Cansino John Towler Joshua Bronson Jurko Gospodnetić diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e67cffc70..6d754d929 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ level an error will be raised during collection. Thanks `@omarkohl`_ for the complete PR (`#1519`_). -* +* Fix (`1516`_): ConftestImportFailure now shows the traceback **Incompatible changes** diff --git a/_pytest/config.py b/_pytest/config.py index 463d8f04f..577425d6a 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -25,6 +25,12 @@ class ConftestImportFailure(Exception): self.path = path self.excinfo = excinfo + def __str__(self): + etype, evalue, etb = self.excinfo + formatted = traceback.format_tb(etb) + # The level of the tracebacks we want to print is hand crafted :( + return repr(evalue) + '\n' + ''.join(formatted[2:]) + def main(args=None, plugins=None): """ return exit code, after performing an in-process test run. From 0171cfa30f188dd1e9a2475c47dd654042e4669e Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Tue, 5 Jul 2016 10:39:12 +0100 Subject: [PATCH 05/34] Fixing link to issue and creating testcase that shows that it finds the line in the stderr lines --- CHANGELOG.rst | 3 ++- testing/test_conftest.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6d754d929..2dc03d693 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ level an error will be raised during collection. Thanks `@omarkohl`_ for the complete PR (`#1519`_). -* Fix (`1516`_): ConftestImportFailure now shows the traceback +* Fix (`#1516`_): ConftestImportFailure now shows the traceback **Incompatible changes** @@ -30,6 +30,7 @@ * removed support code for python 3 < 3.3 addressing (`#1627`_) .. _#607: https://github.com/pytest-dev/pytest/issues/607 +.. _#1516: https://github.com/pytest-dev/pytest/issues/1516 .. _#1519: https://github.com/pytest-dev/pytest/pull/1519 .. _#1664: https://github.com/pytest-dev/pytest/pull/1664 .. _#1627: https://github.com/pytest-dev/pytest/pull/1627 diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 6f5e77f6d..54a7d4009 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -407,3 +407,16 @@ def test_issue1073_conftest_special_objects(testdir): """) res = testdir.runpytest() assert res.ret == 0 + + +def test_conftest_exception_handling(testdir): + testdir.makeconftest(''' + raise ValueError() + ''') + testdir.makepyfile(""" + def test_some(): + pass + """) + res = testdir.runpytest() + assert res.ret == 4 + assert 'raise ValueError()' in [line.strip() for line in res.errlines] From 94e4a2dd6787573807dd9115ac71cb7d89c65cb0 Mon Sep 17 00:00:00 2001 From: aostr Date: Tue, 5 Jul 2016 15:22:27 +0200 Subject: [PATCH 06/34] * implemented changes recommended by nicoddemus --- CHANGELOG.rst | 4 ++-- _pytest/terminal.py | 2 +- testing/test_config.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eeb6ca66c..b3eb6e7e1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,8 +36,8 @@ Thanks `@bagerard`_ for reporting (`#1503`_). Thanks to `@davehunt`_ and `@tomviner`_ for PR. -* Whitelisted pytest warnings to show up warnings summary by default. Added a new - flag ``--disable-pytest-warnings`` to explicitly disable the warnings summary. +* Now pytest warnings summary is shown up by default. Added a new flag + ``--disable-pytest-warnings`` to explicitly disable the warnings summary. This change resolves the (`#1668`_). * Renamed the pytest ``pdb`` module (plugin) into ``debugging``. diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 0eb3696c4..ed58c4d2f 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -22,7 +22,7 @@ def pytest_addoption(parser): group._addoption('-r', action="store", dest="reportchars", default='', metavar="chars", help="show extra test summary info as specified by chars (f)ailed, " - "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings " + "(E)error, (s)skipped, (x)failed, (X)passed, " "(p)passed, (P)passed with output, (a)all except pP. " "The pytest warnings are displayed at all times except when " "--disable-pytest-warnings is set") diff --git a/testing/test_config.py b/testing/test_config.py index c90555756..f6511b8d4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -517,11 +517,11 @@ class TestWarning: def test_hello(fix): pass """) - result = testdir.runpytest() + result = testdir.runpytest("--disable-pytest-warnings") assert result.parseoutcomes()["pytest-warnings"] > 0 - assert "hello" in result.stdout.str() + assert "hello" not in result.stdout.str() - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines(""" ===*pytest-warning summary*=== *WT1*test_warn_on_test_item*:5*hello* From 6383b53ad91a5f6b1727190cb91c19243becede5 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 9 Jul 2016 20:55:43 -0700 Subject: [PATCH 07/34] Allow passing a custom Pdb subclass via --pdbcls. This obviates the need for plugins such as `pytest-ipdb`; instead one can simply call `py.test --pdb=IPython.core.debugger:Pdb` --- AUTHORS | 1 + CHANGELOG.rst | 3 ++- _pytest/debugging.py | 24 ++++++++++++++++++------ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index fd543b145..67111393c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Anatoly Bubenkoff Andreas Zeidler Andy Freeland Anthon van der Neut +Antony Lee Armin Rigo Aron Curzon Aviv Palivoda diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 593b81fa6..8d00c18d0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -185,7 +185,8 @@ Before, you only got exceptions later from ``argparse`` library, giving no clue about the actual reason for double-added options. -* +* Allow passing a custom debugger class (e.g. ``IPython.core.debugger:Pdb`` + via ``--pdbcls``. * diff --git a/_pytest/debugging.py b/_pytest/debugging.py index 84c920d17..dca2978a9 100644 --- a/_pytest/debugging.py +++ b/_pytest/debugging.py @@ -8,21 +8,32 @@ import pytest def pytest_addoption(parser): group = parser.getgroup("general") - group._addoption('--pdb', - action="store_true", dest="usepdb", default=False, - help="start the interactive Python debugger on errors.") + group._addoption( + '--pdb', dest="usepdb", action="store_true", + help="start the interactive Python debugger on errors.") + group._addoption( + '--pdbcls', dest="usepdb_cls", metavar="modulename:classname", + help="start a custom interactive Python debugger on errors.") def pytest_namespace(): return {'set_trace': pytestPDB().set_trace} def pytest_configure(config): - if config.getvalue("usepdb"): + if config.getvalue("usepdb") or config.getvalue("usepdb_cls"): config.pluginmanager.register(PdbInvoke(), 'pdbinvoke') + if config.getvalue("usepdb_cls"): + modname, classname = config.getvalue("usepdb_cls").split(":") + __import__(modname) + pdb_cls = getattr(sys.modules[modname], classname) + else: + pdb_cls = pdb.Pdb + pytestPDB._pdb_cls = pdb_cls old = (pdb.set_trace, pytestPDB._pluginmanager) def fin(): pdb.set_trace, pytestPDB._pluginmanager = old pytestPDB._config = None + pytestPDB._pdb_cls = pdb.Pdb pdb.set_trace = pytest.set_trace pytestPDB._pluginmanager = config.pluginmanager pytestPDB._config = config @@ -32,6 +43,7 @@ class pytestPDB: """ Pseudo PDB that defers to the real pdb. """ _pluginmanager = None _config = None + _pdb_cls = pdb.Pdb def set_trace(self): """ invoke PDB set_trace debugging, dropping any IO capturing. """ @@ -45,7 +57,7 @@ class pytestPDB: tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") self._pluginmanager.hook.pytest_enter_pdb(config=self._config) - pdb.Pdb().set_trace(frame) + self._pdb_cls().set_trace(frame) class PdbInvoke: @@ -98,7 +110,7 @@ def _find_last_non_hidden_frame(stack): def post_mortem(t): - class Pdb(pdb.Pdb): + class Pdb(pytestPDB._pdb_cls): def get_stack(self, f, t): stack, i = pdb.Pdb.get_stack(self, f, t) if f is None: From 5506dc700c8fe418f28193241fdc544d14f6de34 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2016 21:06:35 -0300 Subject: [PATCH 08/34] Deprecate yield tests Closes #16 Closes #1324 --- CHANGELOG.rst | 3 ++- _pytest/python.py | 4 ++-- testing/acceptance_test.py | 15 +++++++++++++++ testing/code/test_source.py | 10 +++++----- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 593b81fa6..7213e772e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -185,7 +185,8 @@ Before, you only got exceptions later from ``argparse`` library, giving no clue about the actual reason for double-added options. -* +* ``yield``-based tests are considered deprecated and will be removed in pytest-4.0. + Thanks `@nicoddemus`_ for the PR. * diff --git a/_pytest/python.py b/_pytest/python.py index 79502ce1a..4a4cd5dc4 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -589,6 +589,8 @@ class Generator(FunctionMixin, PyCollector): raise ValueError("%r generated tests with non-unique name %r" %(self, name)) seen[name] = True l.append(self.Function(name, self, args=args, callobj=call)) + msg = 'yield tests are deprecated, and scheduled to be removed in pytest 4.0' + self.config.warn('C1', msg, fslocation=self.fspath) return l def getcallargs(self, obj): @@ -611,8 +613,6 @@ def hasinit(obj): return True - - class CallSpec2(object): def __init__(self, metafunc): self.metafunc = metafunc diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c5401cda6..ff56da3b1 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -762,3 +762,18 @@ class TestDurationWithFixture: * setup *test_1* * call *test_1* """) + + +def test_yield_tests_deprecation(testdir): + testdir.makepyfile(""" + def func1(arg, arg2): + assert arg == arg2 + def test_gen(): + yield "m1", func1, 15, 3*5 + yield "m2", func1, 42, 6*7 + """) + result = testdir.runpytest('-ra') + result.stdout.fnmatch_lines([ + '*yield tests are deprecated, and scheduled to be removed in pytest 4.0*', + '*2 passed*', + ]) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index e78f4b241..13bfccd54 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -285,13 +285,14 @@ class TestSourceParsingAndCompiling: #print "block", str(block) assert str(stmt).strip().startswith('assert') - def test_compilefuncs_and_path_sanity(self): + @pytest.mark.parametrize('name', ['', None, 'my']) + def test_compilefuncs_and_path_sanity(self, name): def check(comp, name): co = comp(self.source, name) if not name: - expected = "codegen %s:%d>" %(mypath, mylineno+2+1) + expected = "codegen %s:%d>" %(mypath, mylineno+2+2) else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno+2+1) + expected = "codegen %r %s:%d>" % (name, mypath, mylineno+2+2) fn = co.co_filename assert fn.endswith(expected) @@ -300,8 +301,7 @@ class TestSourceParsingAndCompiling: mypath = mycode.path for comp in _pytest._code.compile, _pytest._code.Source.compile: - for name in '', None, 'my': - yield check, comp, name + check(comp, name) def test_offsetless_synerr(self): pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode='eval') From ad4125dc0d0c941f50ae42aa61663a3895a6fd9f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2016 21:12:50 -0300 Subject: [PATCH 09/34] Deprecate "pytest_funcarg__" prefix to declare fixtures Fixes #1684 --- CHANGELOG.rst | 5 ++++- _pytest/fixtures.py | 5 +++++ testing/acceptance_test.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7213e772e..0d55d73fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -188,7 +188,9 @@ * ``yield``-based tests are considered deprecated and will be removed in pytest-4.0. Thanks `@nicoddemus`_ for the PR. -* +* Using ``pytest_funcarg__`` prefix to declare fixtures is considered deprecated and will be + removed in pytest-4.0 (`#1684`_). + Thanks `@nicoddemus`_ for the PR. * @@ -262,6 +264,7 @@ .. _#1632: https://github.com/pytest-dev/pytest/issues/1632 .. _#1633: https://github.com/pytest-dev/pytest/pull/1633 .. _#1664: https://github.com/pytest-dev/pytest/pull/1664 +.. _#1684: https://github.com/pytest-dev/pytest/pull/1684 .. _@DRMacIver: https://github.com/DRMacIver .. _@RedBeardCode: https://github.com/RedBeardCode diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index fcae06bdc..0260e14e2 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -865,6 +865,10 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) defaultfuncargprefixmarker = fixture() +funcarg_prefix_warning = 'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' \ + 'and scheduled to be removed in pytest 4.0.\n' \ + 'remove the prefix and use the @pytest.fixture decorator instead' + @fixture(scope="session") @@ -1043,6 +1047,7 @@ class FixtureManager: continue marker = defaultfuncargprefixmarker name = name[len(self._argprefix):] + self.config.warn('C1', funcarg_prefix_warning) elif not isinstance(marker, FixtureFunctionMarker): # magic globals with __getattr__ might have got us a wrong # fixture attribute diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ff56da3b1..72cd18f11 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -777,3 +777,19 @@ def test_yield_tests_deprecation(testdir): '*yield tests are deprecated, and scheduled to be removed in pytest 4.0*', '*2 passed*', ]) + + +def test_funcarg_prefix_deprecation(testdir): + testdir.makepyfile(""" + def pytest_funcarg__value(): + return 10 + + def test_funcarg_prefix(value): + assert value == 10 + """) + result = testdir.runpytest('-ra') + result.stdout.fnmatch_lines([ + '*declaring fixtures using "pytest_funcarg__" prefix is deprecated and scheduled to be removed in pytest 4.0*', + '*remove the prefix and use the @pytest.fixture decorator instead*', + '*1 passed*', + ]) From 458ecae1df0ede57182db685e7b9b3b8f0c7a652 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 11 Jul 2016 22:03:53 -0300 Subject: [PATCH 10/34] Replace all usages of "pytest_funcarg__" for @pytest.fixture --- CHANGELOG.rst | 3 +- _pytest/assertion/__init__.py | 4 +- _pytest/fixtures.py | 8 +- _pytest/monkeypatch.py | 11 ++- _pytest/pytester.py | 3 +- _pytest/tmpdir.py | 4 +- testing/code/test_excinfo.py | 4 +- testing/python/collect.py | 11 ++- testing/python/fixture.py | 176 ++++++++++++++++++++++++---------- testing/python/integration.py | 7 +- testing/python/metafunc.py | 37 ++++--- testing/test_assertion.py | 9 +- testing/test_junitxml.py | 17 +++- testing/test_mark.py | 3 +- testing/test_monkeypatch.py | 7 +- testing/test_pdb.py | 5 +- testing/test_runner.py | 4 +- testing/test_runner_xunit.py | 3 +- testing/test_skipping.py | 6 +- testing/test_terminal.py | 12 ++- testing/test_unittest.py | 4 +- 21 files changed, 237 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0d55d73fd..c7b7982de 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,7 +40,8 @@ 2])`` only ran once. Now a failure is raised. Fixes `#460`_. Thanks to `@nikratio`_ for bug report, `@RedBeardCode`_ and `@tomviner`_ for PR. -* +* ``_pytest.monkeypatch.monkeypatch`` class has been renamed to ``_pytest.monkeypatch.MonkeyPatch`` + so it doesn't conflict with the ``monkeypatch`` fixture. * diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index 4f28c8da7..dd30e1471 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -6,7 +6,7 @@ import os import sys from _pytest.config import hookimpl -from _pytest.monkeypatch import monkeypatch +from _pytest.monkeypatch import MonkeyPatch from _pytest.assertion import util @@ -56,7 +56,7 @@ def pytest_load_initial_conftests(early_config, parser, args): if mode != "plain": _load_modules(mode) - m = monkeypatch() + m = MonkeyPatch() early_config._cleanup.append(m.undo) m.setattr(py.builtin.builtins, 'AssertionError', reinterpret.AssertionError) # noqa diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 0260e14e2..5dbd1205d 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -865,7 +865,7 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) defaultfuncargprefixmarker = fixture() -funcarg_prefix_warning = 'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' \ +funcarg_prefix_warning = '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' \ 'and scheduled to be removed in pytest 4.0.\n' \ 'remove the prefix and use the @pytest.fixture decorator instead' @@ -1046,8 +1046,8 @@ class FixtureManager: if not callable(obj): continue marker = defaultfuncargprefixmarker + self.config.warn('C1', funcarg_prefix_warning.format(name=name)) name = name[len(self._argprefix):] - self.config.warn('C1', funcarg_prefix_warning) elif not isinstance(marker, FixtureFunctionMarker): # magic globals with __getattr__ might have got us a wrong # fixture attribute @@ -1055,7 +1055,9 @@ class FixtureManager: else: if marker.name: name = marker.name - assert not name.startswith(self._argprefix), name + msg = 'fixtures cannot have "pytest_funcarg__" prefix ' \ + 'and be decorated with @pytest.fixture:\n%s' % name + assert not name.startswith(self._argprefix), msg fixturedef = FixtureDef(self, nodeid, name, obj, marker.scope, marker.params, unittest=unittest, ids=marker.ids) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index d4c169d37..d1de4a679 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -5,11 +5,14 @@ import re from py.builtin import _basestring +import pytest + RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") -def pytest_funcarg__monkeypatch(request): - """The returned ``monkeypatch`` funcarg provides these +@pytest.fixture +def monkeypatch(request): + """The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: monkeypatch.setattr(obj, name, value, raising=True) @@ -26,7 +29,7 @@ def pytest_funcarg__monkeypatch(request): parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. """ - mpatch = monkeypatch() + mpatch = MonkeyPatch() request.addfinalizer(mpatch.undo) return mpatch @@ -93,7 +96,7 @@ class Notset: notset = Notset() -class monkeypatch: +class MonkeyPatch: """ Object keeping a record of setattr/item/env/syspath changes. """ def __init__(self): diff --git a/_pytest/pytester.py b/_pytest/pytester.py index fa63219d8..fc5b3ebd9 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -321,7 +321,8 @@ def linecomp(request): return LineComp() -def pytest_funcarg__LineMatcher(request): +@pytest.fixture(name='LineMatcher') +def LineMatcher_fixture(request): return LineMatcher diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py index ebc48dbe5..88e7250db 100644 --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -3,7 +3,7 @@ import re import pytest import py -from _pytest.monkeypatch import monkeypatch +from _pytest.monkeypatch import MonkeyPatch class TempdirFactory: @@ -92,7 +92,7 @@ def pytest_configure(config): available at pytest_configure time, but ideally should be moved entirely to the tmpdir_factory session fixture. """ - mp = monkeypatch() + mp = MonkeyPatch() t = TempdirFactory(config) config._cleanup.extend([mp.undo, t.finish]) mp.setattr(config, '_tmpdirhandler', t, raising=False) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 48696b5b0..2ccdd7028 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -381,7 +381,9 @@ def test_match_raises_error(testdir): ]) class TestFormattedExcinfo: - def pytest_funcarg__importasmod(self, request): + + @pytest.fixture + def importasmod(self, request): def importasmod(source): source = _pytest._code.Source(source) tmpdir = request.getfixturevalue("tmpdir") diff --git a/testing/python/collect.py b/testing/python/collect.py index 13f7aacf5..c4b9259bd 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -795,21 +795,24 @@ class TestTracebackCutting: def test_traceback_argsetup(self, testdir): testdir.makeconftest(""" - def pytest_funcarg__hello(request): + import pytest + + @pytest.fixture + def hello(request): raise ValueError("xyz") """) p = testdir.makepyfile("def test(hello): pass") result = testdir.runpytest(p) assert result.ret != 0 out = result.stdout.str() - assert out.find("xyz") != -1 - assert out.find("conftest.py:2: ValueError") != -1 + assert "xyz" in out + assert "conftest.py:5: ValueError" in out numentries = out.count("_ _ _") # separator for traceback entries assert numentries == 0 result = testdir.runpytest("--fulltrace", p) out = result.stdout.str() - assert out.find("conftest.py:2: ValueError") != -1 + assert "conftest.py:5: ValueError" in out numentries = out.count("_ _ _ _") # separator for traceback entries assert numentries > 3 diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 4f7e1294f..e2bca5286 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -30,7 +30,10 @@ class TestFillFixtures: def test_funcarg_lookupfails(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__xyzsomething(request): + import pytest + + @pytest.fixture + def xyzsomething(request): return 42 def test_func(some): @@ -46,9 +49,13 @@ class TestFillFixtures: def test_funcarg_basic(self, testdir): item = testdir.getitem(""" - def pytest_funcarg__some(request): + import pytest + + @pytest.fixture + def some(request): return request.function.__name__ - def pytest_funcarg__other(request): + @pytest.fixture + def other(request): return 42 def test_func(some, other): pass @@ -61,7 +68,10 @@ class TestFillFixtures: def test_funcarg_lookup_modulelevel(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__something(request): + import pytest + + @pytest.fixture + def something(request): return request.function.__name__ class TestClass: @@ -75,9 +85,13 @@ class TestFillFixtures: def test_funcarg_lookup_classlevel(self, testdir): p = testdir.makepyfile(""" + import pytest class TestClass: - def pytest_funcarg__something(self, request): + + @pytest.fixture + def something(self, request): return request.instance + def test_method(self, something): assert something is self """) @@ -91,12 +105,14 @@ class TestFillFixtures: sub2 = testdir.mkpydir("sub2") sub1.join("conftest.py").write(_pytest._code.Source(""" import pytest - def pytest_funcarg__arg1(request): + @pytest.fixture + def arg1(request): pytest.raises(Exception, "request.getfixturevalue('arg2')") """)) sub2.join("conftest.py").write(_pytest._code.Source(""" import pytest - def pytest_funcarg__arg2(request): + @pytest.fixture + def arg2(request): pytest.raises(Exception, "request.getfixturevalue('arg1')") """)) @@ -396,7 +412,10 @@ class TestFillFixtures: class TestRequestBasic: def test_request_attributes(self, testdir): item = testdir.getitem(""" - def pytest_funcarg__something(request): pass + import pytest + + @pytest.fixture + def something(request): pass def test_func(something): pass """) req = fixtures.FixtureRequest(item) @@ -410,8 +429,11 @@ class TestRequestBasic: def test_request_attributes_method(self, testdir): item, = testdir.getitems(""" + import pytest class TestB: - def pytest_funcarg__something(self, request): + + @pytest.fixture + def something(self, request): return 1 def test_func(self, something): pass @@ -420,9 +442,11 @@ class TestRequestBasic: assert req.cls.__name__ == "TestB" assert req.instance.__class__ == req.cls - def XXXtest_request_contains_funcarg_arg2fixturedefs(self, testdir): + def test_request_contains_funcarg_arg2fixturedefs(self, testdir): modcol = testdir.getmodulecol(""" - def pytest_funcarg__something(request): + import pytest + @pytest.fixture + def something(request): pass class TestClass: def test_method(self, something): @@ -432,15 +456,21 @@ class TestRequestBasic: assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs assert len(arg2fixturedefs) == 1 - assert arg2fixturedefs[0].__name__ == "pytest_funcarg__something" + assert arg2fixturedefs['something'][0].argname == "something" def test_getfixturevalue_recursive(self, testdir): testdir.makeconftest(""" - def pytest_funcarg__something(request): + import pytest + + @pytest.fixture + def something(request): return 1 """) testdir.makepyfile(""" - def pytest_funcarg__something(request): + import pytest + + @pytest.fixture + def something(request): return request.getfixturevalue("something") + 1 def test_func(something): assert something == 2 @@ -452,9 +482,12 @@ class TestRequestBasic: 'getfixmethod', ('getfixturevalue', 'getfuncargvalue')) def test_getfixturevalue(self, testdir, getfixmethod): item = testdir.getitem(""" + import pytest l = [2] - def pytest_funcarg__something(request): return 1 - def pytest_funcarg__other(request): + @pytest.fixture + def something(request): return 1 + @pytest.fixture + def other(request): return l.pop() def test_func(something): pass """) @@ -477,8 +510,10 @@ class TestRequestBasic: def test_request_addfinalizer(self, testdir): item = testdir.getitem(""" + import pytest teardownlist = [] - def pytest_funcarg__something(request): + @pytest.fixture + def something(request): request.addfinalizer(lambda: teardownlist.append(1)) def test_func(something): pass """) @@ -503,7 +538,8 @@ class TestRequestBasic: result = testdir.runpytest_subprocess() assert result.ret != 0 result.stdout.fnmatch_lines([ - "*AssertionError:*pytest_funcarg__marked_with_prefix_and_decorator*" + "*AssertionError: fixtures cannot have*@pytest.fixture*", + "*pytest_funcarg__marked_with_prefix_and_decorator*" ]) def test_request_addfinalizer_failing_setup(self, testdir): @@ -541,8 +577,10 @@ class TestRequestBasic: def test_request_addfinalizer_partial_setup_failure(self, testdir): p = testdir.makepyfile(""" + import pytest l = [] - def pytest_funcarg__something(request): + @pytest.fixture + def something(request): request.addfinalizer(lambda: l.append(None)) def test_func(something, missingarg): pass @@ -583,9 +621,11 @@ class TestRequestBasic: def test_funcargnames_compatattr(self, testdir): testdir.makepyfile(""" + import pytest def pytest_generate_tests(metafunc): assert metafunc.funcargnames == metafunc.fixturenames - def pytest_funcarg__fn(request): + @pytest.fixture + def fn(request): assert request._pyfuncitem.funcargnames == \ request._pyfuncitem.fixturenames return request.funcargnames, request.fixturenames @@ -630,7 +670,9 @@ class TestRequestBasic: # this tests that normalization of nodeids takes place b = testdir.mkdir("tests").mkdir("unit") b.join("conftest.py").write(_pytest._code.Source(""" - def pytest_funcarg__arg1(): + import pytest + @pytest.fixture + def arg1(): pass """)) p = b.join("test_module.py") @@ -678,7 +720,10 @@ class TestRequestBasic: class TestRequestMarking: def test_applymarker(self, testdir): item1,item2 = testdir.getitems(""" - def pytest_funcarg__something(request): + import pytest + + @pytest.fixture + def something(request): pass class TestClass: def test_func1(self, something): @@ -737,7 +782,10 @@ class TestRequestCachedSetup: reprec = testdir.inline_runsource(""" mysetup = ["hello",].pop - def pytest_funcarg__something(request): + import pytest + + @pytest.fixture + def something(request): return request.cached_setup(mysetup, scope="module") def test_func1(something): @@ -752,7 +800,9 @@ class TestRequestCachedSetup: reprec = testdir.inline_runsource(""" mysetup = ["hello", "hello2", "hello3"].pop - def pytest_funcarg__something(request): + import pytest + @pytest.fixture + def something(request): return request.cached_setup(mysetup, scope="class") def test_func1(something): assert something == "hello3" @@ -802,9 +852,13 @@ class TestRequestCachedSetup: def test_request_cached_setup_two_args(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__arg1(request): + import pytest + + @pytest.fixture + def arg1(request): return request.cached_setup(lambda: 42) - def pytest_funcarg__arg2(request): + @pytest.fixture + def arg2(request): return request.cached_setup(lambda: 17) def test_two_different_setups(arg1, arg2): assert arg1 != arg2 @@ -816,10 +870,14 @@ class TestRequestCachedSetup: def test_request_cached_setup_getfixturevalue(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__arg1(request): + import pytest + + @pytest.fixture + def arg1(request): arg1 = request.getfixturevalue("arg2") return request.cached_setup(lambda: arg1 + 1) - def pytest_funcarg__arg2(request): + @pytest.fixture + def arg2(request): return request.cached_setup(lambda: 10) def test_two_funcarg(arg1): assert arg1 == 11 @@ -831,8 +889,10 @@ class TestRequestCachedSetup: def test_request_cached_setup_functional(self, testdir): testdir.makepyfile(test_0=""" + import pytest l = [] - def pytest_funcarg__something(request): + @pytest.fixture + def something(request): val = request.cached_setup(fsetup, fteardown) return val def fsetup(mycache=[1]): @@ -858,7 +918,10 @@ class TestRequestCachedSetup: def test_issue117_sessionscopeteardown(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__app(request): + import pytest + + @pytest.fixture + def app(request): app = request.cached_setup( scope='session', setup=lambda: 0, @@ -1119,16 +1182,23 @@ class TestFixtureUsages: class TestFixtureManagerParseFactories: - def pytest_funcarg__testdir(self, request): + + @pytest.fixture + def testdir(self, request): testdir = request.getfixturevalue("testdir") testdir.makeconftest(""" - def pytest_funcarg__hello(request): + import pytest + + @pytest.fixture + def hello(request): return "conftest" - def pytest_funcarg__fm(request): + @pytest.fixture + def fm(request): return request._fixturemanager - def pytest_funcarg__item(request): + @pytest.fixture + def item(request): return request._pyfuncitem """) return testdir @@ -1154,17 +1224,21 @@ class TestFixtureManagerParseFactories: faclist = fm.getfixturedefs(name, item.nodeid) assert len(faclist) == 1 fac = faclist[0] - assert fac.func.__name__ == "pytest_funcarg__" + name + assert fac.func.__name__ == name """) reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) def test_parsefactories_conftest_and_module_and_class(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__hello(request): + import pytest + + @pytest.fixture + def hello(request): return "module" class TestClass: - def pytest_funcarg__hello(self, request): + @pytest.fixture + def hello(self, request): return "class" def test_hello(self, item, fm): faclist = fm.getfixturedefs("hello", item.nodeid) @@ -1212,7 +1286,9 @@ class TestFixtureManagerParseFactories: class TestAutouseDiscovery: - def pytest_funcarg__testdir(self, testdir): + + @pytest.fixture + def testdir(self, testdir): testdir.makeconftest(""" import pytest @pytest.fixture(autouse=True) @@ -1226,10 +1302,12 @@ class TestAutouseDiscovery: def perfunction2(arg1): pass - def pytest_funcarg__fm(request): + @pytest.fixture + def fm(request): return request._fixturemanager - def pytest_funcarg__item(request): + @pytest.fixture + def item(request): return request._pyfuncitem """) return testdir @@ -1773,17 +1851,19 @@ class TestFixtureMarker: def test_scope_module_and_finalizer(self, testdir): testdir.makeconftest(""" import pytest - finalized = [] - created = [] + finalized_list = [] + created_list = [] @pytest.fixture(scope="module") def arg(request): - created.append(1) + created_list.append(1) assert request.scope == "module" - request.addfinalizer(lambda: finalized.append(1)) - def pytest_funcarg__created(request): - return len(created) - def pytest_funcarg__finalized(request): - return len(finalized) + request.addfinalizer(lambda: finalized_list.append(1)) + @pytest.fixture + def created(request): + return len(created_list) + @pytest.fixture + def finalized(request): + return len(finalized_list) """) testdir.makepyfile( test_mod1=""" diff --git a/testing/python/integration.py b/testing/python/integration.py index 906e98321..237becd6f 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -15,7 +15,9 @@ class TestOEJSKITSpecials: return self.fspath, 3, "xyz" """) modcol = testdir.getmodulecol(""" - def pytest_funcarg__arg1(request): + import pytest + @pytest.fixture + def arg1(request): return 42 class MyClass: pass @@ -43,7 +45,8 @@ class TestOEJSKITSpecials: @pytest.fixture(autouse=True) def hello(): pass - def pytest_funcarg__arg1(request): + @pytest.fixture + def arg1(request): return 42 class MyClass: pass diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 46f460dbb..ac8e512d4 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -448,13 +448,13 @@ class TestMetafunc: def test_parametrize_functional(self, testdir): testdir.makepyfile(""" + import pytest def pytest_generate_tests(metafunc): metafunc.parametrize('x', [1,2], indirect=True) metafunc.parametrize('y', [2]) - def pytest_funcarg__x(request): + @pytest.fixture + def x(request): return request.param * 10 - #def pytest_funcarg__y(request): - # return request.param def test_simple(x,y): assert x in (10,20) @@ -578,7 +578,8 @@ class TestMetafuncFunctional: def pytest_generate_tests(metafunc): metafunc.addcall(param=metafunc) - def pytest_funcarg__metafunc(request): + @pytest.fixture + def metafunc(request): assert request._pyfuncitem._genid == "0" return request.param @@ -630,7 +631,9 @@ class TestMetafuncFunctional: metafunc.addcall(param=10) metafunc.addcall(param=20) - def pytest_funcarg__arg1(request): + import pytest + @pytest.fixture + def arg1(request): return request.param def test_func1(arg1): @@ -669,9 +672,12 @@ class TestMetafuncFunctional: def pytest_generate_tests(metafunc): metafunc.addcall(param=(1,1), id="hello") - def pytest_funcarg__arg1(request): + import pytest + @pytest.fixture + def arg1(request): return request.param[0] - def pytest_funcarg__arg2(request): + @pytest.fixture + def arg2(request): return request.param[1] class TestClass: @@ -755,11 +761,14 @@ class TestMetafuncFunctional: metafunc.parametrize("arg1", [1], indirect=True) metafunc.parametrize("arg2", [10], indirect=True) - def pytest_funcarg__arg1(request): + import pytest + @pytest.fixture + def arg1(request): x = request.getfixturevalue("arg2") return x + request.param - def pytest_funcarg__arg2(request): + @pytest.fixture + def arg2(request): return request.param def test_func1(arg1, arg2): @@ -777,10 +786,13 @@ class TestMetafuncFunctional: assert "arg1" in metafunc.fixturenames metafunc.parametrize("arg1", [1], indirect=True) - def pytest_funcarg__arg1(request): + import pytest + @pytest.fixture + def arg1(request): return request.param - def pytest_funcarg__arg2(request, arg1): + @pytest.fixture + def arg2(request, arg1): return 10 * arg1 def test_func(arg2): @@ -870,7 +882,8 @@ class TestMetafuncFunctional: if "arg" in metafunc.funcargnames: metafunc.parametrize("arg", [1,2], indirect=True, scope=%r) - def pytest_funcarg__arg(request): + @pytest.fixture + def arg(request): l.append(request.param) return request.param def test_hello(arg): diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 90f74ca7f..56cd73bd3 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -30,17 +30,20 @@ class TestBinReprIntegration: def test_pytest_assertrepr_compare_called(self, testdir): testdir.makeconftest(""" + import pytest l = [] def pytest_assertrepr_compare(op, left, right): l.append((op, left, right)) - def pytest_funcarg__l(request): + + @pytest.fixture + def list(request): return l """) testdir.makepyfile(""" def test_hello(): assert 0 == 1 - def test_check(l): - assert l == [("==", 0, 1)] + def test_check(list): + assert list == [("==", 0, 1)] """) result = testdir.runpytest("-v") result.stdout.fnmatch_lines([ diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 8291ea148..e05875942 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -119,7 +119,10 @@ class TestPython: def test_setup_error(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__arg(request): + import pytest + + @pytest.fixture + def arg(request): raise ValueError() def test_function(arg): pass @@ -131,7 +134,7 @@ class TestPython: tnode = node.find_first_by_tag("testcase") tnode.assert_attr( file="test_setup_error.py", - line="2", + line="5", classname="test_setup_error", name="test_function") fnode = tnode.find_first_by_tag("error") @@ -444,7 +447,10 @@ class TestPython: def test_setup_error_captures_stdout(self, testdir): testdir.makepyfile(""" - def pytest_funcarg__arg(request): + import pytest + + @pytest.fixture + def arg(request): print('hello-stdout') raise ValueError() def test_function(arg): @@ -459,7 +465,10 @@ class TestPython: def test_setup_error_captures_stderr(self, testdir): testdir.makepyfile(""" import sys - def pytest_funcarg__arg(request): + import pytest + + @pytest.fixture + def arg(request): sys.stderr.write('hello-stderr') raise ValueError() def test_function(arg): diff --git a/testing/test_mark.py b/testing/test_mark.py index cf59baed9..e0bf3c3c8 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -481,7 +481,8 @@ class TestFunctional: def test_mark_dynamically_in_funcarg(self, testdir): testdir.makeconftest(""" import pytest - def pytest_funcarg__arg(request): + @pytest.fixture + def arg(request): request.applymarker(pytest.mark.hello) def pytest_terminal_summary(terminalreporter): l = terminalreporter.stats['passed'] diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 048c942c8..7599be47f 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -3,10 +3,11 @@ import sys import textwrap import pytest -from _pytest.monkeypatch import monkeypatch as MonkeyPatch +from _pytest.monkeypatch import MonkeyPatch -def pytest_funcarg__mp(request): +@pytest.fixture +def mp(request): cwd = os.getcwd() sys_path = list(sys.path) @@ -205,7 +206,7 @@ def test_setenv_prepend(): def test_monkeypatch_plugin(testdir): reprec = testdir.inline_runsource(""" def test_method(monkeypatch): - assert monkeypatch.__class__.__name__ == "monkeypatch" + assert monkeypatch.__class__.__name__ == "MonkeyPatch" """) res = reprec.countoutcomes() assert tuple(res) == (1, 0, 0), res diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 44163a204..14c1fe8b8 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1,6 +1,7 @@ import sys import _pytest._code +import pytest def runpdb_and_get_report(testdir, source): @@ -12,7 +13,9 @@ def runpdb_and_get_report(testdir, source): class TestPDB: - def pytest_funcarg__pdblist(self, request): + + @pytest.fixture + def pdblist(self, request): monkeypatch = request.getfixturevalue("monkeypatch") pdblist = [] def mypdb(*args): diff --git a/testing/test_runner.py b/testing/test_runner.py index 7db809b24..5826d9601 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -399,13 +399,15 @@ def test_callinfo(): @pytest.mark.xfail def test_runtest_in_module_ordering(testdir): p1 = testdir.makepyfile(""" + import pytest def pytest_runtest_setup(item): # runs after class-level! item.function.mylist.append("module") class TestClass: def pytest_runtest_setup(self, item): assert not hasattr(item.function, 'mylist') item.function.mylist = ['class'] - def pytest_funcarg__mylist(self, request): + @pytest.fixture + def mylist(self, request): return request.function.mylist def pytest_runtest_call(self, item, __multicall__): try: diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index f32a1311b..dc8ae9992 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -234,7 +234,8 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir): import pytest def setup_module(mod): raise ValueError(42) - def pytest_funcarg__hello(request): + @pytest.fixture + def hello(request): raise ValueError("xyz43") def test_function1(hello): pass diff --git a/testing/test_skipping.py b/testing/test_skipping.py index d2a925a4e..bc719b142 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -309,7 +309,8 @@ class TestXFail: def test_dynamic_xfail_no_run(self, testdir): p = testdir.makepyfile(""" import pytest - def pytest_funcarg__arg(request): + @pytest.fixture + def arg(request): request.applymarker(pytest.mark.xfail(run=False)) def test_this(arg): assert 0 @@ -323,7 +324,8 @@ class TestXFail: def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): p = testdir.makepyfile(""" import pytest - def pytest_funcarg__arg(request): + @pytest.fixture + def arg(request): request.applymarker(pytest.mark.xfail) def test_this2(arg): assert 0 diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0d1aac999..5d055e416 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -600,7 +600,10 @@ def test_getreportopt(): def test_terminalreporter_reportopt_addopts(testdir): testdir.makeini("[pytest]\naddopts=-rs") testdir.makepyfile(""" - def pytest_funcarg__tr(request): + import pytest + + @pytest.fixture + def tr(request): tr = request.config.pluginmanager.getplugin("terminalreporter") return tr def test_opt(tr): @@ -614,7 +617,10 @@ def test_terminalreporter_reportopt_addopts(testdir): def test_tbstyle_short(testdir): p = testdir.makepyfile(""" - def pytest_funcarg__arg(request): + import pytest + + @pytest.fixture + def arg(request): return 42 def test_opt(arg): x = 0 @@ -625,7 +631,7 @@ def test_tbstyle_short(testdir): assert 'arg = 42' not in s assert 'x = 0' not in s result.stdout.fnmatch_lines([ - "*%s:5*" % p.basename, + "*%s:8*" % p.basename, " assert x", "E assert*", ]) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 73735b8cd..88c117657 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -265,8 +265,8 @@ def test_testcase_custom_exception_info(testdir, type): def run(self, result): excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0) # we fake an incompatible exception info - from _pytest.monkeypatch import monkeypatch - mp = monkeypatch() + from _pytest.monkeypatch import MonkeyPatch + mp = MonkeyPatch() def t(*args): mp.undo() raise TypeError() From 7ee3dd1cb5e40056283f1b2b39c2586429ac55bf Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 11 Jul 2016 17:43:06 -0700 Subject: [PATCH 11/34] Add tests for custom pdb class. (and edit CHANGELOG) --- CHANGELOG.rst | 7 ++++--- _pytest/debugging.py | 3 ++- testing/test_pdb.py | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8d00c18d0..d56161e20 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -116,7 +116,8 @@ Example '-o xfail_strict=True'. A complete ini-options can be viewed by py.test --help. Thanks `@blueyed`_ and `@fengxx`_ for the PR -* +* Allow passing a custom debugger class (e.g. ``IPython.core.debugger:Pdb`` + via ``--pdbcls``). Thanks to `@anntzer`_ for the PR. * @@ -185,8 +186,7 @@ Before, you only got exceptions later from ``argparse`` library, giving no clue about the actual reason for double-added options. -* Allow passing a custom debugger class (e.g. ``IPython.core.debugger:Pdb`` - via ``--pdbcls``. +* * @@ -266,6 +266,7 @@ .. _@DRMacIver: https://github.com/DRMacIver .. _@RedBeardCode: https://github.com/RedBeardCode .. _@Vogtinator: https://github.com/Vogtinator +.. _@anntzer: https://github.com/anntzer .. _@bagerard: https://github.com/bagerard .. _@blueyed: https://github.com/blueyed .. _@ceridwen: https://github.com/ceridwen diff --git a/_pytest/debugging.py b/_pytest/debugging.py index dca2978a9..d8cd5a4e3 100644 --- a/_pytest/debugging.py +++ b/_pytest/debugging.py @@ -13,7 +13,8 @@ def pytest_addoption(parser): help="start the interactive Python debugger on errors.") group._addoption( '--pdbcls', dest="usepdb_cls", metavar="modulename:classname", - help="start a custom interactive Python debugger on errors.") + help="start a custom interactive Python debugger on errors. " + "For example: --pdbcls=IPython.core.debugger:Pdb") def pytest_namespace(): return {'set_trace': pytestPDB().set_trace} diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 44163a204..6df2b496f 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -311,3 +311,28 @@ class TestPDB: child.sendeof() if child.isalive(): child.wait() + + def test_pdb_custom_cls(self, testdir): + called = [] + + # install dummy debugger class and track which methods were called on it + class _CustomPdb: + def __init__(self, *args, **kwargs): + called.append("init") + + def reset(self): + called.append("reset") + + def interaction(self, *args): + called.append("interaction") + + _pytest._CustomPdb = _CustomPdb + + p1 = testdir.makepyfile("""xxx """) + result = testdir.runpytest_inprocess( + "--pdbcls=_pytest:_CustomPdb", p1) + result.stdout.fnmatch_lines([ + "*NameError*xxx*", + "*1 error*", + ]) + assert called == ["init", "reset", "interaction"] From 15e97a7c7842f6947d4750f75b04d6d7fde51507 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Jul 2016 23:49:09 +0200 Subject: [PATCH 12/34] Add punctuation to funcarg_prefix_warning --- _pytest/fixtures.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 5dbd1205d..38e96e891 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -864,11 +864,12 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N else: return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) -defaultfuncargprefixmarker = fixture() -funcarg_prefix_warning = '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' \ - 'and scheduled to be removed in pytest 4.0.\n' \ - 'remove the prefix and use the @pytest.fixture decorator instead' +defaultfuncargprefixmarker = fixture() +funcarg_prefix_warning = ( + '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' + 'and scheduled to be removed in pytest 4.0.\n' + 'Please remove the prefix and use the @pytest.fixture decorator instead.') @fixture(scope="session") @@ -877,8 +878,6 @@ def pytestconfig(request): return request.config - - class FixtureManager: """ pytest fixtures definitions and information is stored and managed From f827810fa8396970c68346d622f7f9c9e6bda6b2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 12 Jul 2016 21:02:40 -0300 Subject: [PATCH 13/34] Fix 2.10 -> 3.0 versions in docs --- doc/en/assert.rst | 2 +- doc/en/capture.rst | 2 +- doc/en/doctest.rst | 2 +- doc/en/usage.rst | 2 +- doc/en/yieldfixture.rst | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index bde51fd35..2074cfe53 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -85,7 +85,7 @@ and if you need to have access to the actual exception info you may use:: the actual exception raised. The main attributes of interest are ``.type``, ``.value`` and ``.traceback``. -.. versionchanged:: 2.10 +.. versionchanged:: 3.0 In the context manager form you may use the keyword argument ``message`` to specify a custom failure message:: diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 0004f5b18..8128419e1 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -116,7 +116,7 @@ libraries or subprocesses that directly write to operating system level output streams (FD1 and FD2). -.. versionadded:: 2.10 +.. versionadded:: 3.0 To temporarily disable capture within a test, both ``capsys`` and ``capfd`` have a ``disabled()`` method that can be used diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 80f5c9506..b9b19cb85 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -105,7 +105,7 @@ itself:: The 'doctest_namespace' fixture ------------------------------- -.. versionadded:: 2.10 +.. versionadded:: 3.0 The ``doctest_namespace`` fixture can be used to inject items into the namespace in which your doctests run. It is intended to be used within diff --git a/doc/en/usage.rst b/doc/en/usage.rst index c2a269a15..351ad526a 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -204,7 +204,7 @@ This will add an extra property ``example_key="1"`` to the generated LogXML: add_global_property ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. versionadded:: 2.10 +.. versionadded:: 3.0 If you want to add a properties node in the testsuite level, which may contains properties that are relevant to all testcases you can use ``LogXML.add_global_properties`` diff --git a/doc/en/yieldfixture.rst b/doc/en/yieldfixture.rst index b372973e5..f69b38eb3 100644 --- a/doc/en/yieldfixture.rst +++ b/doc/en/yieldfixture.rst @@ -3,12 +3,12 @@ "yield_fixture" functions --------------------------------------------------------------- -.. deprecated:: 2.10 +.. deprecated:: 3.0 .. versionadded:: 2.4 .. important:: - Since pytest-2.10, fixtures using the normal ``fixture`` decorator can use a ``yield`` + Since pytest-3.0, fixtures using the normal ``fixture`` decorator can use a ``yield`` statement to provide fixture values and execute teardown code, exactly like ``yield_fixture`` in previous versions. From 4a763accc5db692b1bec8b5fb3b26b18e162d50d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2016 00:15:22 -0300 Subject: [PATCH 14/34] Improve overall CHANGELOG formatting and consistency for 3.0 --- CHANGELOG.rst | 162 ++++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 786b44c29..6bed6ed42 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,42 +3,46 @@ **Incompatible changes** -* Removed the following deprecated commandline options +A number of incompatible changes were made in this release, with the intent of removing features deprecated for a long +time or change existing behaviors in order to make them less surprising/more useful. - * ``--genscript`` - * ``--no-assert`` - * ``--nomagic`` - * ``--report`` +* The following deprecated commandline options were removed: - Thanks to `@RedBeardCode`_ for the PR (`#1664`_) + * ``--genscript``: no longer supported; + * ``--no-assert``: use ``--assert=plain`` instead; + * ``--nomagic``: use ``--assert=plain`` instead; + * ``--report``: use ``-r`` instead; + + Thanks to `@RedBeardCode`_ for the PR (`#1664`_). * ImportErrors in plugins now are a fatal error instead of issuing a pytest warning (`#1479`_). Thanks to `@The-Compiler`_ for the PR. -* removed support code for python 3 < 3.3 addressing (`#1627`_) +* Removed support code for Python 3 versions < 3.3 (`#1627`_). -* Remove all py.test-X* entry points. The versioned, suffixed entry points +* Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points were never documented and a leftover from a pre-virtualenv era. These entry points also created broken entry points in wheels, so removing them also removes a source of confusion for users (`#1632`_). Thanks `@obestwalter`_ for the PR. -* Fix `#607`_: pytest.skip() is no longer allowed at module level to - prevent misleading use as test function decorator. When used at a module - level an error will be raised during collection. +* ``pytest.skip()`` now raises an error when used to decorate a test function, + as opposed to its original intent (to imperatively skip a test inside a test function). Previously + this usage would cause the entire module to be skipped (`#607`_). Thanks `@omarkohl`_ for the complete PR (`#1519`_). -* Fix `#1421`_: Exit tests if a collection error occurs and add - ``--continue-on-collection-errors`` option to restore previous behaviour. +* Exit tests if a collection error occurs. A poll indicated most users will hit CTRL-C + anyway as soon as they see collection errors, so pytest might as well make that the default behavior (`#1421`_). + A ``--continue-on-collection-errors`` option has been added to restore the previous behaviour. Thanks `@olegpidsadnyi`_ and `@omarkohl`_ for the complete PR (`#1628`_). -* Renamed the pytest ``pdb`` module (plugin) into ``debugging``. +* Renamed the pytest ``pdb`` module (plugin) into ``debugging`` to avoid clashes with the builtin ``pdb`` module. -* Raise helpful failure message, when requesting parametrized fixture at runtime, - e.g. with ``request.getfuncargvalue``. Previously these params were simply - never defined. So a fixture decorated like ``@pytest.fixture(params=[0, 1, - 2])`` only ran once. Now a failure is raised. Fixes `#460`_. Thanks to - `@nikratio`_ for bug report, `@RedBeardCode`_ and `@tomviner`_ for PR. +* Raise a helpful failure message when requesting a parametrized fixture at runtime, + e.g. with ``request.getfixturevalue``. Previously these parameters were simply + never defined, so a fixture decorated like ``@pytest.fixture(params=[0, 1, 2])`` + only ran once (`#460`_). + Thanks to `@nikratio`_ for the bug report, `@RedBeardCode`_ and `@tomviner`_ for the PR. * ``_pytest.monkeypatch.monkeypatch`` class has been renamed to ``_pytest.monkeypatch.MonkeyPatch`` so it doesn't conflict with the ``monkeypatch`` fixture. @@ -54,14 +58,14 @@ **New Features** * Support nose-style ``__test__`` attribute on methods of classes, - including unittest-style Classes. If set to False, the test will not be + including unittest-style Classes. If set to ``False``, the test will not be collected. * New ``doctest_namespace`` fixture for injecting names into the - namespace in which your doctests run. + namespace in which doctests run. Thanks `@milliams`_ for the complete PR (`#1428`_). -* New ``name`` argument to ``pytest.fixture`` mark, which allows a custom name +* New ``name`` argument to ``pytest.fixture`` decorator which allows a custom name for a fixture (to solve the funcarg-shadowing-fixture problem). Thanks `@novas0x2a`_ for the complete PR (`#1444`_). @@ -69,56 +73,65 @@ tests. Thanks `@kalekundert`_ for the complete PR (`#1441`_). -* New Add ability to add global properties in the final xunit output file. +* Ability to add global properties in the final xunit output file by accessing + the internal ``junitxml`` plugin (experimental). Thanks `@tareqalayan`_ for the complete PR `#1454`_). * New ``ExceptionInfo.match()`` method to match a regular expression on the - string representation of an exception. Closes proposal `#372`_. - Thanks `@omarkohl`_ for the complete PR (`#1502`_) and `@nicoddemus`_ for the - implementation tips. + string representation of an exception (`#372`_). + Thanks `@omarkohl`_ for the complete PR (`#1502`_). * ``__tracebackhide__`` can now also be set to a callable which then can decide whether to filter the traceback based on the ``ExceptionInfo`` object passed to it. Thanks `@The-Compiler`_ for the complete PR (`#1526`_). -* New ``pytest_make_parametrize_id`` hook. +* New ``pytest_make_parametrize_id(config, val)`` hook which can be used by plugins to provide + friendly strings for custom types. Thanks `@palaviv`_ for the PR. -* ``capsys`` and ``capfd`` now have a ``disabled()`` method, which is a context manager - that can be used to temporarily disable capture within a test. +* ``capsys`` and ``capfd`` now have a ``disabled()`` context-manager method, which + can be used to temporarily disable capture within a test. Thanks `@nicoddemus`_ for the PR. -* New cli flag ``--fixtures-per-test`` that shows which fixtures are being used +* New cli flag ``--fixtures-per-test``: shows which fixtures are being used for each selected test item. Features doc strings of fixtures by default. Can also show where fixtures are defined if combined with ``-v``. Thanks `@hackebrot`_ for the PR. -* Introduce pytest command as recommended entry point. Closes proposal +* Introduce ``pytest`` command as recommended entry point. Note that ``py.test`` + still works and is not scheduled for removal. Closes proposal `#1629`_. Thanks `@obestwalter`_ and `@davehunt`_ for the complete PR - (`#1633`_) + (`#1633`_). + +* New cli flags: + + + ``--setup-plan``: performs normal collection and reports + the potential setup and teardown and does not execute any fixtures and tests; + + ``--setup-only``: performs normal collection, executes setup and teardown of + fixtures and reports them; + + ``--setup-show``: performs normal test execution and additionally shows + setup and teardown of fixtures; -* New cli flags: (1) ``--setup-plan`` performs normal collection and reports - the potential setup and teardown, does not execute any fixtures and tests (2) - ``--setup-only`` performs normal collection, executes setup and teardown of - fixtures and reports them. (3) ``--setup-show`` performs normal test - execution and additionally shows the setup and teardown of fixtures. Thanks `@d6e`_, `@kvas-it`_, `@sallner`_ and `@omarkohl`_ for the PRs. -* Added two new hooks: ``pytest_fixture_setup`` which executes the fixture - setup and ``pytest_fixture_post_finalizer`` which is called after the fixture's - finalizer and has access to the fixture's result cache. - Thanks `@d6e`_, `@sallner`_ +* New cli flag ``--override-ini``/``-o``: overrides values from the ini file. + For example: ``"-o xfail_strict=True"``'. + Thanks `@blueyed`_ and `@fengxx`_ for the PR. -* Issue a warning for asserts whose test is a tuple literal. Such asserts will +* New hooks: + + + ``pytest_fixture_setup(fixturedef, request)``: executes fixture setup; + + ``pytest_fixture_post_finalizer(fixturedef)``: called after the fixture's + finalizer and has access to the fixture's result cache. + + Thanks `@d6e`_, `@sallner`_. + +* Issue warnings for asserts whose test is a tuple literal. Such asserts will never fail because tuples are always truthy and are usually a mistake (see `#1562`_). Thanks `@kvas-it`_, for the PR. -* New cli flag ``--override-ini`` or ``-o`` that overrides values from the ini file. - Example '-o xfail_strict=True'. A complete ini-options can be viewed - by py.test --help. Thanks `@blueyed`_ and `@fengxx`_ for the PR - -* Allow passing a custom debugger class (e.g. ``IPython.core.debugger:Pdb`` - via ``--pdbcls``). Thanks to `@anntzer`_ for the PR. +* Allow passing a custom debugger class (e.g. ``--pdbcls=IPython.core.debugger:Pdb``). + Thanks to `@anntzer`_ for the PR. * @@ -136,27 +149,26 @@ the preferred way to write teardown code (`#1461`_). Thanks `@csaftoiu`_ for bringing this to attention and `@nicoddemus`_ for the PR. -* Fix `#1351`_: - explicitly passed parametrize ids do not get escaped to ascii. +* Explicitly passed parametrize ids do not get escaped to ascii (`#1351`_). Thanks `@ceridwen`_ for the PR. -* parametrize ids can accept None as specific test id. The +* Parametrize ids can accept ``None`` as specific test id, in which case the automatically generated id for that argument will be used. Thanks `@palaviv`_ for the complete PR (`#1468`_). -* improved idmaker name selection in case of duplicate ids in +* Improved automatic id generation selection in case of duplicate ids in parametrize. Thanks `@palaviv`_ for the complete PR (`#1474`_). -* Fix `#1426`_ Make ImportError during collection more explicit by reminding - the user to check the name of the test module/package(s). +* Make ImportError during collection more explicit by reminding + the user to check the name of the test module/package(s) (`#1426`_). Thanks `@omarkohl`_ for the complete PR (`#1520`_). * Add ``build/`` and ``dist/`` to the default ``--norecursedirs`` list. Thanks `@mikofski`_ for the report and `@tomviner`_ for the PR (`#1544`_). -* pytest.raises in the context manager form accepts a custom - message to raise when no exception occurred. +* ``pytest.raises`` in the context manager form accepts a custom + ``message`` to raise when no exception occurred. Thanks `@palaviv`_ for the complete PR (`#1616`_). * ``conftest.py`` files now benefit from assertion rewriting; previously it @@ -170,17 +182,10 @@ is specified on the command line together with the ``--pyargs`` option. Thanks to `@taschini`_ for the PR (`#1597`_). -* Add proposal to docs for a new feature that enables users to combine multiple - fixtures into one. Thanks to `@hpk42`_ and `@hackebrot`_. - -* Rename ``getfuncargvalue`` to ``getfixturevalue``. ``getfuncargvalue`` is - deprecated but still present. Thanks to `@RedBeardCode`_ and `@tomviner`_ - for PR (`#1626`_). - -* Always include full assertion explanation. The previous behaviour was hiding - sub-expressions that happened to be False, assuming this was redundant information. +* Always include full assertion explanation during assertion rewriting. The previous behaviour was hiding + sub-expressions that happened to be ``False``, assuming this was redundant information. Thanks `@bagerard`_ for reporting (`#1503`_). Thanks to `@davehunt`_ and - `@tomviner`_ for PR. + `@tomviner`_ for the PR. * ``OptionGroup.addoption()`` now checks if option names were already added before, to make it easier to track down issues like `#1618`_. @@ -194,6 +199,10 @@ removed in pytest-4.0 (`#1684`_). Thanks `@nicoddemus`_ for the PR. +* Rename ``getfuncargvalue`` to ``getfixturevalue``. ``getfuncargvalue`` is + still present but is now considered deprecated. Thanks to `@RedBeardCode`_ and `@tomviner`_ + for the PR (`#1626`_). + * * @@ -202,22 +211,21 @@ **Bug Fixes** -* When receiving identical test ids in parametrize we generate unique test ids. +* Parametrize now correctly handles duplicated test ids. -* Fix internal error issue when ``method`` argument is missing for - ``teardown_method()``. Fixes `#1605`_. +* Fix internal error issue when the ``method`` argument is missing for + ``teardown_method()`` (`#1605`_). * Fix exception visualization in case the current working directory (CWD) gets - deleted during testing. Fixes `#1235`_. Thanks `@bukzor`_ for reporting. PR by - `@marscher`_. Thanks `@nicoddemus`_ for his help. + deleted during testing (`#1235`_). Thanks `@bukzor`_ for reporting. PR by + `@marscher`_. -* Improve of the test output for logical expression with brackets. - Fixes `#925`_. Thanks `@DRMacIver`_ for reporting. Thanks to `@RedBeardCode`_ - for PR. +* Improve test output for logical expression with brackets (`#925`_). + Thanks `@DRMacIver`_ for reporting and `@RedBeardCode`_ for the PR. -* Create correct diff for strings ending with newlines. Fixes `#1553`_. - Thanks `@Vogtinator`_ for reporting. Thanks to `@RedBeardCode`_ and - `@tomviner`_ for PR. +* Create correct diff for strings ending with newlines (`#1553`_). + Thanks `@Vogtinator`_ for reporting and `@RedBeardCode`_ and + `@tomviner`_ for the PR. * From a98e3cefc5b5cfea1fe322cf796e92b97cda7956 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 22 Jun 2016 12:42:11 +0200 Subject: [PATCH 15/34] Enable re-writing of setuptools-installed plugins Hook up the PEP 302 import hook very early in pytest startup so that it gets installed before setuptools-installed plugins are imported. Also iterate over all installed plugins and mark them for rewriting. If an installed plugin is already imported then a warning is issued, we can not break since that might break existing plugins and the fallback will still be gracefull to plain asserts. Some existing tests are failing in this commit because of the new warning triggered by inline pytest runs due to the hypothesis plugin already being imported. The tests will be fixed in the next commit. --- CHANGELOG.rst | 3 + _pytest/assertion/__init__.py | 73 +++++++--------------- _pytest/assertion/rewrite.py | 26 +++++++- _pytest/config.py | 52 +++++++++++++++- testing/test_assertion.py | 110 ++++++++++++++++++++++++++++++++++ testing/test_assertrewrite.py | 46 ++++---------- testing/test_config.py | 15 ++++- 7 files changed, 233 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6bed6ed42..4273bd73f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -143,6 +143,9 @@ time or change existing behaviors in order to make them less surprising/more use **Changes** +* Plugins now benefit from assertion rewriting. Thanks + `@sober7`_, `@nicoddemus`_ and `@flub`_ for the PR. + * Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like those marked with the ``@pytest.yield_fixture`` decorator. This change renders ``@pytest.yield_fixture`` deprecated and makes ``@pytest.fixture`` with ``yield`` statements diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index dd30e1471..746c810ee 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -5,8 +5,7 @@ import py import os import sys -from _pytest.config import hookimpl -from _pytest.monkeypatch import MonkeyPatch +from _pytest.monkeypatch import monkeypatch from _pytest.assertion import util @@ -35,10 +34,7 @@ class AssertionState: self.trace = config.trace.root.get("assertion") -@hookimpl(tryfirst=True) -def pytest_load_initial_conftests(early_config, parser, args): - ns, ns_unknown_args = parser.parse_known_and_unknown_args(args) - mode = ns.assertmode +def install_importhook(config, mode): if mode == "rewrite": try: import ast # noqa @@ -51,37 +47,37 @@ def pytest_load_initial_conftests(early_config, parser, args): sys.version_info[:3] == (2, 6, 0)): mode = "reinterp" - early_config._assertstate = AssertionState(early_config, mode) - warn_about_missing_assertion(mode, early_config.pluginmanager) + config._assertstate = AssertionState(config, mode) - if mode != "plain": - _load_modules(mode) - m = MonkeyPatch() - early_config._cleanup.append(m.undo) - m.setattr(py.builtin.builtins, 'AssertionError', - reinterpret.AssertionError) # noqa + _load_modules(mode) + m = monkeypatch() + config._cleanup.append(m.undo) + m.setattr(py.builtin.builtins, 'AssertionError', + reinterpret.AssertionError) # noqa hook = None if mode == "rewrite": - hook = rewrite.AssertionRewritingHook(early_config) # noqa + hook = rewrite.AssertionRewritingHook(config) # noqa sys.meta_path.insert(0, hook) - early_config._assertstate.hook = hook - early_config._assertstate.trace("configured with mode set to %r" % (mode,)) + config._assertstate.hook = hook + config._assertstate.trace("configured with mode set to %r" % (mode,)) def undo(): - hook = early_config._assertstate.hook + hook = config._assertstate.hook if hook is not None and hook in sys.meta_path: sys.meta_path.remove(hook) - early_config.add_cleanup(undo) + config.add_cleanup(undo) + return hook def pytest_collection(session): # this hook is only called when test modules are collected # so for example not in the master process of pytest-xdist # (which does not collect test modules) - hook = session.config._assertstate.hook - if hook is not None: - hook.set_session(session) + assertstate = getattr(session.config, '_assertstate', None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(session) def _running_on_ci(): @@ -138,9 +134,10 @@ def pytest_runtest_teardown(item): def pytest_sessionfinish(session): - hook = session.config._assertstate.hook - if hook is not None: - hook.session = None + assertstate = getattr(session.config, '_assertstate', None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(None) def _load_modules(mode): @@ -151,31 +148,5 @@ def _load_modules(mode): from _pytest.assertion import rewrite # noqa -def warn_about_missing_assertion(mode, pluginmanager): - try: - assert False - except AssertionError: - pass - else: - if mode == "rewrite": - specifically = ("assertions which are not in test modules " - "will be ignored") - else: - specifically = "failing tests may report as passing" - - # temporarily disable capture so we can print our warning - capman = pluginmanager.getplugin('capturemanager') - try: - out, err = capman.suspendcapture() - sys.stderr.write("WARNING: " + specifically + - " because assert statements are not executed " - "by the underlying Python interpreter " - "(are you using python -O?)\n") - finally: - capman.resumecapture() - sys.stdout.write(out) - sys.stderr.write(err) - - # Expose this plugin's implementation for the pytest_assertrepr_compare hook pytest_assertrepr_compare = util.assertrepr_compare diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 06944b016..50d8062ae 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -51,6 +51,7 @@ class AssertionRewritingHook(object): self.session = None self.modules = {} self._register_with_pkg_resources() + self._must_rewrite = set() def set_session(self, session): self.session = session @@ -87,7 +88,7 @@ class AssertionRewritingHook(object): fn = os.path.join(pth, name.rpartition(".")[2] + ".py") fn_pypath = py.path.local(fn) - if not self._should_rewrite(fn_pypath, state): + if not self._should_rewrite(name, fn_pypath, state): return None # The requested module looks like a test file, so rewrite it. This is @@ -137,7 +138,7 @@ class AssertionRewritingHook(object): self.modules[name] = co, pyc return self - def _should_rewrite(self, fn_pypath, state): + def _should_rewrite(self, name, fn_pypath, state): # always rewrite conftest files fn = str(fn_pypath) if fn_pypath.basename == 'conftest.py': @@ -161,8 +162,29 @@ class AssertionRewritingHook(object): finally: self.session = session del session + else: + for marked in self._must_rewrite: + if marked.startswith(name): + return True return False + def mark_rewrite(self, *names): + """Mark import names as needing to be re-written. + + The named module or package as well as any nested modules will + be re-written on import. + """ + already_imported = set(names).intersection(set(sys.modules)) + if already_imported: + self._warn_already_imported(already_imported) + self._must_rewrite.update(names) + + def _warn_already_imported(self, names): + self.config.warn( + 'P1', + 'Modules are already imported so can not be re-written: %s' % + ','.join(names)) + def load_module(self, name): # If there is an existing module object named 'fullname' in # sys.modules, the loader must use that existing module. (Otherwise, diff --git a/_pytest/config.py b/_pytest/config.py index 8cb1e6e01..5ac120ab3 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -5,6 +5,7 @@ import traceback import types import warnings +import pkg_resources import py # DON't import pytest here because it causes import cycle troubles import sys, os @@ -918,14 +919,63 @@ class Config(object): self._parser.addini('addopts', 'extra command line options', 'args') self._parser.addini('minversion', 'minimally required pytest version') + def _consider_importhook(self, args, entrypoint_name): + """Install the PEP 302 import hook if using assertion re-writing. + + Needs to parse the --assert= option from the commandline + and find all the installed plugins to mark them for re-writing + by the importhook. + """ + import _pytest.assertion + ns, unknown_args = self._parser.parse_known_and_unknown_args(args) + mode = ns.assertmode + if ns.noassert or ns.nomagic: + mode = "plain" + self._warn_about_missing_assertion(mode) + if mode != 'plain': + hook = _pytest.assertion.install_importhook(self, mode) + if hook: + for entrypoint in pkg_resources.iter_entry_points('pytest11'): + for entry in entrypoint.dist._get_metadata('RECORD'): + fn = entry.split(',')[0] + is_simple_module = os.sep not in fn and fn.endswith('.py') + is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py') + if is_simple_module: + module_name, ext = os.path.splitext(fn) + hook.mark_rewrite(module_name) + elif is_package: + package_name = os.path.dirname(fn) + hook.mark_rewrite(package_name) + + def _warn_about_missing_assertion(self, mode): + try: + assert False + except AssertionError: + pass + else: + if mode == "rewrite": + specifically = ("assertions not in test modules or plugins" + "will be ignored") + else: + specifically = "failing tests may report as passing" + sys.stderr.write("WARNING: " + specifically + + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n") + def _preparse(self, args, addopts=True): self._initini(args) if addopts: args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args args[:] = self.getini("addopts") + args self._checkversion() + entrypoint_name = 'pytest11' + self._consider_importhook(args, entrypoint_name) self.pluginmanager.consider_preparse(args) - self.pluginmanager.load_setuptools_entrypoints("pytest11") + try: + self.pluginmanager.load_setuptools_entrypoints(entrypoint_name) + except ImportError as e: + self.warn("I2", "could not load setuptools entry import: %s" % (e,)) self.pluginmanager.consider_env() self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy()) if self.known_args_namespace.confcutdir is None and self.inifile: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 56cd73bd3..0346cb9a9 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -26,6 +26,116 @@ def mock_config(): def interpret(expr): return reinterpret.reinterpret(expr, _pytest._code.Frame(sys._getframe(1))) + +class TestImportHookInstallation: + + @pytest.mark.parametrize('initial_conftest', [True, False]) + @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) + def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode): + """Test that conftest files are using assertion rewrite on import. + (#1619) + """ + testdir.tmpdir.join('foo/tests').ensure(dir=1) + conftest_path = 'conftest.py' if initial_conftest else 'foo/conftest.py' + contents = { + conftest_path: """ + import pytest + @pytest.fixture + def check_first(): + def check(values, value): + assert values.pop(0) == value + return check + """, + 'foo/tests/test_foo.py': """ + def test(check_first): + check_first([10, 30], 30) + """ + } + testdir.makepyfile(**contents) + result = testdir.runpytest_subprocess('--assert=%s' % mode) + if mode == 'plain': + expected = 'E AssertionError' + elif mode == 'rewrite': + expected = '*assert 10 == 30*' + elif mode == 'reinterp': + expected = '*AssertionError:*was re-run*' + else: + assert 0 + result.stdout.fnmatch_lines([expected]) + + @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) + def test_installed_plugin_rewrite(self, testdir, mode): + # Make sure the hook is installed early enough so that plugins + # installed via setuptools are re-written. + ham = testdir.tmpdir.join('hampkg').ensure(dir=1) + ham.join('__init__.py').write(""" +import pytest + +@pytest.fixture +def check_first2(): + def check(values, value): + assert values.pop(0) == value + return check + """) + testdir.makepyfile( + spamplugin=""" + import pytest + from hampkg import check_first2 + + @pytest.fixture + def check_first(): + def check(values, value): + assert values.pop(0) == value + return check + """, + mainwrapper=""" + import pytest, pkg_resources + + class DummyDistInfo: + project_name = 'spam' + version = '1.0' + + def _get_metadata(self, name): + return ['spamplugin.py,sha256=abc,123', + 'hampkg/__init__.py,sha256=abc,123'] + + class DummyEntryPoint: + name = 'spam' + module_name = 'spam.py' + attrs = () + extras = None + dist = DummyDistInfo() + + def load(self, require=True, *args, **kwargs): + import spamplugin + return spamplugin + + def iter_entry_points(name): + yield DummyEntryPoint() + + pkg_resources.iter_entry_points = iter_entry_points + pytest.main() + """, + test_foo=""" + def test(check_first): + check_first([10, 30], 30) + + def test2(check_first2): + check_first([10, 30], 30) + """, + ) + result = testdir.run(sys.executable, 'mainwrapper.py', '-s', '--assert=%s' % mode) + if mode == 'plain': + expected = 'E AssertionError' + elif mode == 'rewrite': + expected = '*assert 10 == 30*' + elif mode == 'reinterp': + expected = '*AssertionError:*was re-run*' + else: + assert 0 + result.stdout.fnmatch_lines([expected]) + + class TestBinReprIntegration: def test_pytest_assertrepr_compare_called(self, testdir): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 5f8127af9..496034c23 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -12,7 +12,7 @@ if sys.platform.startswith("java"): import _pytest._code from _pytest.assertion import util -from _pytest.assertion.rewrite import rewrite_asserts, PYTEST_TAG +from _pytest.assertion.rewrite import rewrite_asserts, PYTEST_TAG, AssertionRewritingHook from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -524,6 +524,16 @@ def test_rewritten(): testdir.makepyfile("import a_package_without_init_py.module") assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED + def test_rewrite_warning(self, pytestconfig, monkeypatch): + hook = AssertionRewritingHook(pytestconfig) + warnings = [] + def mywarn(code, msg): + warnings.append((code, msg)) + monkeypatch.setattr(hook.config, 'warn', mywarn) + hook.mark_rewrite('_pytest') + assert '_pytest' in warnings[0][1] + + class TestAssertionRewriteHookDetails(object): def test_loader_is_package_false_for_module(self, testdir): testdir.makepyfile(test_fun=""" @@ -704,40 +714,6 @@ class TestAssertionRewriteHookDetails(object): result = testdir.runpytest() result.stdout.fnmatch_lines('*1 passed*') - @pytest.mark.parametrize('initial_conftest', [True, False]) - @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) - def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode): - """Test that conftest files are using assertion rewrite on import. - (#1619) - """ - testdir.tmpdir.join('foo/tests').ensure(dir=1) - conftest_path = 'conftest.py' if initial_conftest else 'foo/conftest.py' - contents = { - conftest_path: """ - import pytest - @pytest.fixture - def check_first(): - def check(values, value): - assert values.pop(0) == value - return check - """, - 'foo/tests/test_foo.py': """ - def test(check_first): - check_first([10, 30], 30) - """ - } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess('--assert=%s' % mode) - if mode == 'plain': - expected = 'E AssertionError' - elif mode == 'rewrite': - expected = '*assert 10 == 30*' - elif mode == 'reinterp': - expected = '*AssertionError:*was re-run*' - else: - assert 0 - result.stdout.fnmatch_lines([expected]) - def test_issue731(testdir): testdir.makepyfile(""" diff --git a/testing/test_config.py b/testing/test_config.py index bb686c3b0..57c95cd50 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -373,10 +373,14 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch): pkg_resources = pytest.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" + class Dist: + project_name = 'spam' + version = '1.0' + def _get_metadata(self, name): + return ['foo.txt,sha256=abc,123'] class EntryPoint: name = "mytestplugin" - class dist: - pass + dist = Dist() def load(self): class PseudoPlugin: x = 42 @@ -412,8 +416,14 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch): pkg_resources = pytest.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" + class Dist: + project_name = 'spam' + version = '1.0' + def _get_metadata(self, name): + return ['foo.txt,sha256=abc,123'] class EntryPoint: name = "mytestplugin" + dist = Dist() def load(self): assert 0, "should not arrive here" return iter([EntryPoint()]) @@ -505,7 +515,6 @@ def test_load_initial_conftest_last_ordering(testdir): expected = [ "_pytest.config", 'test_config', - '_pytest.assertion', '_pytest.capture', ] assert [x.function.__module__ for x in l] == expected From 944da5b98a381199bf5feda1718a053323dd1084 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 23 Jun 2016 23:51:02 +0200 Subject: [PATCH 16/34] Avoid rewrite warning for inline runs When running pytest inline/inprocess we plugins have already been imported and re-writen, so avoid the warning. --- _pytest/assertion/rewrite.py | 6 +++--- _pytest/pytester.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 50d8062ae..aa33f1352 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -163,9 +163,9 @@ class AssertionRewritingHook(object): self.session = session del session else: - for marked in self._must_rewrite: - if marked.startswith(name): - return True + toplevel_name = name.split('.', 1)[0] + if toplevel_name in self._must_rewrite: + return True return False def mark_rewrite(self, *names): diff --git a/_pytest/pytester.py b/_pytest/pytester.py index fc5b3ebd9..7831655d1 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -16,6 +16,7 @@ from _pytest._code import Source import py import pytest from _pytest.main import Session, EXIT_OK +from _pytest.assertion.rewrite import AssertionRewritingHook def pytest_addoption(parser): @@ -685,8 +686,17 @@ class Testdir: ``pytest.main()`` instance should use. :return: A :py:class:`HookRecorder` instance. - """ + # When running py.test inline any plugins active in the main + # test process are already imported. So this disables the + # warning which will trigger to say they can no longer be + # re-written, which is fine as they are already re-written. + orig_warn = AssertionRewritingHook._warn_already_imported + def revert(): + AssertionRewritingHook._warn_already_imported = orig_warn + self.request.addfinalizer(revert) + AssertionRewritingHook._warn_already_imported = lambda *a: None + rec = [] class Collect: def pytest_configure(x, config): From 743f59afb23ab29c890cca9b3827a4b67b7ef314 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sat, 25 Jun 2016 18:26:45 +0200 Subject: [PATCH 17/34] Introduce pytest.register_assert_rewrite() Plugins can now explicitly mark modules to be re-written. By default only the modules containing the plugin entrypoint are re-written. --- _pytest/assertion/__init__.py | 29 ++++++++++ _pytest/assertion/rewrite.py | 6 +- _pytest/config.py | 12 ++-- testing/test_assertion.py | 101 +++++++++++++++++++++++++++++----- 4 files changed, 127 insertions(+), 21 deletions(-) diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index 746c810ee..271a2e7d5 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -7,6 +7,7 @@ import sys from _pytest.monkeypatch import monkeypatch from _pytest.assertion import util +from _pytest.assertion import rewrite def pytest_addoption(parser): @@ -26,6 +27,34 @@ def pytest_addoption(parser): provide assert expression information. """) +def pytest_namespace(): + return {'register_assert_rewrite': register_assert_rewrite} + + +def register_assert_rewrite(*names): + """Register a module name to be rewritten on import. + + This function will make sure that the module will get it's assert + statements rewritten when it is imported. Thus you should make + sure to call this before the module is actually imported, usually + in your __init__.py if you are a plugin using a package. + """ + for hook in sys.meta_path: + if isinstance(hook, rewrite.AssertionRewritingHook): + importhook = hook + break + else: + importhook = DummyRewriteHook() + importhook.mark_rewrite(*names) + + +class DummyRewriteHook(object): + """A no-op import hook for when rewriting is disabled.""" + + def mark_rewrite(self, *names): + pass + + class AssertionState: """State for the assertion plugin.""" diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index aa33f1352..50d8062ae 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -163,9 +163,9 @@ class AssertionRewritingHook(object): self.session = session del session else: - toplevel_name = name.split('.', 1)[0] - if toplevel_name in self._must_rewrite: - return True + for marked in self._must_rewrite: + if marked.startswith(name): + return True return False def mark_rewrite(self, *names): diff --git a/_pytest/config.py b/_pytest/config.py index 5ac120ab3..536c2fb34 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -11,6 +11,7 @@ import py import sys, os import _pytest._code import _pytest.hookspec # the extension point definitions +import _pytest.assertion from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker hookimpl = HookimplMarker("pytest") @@ -154,6 +155,9 @@ class PytestPluginManager(PluginManager): self.trace.root.setwriter(err.write) self.enable_tracing() + # Config._consider_importhook will set a real object if required. + self.rewrite_hook = _pytest.assertion.DummyRewriteHook() + def addhooks(self, module_or_class): """ .. deprecated:: 2.8 @@ -362,7 +366,9 @@ class PytestPluginManager(PluginManager): self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) def consider_module(self, mod): - self._import_plugin_specs(getattr(mod, "pytest_plugins", None)) + plugins = getattr(mod, 'pytest_plugins', []) + self.rewrite_hook.mark_rewrite(*plugins) + self._import_plugin_specs(plugins) def _import_plugin_specs(self, spec): if spec: @@ -926,15 +932,13 @@ class Config(object): and find all the installed plugins to mark them for re-writing by the importhook. """ - import _pytest.assertion ns, unknown_args = self._parser.parse_known_and_unknown_args(args) mode = ns.assertmode - if ns.noassert or ns.nomagic: - mode = "plain" self._warn_about_missing_assertion(mode) if mode != 'plain': hook = _pytest.assertion.install_importhook(self, mode) if hook: + self.pluginmanager.rewrite_hook = hook for entrypoint in pkg_resources.iter_entry_points('pytest11'): for entry in entrypoint.dist._get_metadata('RECORD'): fn = entry.split(',')[0] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 0346cb9a9..215d3e419 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -63,22 +63,53 @@ class TestImportHookInstallation: assert 0 result.stdout.fnmatch_lines([expected]) + @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) + def test_pytest_plugins_rewrite(self, testdir, mode): + contents = { + 'conftest.py': """ + pytest_plugins = ['ham'] + """, + 'ham.py': """ + import pytest + @pytest.fixture + def check_first(): + def check(values, value): + assert values.pop(0) == value + return check + """, + 'test_foo.py': """ + def test_foo(check_first): + check_first([10, 30], 30) + """, + } + testdir.makepyfile(**contents) + result = testdir.runpytest_subprocess('--assert=%s' % mode) + if mode == 'plain': + expected = 'E AssertionError' + elif mode == 'rewrite': + expected = '*assert 10 == 30*' + elif mode == 'reinterp': + expected = '*AssertionError:*was re-run*' + else: + assert 0 + result.stdout.fnmatch_lines([expected]) + @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) def test_installed_plugin_rewrite(self, testdir, mode): # Make sure the hook is installed early enough so that plugins # installed via setuptools are re-written. - ham = testdir.tmpdir.join('hampkg').ensure(dir=1) - ham.join('__init__.py').write(""" -import pytest + testdir.tmpdir.join('hampkg').ensure(dir=1) + contents = { + 'hampkg/__init__.py': """ + import pytest -@pytest.fixture -def check_first2(): - def check(values, value): - assert values.pop(0) == value - return check - """) - testdir.makepyfile( - spamplugin=""" + @pytest.fixture + def check_first2(): + def check(values, value): + assert values.pop(0) == value + return check + """, + 'spamplugin.py': """ import pytest from hampkg import check_first2 @@ -88,7 +119,7 @@ def check_first2(): assert values.pop(0) == value return check """, - mainwrapper=""" + 'mainwrapper.py': """ import pytest, pkg_resources class DummyDistInfo: @@ -116,14 +147,15 @@ def check_first2(): pkg_resources.iter_entry_points = iter_entry_points pytest.main() """, - test_foo=""" + 'test_foo.py': """ def test(check_first): check_first([10, 30], 30) def test2(check_first2): check_first([10, 30], 30) """, - ) + } + testdir.makepyfile(**contents) result = testdir.run(sys.executable, 'mainwrapper.py', '-s', '--assert=%s' % mode) if mode == 'plain': expected = 'E AssertionError' @@ -135,6 +167,47 @@ def check_first2(): assert 0 result.stdout.fnmatch_lines([expected]) + def test_rewrite_ast(self, testdir): + testdir.tmpdir.join('pkg').ensure(dir=1) + contents = { + 'pkg/__init__.py': """ + import pytest + pytest.register_assert_rewrite('pkg.helper') + """, + 'pkg/helper.py': """ + def tool(): + a, b = 2, 3 + assert a == b + """, + 'pkg/plugin.py': """ + import pytest, pkg.helper + @pytest.fixture + def tool(): + return pkg.helper.tool + """, + 'pkg/other.py': """ + l = [3, 2] + def tool(): + assert l.pop() == 3 + """, + 'conftest.py': """ + pytest_plugins = ['pkg.plugin'] + """, + 'test_pkg.py': """ + import pkg.other + def test_tool(tool): + tool() + def test_other(): + pkg.other.tool() + """, + } + testdir.makepyfile(**contents) + result = testdir.runpytest_subprocess('--assert=rewrite') + result.stdout.fnmatch_lines(['>*assert a == b*', + 'E*assert 2 == 3*', + '>*assert l.pop() == 3*', + 'E*AssertionError*re-run*']) + class TestBinReprIntegration: From 3cfebdd7c5e3ff31a12b56638d49bf29303be74e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Jul 2016 18:33:24 +0200 Subject: [PATCH 18/34] funcarg_prefix_warning: remove newline Followup to https://github.com/pytest-dev/pytest/pull/1718. --- _pytest/fixtures.py | 2 +- testing/acceptance_test.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 38e96e891..ab437459b 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -868,7 +868,7 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N defaultfuncargprefixmarker = fixture() funcarg_prefix_warning = ( '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' - 'and scheduled to be removed in pytest 4.0.\n' + 'and scheduled to be removed in pytest 4.0. ' 'Please remove the prefix and use the @pytest.fixture decorator instead.') diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 72cd18f11..d709a6f4d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -789,7 +789,9 @@ def test_funcarg_prefix_deprecation(testdir): """) result = testdir.runpytest('-ra') result.stdout.fnmatch_lines([ - '*declaring fixtures using "pytest_funcarg__" prefix is deprecated and scheduled to be removed in pytest 4.0*', - '*remove the prefix and use the @pytest.fixture decorator instead*', + ('WC1 None pytest_funcarg__value: ' + 'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' + 'and scheduled to be removed in pytest 4.0. ' + 'Please remove the prefix and use the @pytest.fixture decorator instead.'), '*1 passed*', ]) From ab0b6faa5fc7938b55c66724d24c75b10b9307e1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Jul 2016 19:35:05 -0300 Subject: [PATCH 19/34] Deprecate support for passing command-line as string to pytest.main() Fixes #1723 --- CHANGELOG.rst | 4 ++++ _pytest/config.py | 7 +++++++ testing/acceptance_test.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cfd77cb0c..906aa640e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -203,6 +203,9 @@ time or change existing behaviors in order to make them less surprising/more use removed in pytest-4.0 (`#1684`_). Thanks `@nicoddemus`_ for the PR. +* Passing a command-line string to ``pytest.main()`` is considered deprecated and scheduled + for removal in pytest-4.0. It is recommended to pass a list of arguments instead (`#1723`_). + * Rename ``getfuncargvalue`` to ``getfixturevalue``. ``getfuncargvalue`` is still present but is now considered deprecated. Thanks to `@RedBeardCode`_ and `@tomviner`_ for the PR (`#1626`_). @@ -282,6 +285,7 @@ time or change existing behaviors in order to make them less surprising/more use .. _#1633: https://github.com/pytest-dev/pytest/pull/1633 .. _#1664: https://github.com/pytest-dev/pytest/pull/1664 .. _#1684: https://github.com/pytest-dev/pytest/pull/1684 +.. _#1723: https://github.com/pytest-dev/pytest/pull/1723 .. _@DRMacIver: https://github.com/DRMacIver .. _@RedBeardCode: https://github.com/RedBeardCode diff --git a/_pytest/config.py b/_pytest/config.py index 8cb1e6e01..2309ac317 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -98,6 +98,7 @@ def get_plugin_manager(): return get_config().pluginmanager def _prepareconfig(args=None, plugins=None): + warning = None if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): @@ -106,6 +107,10 @@ def _prepareconfig(args=None, plugins=None): if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args, posix=sys.platform != "win32") + # we want to remove this way of passing arguments to pytest.main() + # in pytest-4.0 + warning = ('passing a string to pytest.main() is deprecated, ' + 'pass a list of arguments instead.') config = get_config() pluginmanager = config.pluginmanager try: @@ -115,6 +120,8 @@ def _prepareconfig(args=None, plugins=None): pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) + if warning: + config.warn('C1', warning) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args) except BaseException: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d709a6f4d..58887712e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -795,3 +795,17 @@ def test_funcarg_prefix_deprecation(testdir): 'Please remove the prefix and use the @pytest.fixture decorator instead.'), '*1 passed*', ]) + + +def test_str_args_deprecated(tmpdir, testdir): + """Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0.""" + warnings = [] + + class Collect: + def pytest_logwarning(self, message): + warnings.append(message) + + ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) + testdir.delete_loaded_modules() + assert warnings == ['passing a string to pytest.main() is deprecated, pass a list of arguments instead.'] + assert ret == EXIT_NOTESTSCOLLECTED From 02dd7d612a1548d2234a838712449e447073f166 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Jul 2016 08:16:27 -0300 Subject: [PATCH 20/34] Remove duplicated changelog entry and formatting fix --- CHANGELOG.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cfd77cb0c..aa3a831dd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -161,8 +161,7 @@ time or change existing behaviors in order to make them less surprising/more use Thanks `@palaviv`_ for the complete PR (`#1474`_). * Now pytest warnings summary is shown up by default. Added a new flag - ``--disable-pytest-warnings`` to explicitly disable the warnings summary. - This change resolves the (`#1668`_). + ``--disable-pytest-warnings`` to explicitly disable the warnings summary (`#1668`_). * Make ImportError during collection more explicit by reminding the user to check the name of the test module/package(s) (`#1426`_). @@ -220,8 +219,6 @@ time or change existing behaviors in order to make them less surprising/more use * Fix internal error issue when the ``method`` argument is missing for ``teardown_method()`` (`#1605`_). -* Renamed the pytest ``pdb`` module (plugin) into ``debugging``. - * Fix exception visualization in case the current working directory (CWD) gets deleted during testing (`#1235`_). Thanks `@bukzor`_ for reporting. PR by `@marscher`_. From 51ee7f8734e20d87d08a6e49604b630d5cdd656d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 14 Jul 2016 12:42:29 +0100 Subject: [PATCH 21/34] Fixup things after rebase Some changes to make things work on top of current features branch. --- _pytest/assertion/__init__.py | 4 ++-- _pytest/config.py | 5 +---- testing/test_config.py | 9 +++++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index 271a2e7d5..c231a4769 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -5,7 +5,6 @@ import py import os import sys -from _pytest.monkeypatch import monkeypatch from _pytest.assertion import util from _pytest.assertion import rewrite @@ -79,7 +78,8 @@ def install_importhook(config, mode): config._assertstate = AssertionState(config, mode) _load_modules(mode) - m = monkeypatch() + from _pytest.monkeypatch import MonkeyPatch + m = MonkeyPatch() config._cleanup.append(m.undo) m.setattr(py.builtin.builtins, 'AssertionError', reinterpret.AssertionError) # noqa diff --git a/_pytest/config.py b/_pytest/config.py index 536c2fb34..f931027d1 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -976,10 +976,7 @@ class Config(object): entrypoint_name = 'pytest11' self._consider_importhook(args, entrypoint_name) self.pluginmanager.consider_preparse(args) - try: - self.pluginmanager.load_setuptools_entrypoints(entrypoint_name) - except ImportError as e: - self.warn("I2", "could not load setuptools entry import: %s" % (e,)) + self.pluginmanager.load_setuptools_entrypoints(entrypoint_name) self.pluginmanager.consider_env() self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy()) if self.known_args_namespace.confcutdir is None and self.inifile: diff --git a/testing/test_config.py b/testing/test_config.py index 57c95cd50..6a576da12 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -400,9 +400,14 @@ def test_setuptools_importerror_issue1479(testdir, monkeypatch): pkg_resources = pytest.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" + class Dist: + project_name = 'spam' + version = '1.0' + def _get_metadata(self, name): + return ['foo.txt,sha256=abc,123'] class EntryPoint: name = "mytestplugin" - dist = None + dist = Dist() def load(self): raise ImportError("Don't hide me!") return iter([EntryPoint()]) @@ -697,4 +702,4 @@ class TestOverrideIniArgs: "ini2:url=/tmp/user2?a=b&d=e", "ini3:True", "ini4:False" - ]) \ No newline at end of file + ]) From 22bb43413ff94dc6df43b5e75a7c142d1ca351f9 Mon Sep 17 00:00:00 2001 From: RedBeardCode Date: Thu, 14 Jul 2016 09:14:19 +0200 Subject: [PATCH 22/34] Added confcutdir in testing/test_conftest.py::test_conftest_import_order and testing/python/fixture.py::TestAutouseManagement::():: test_class_function_parametrization_finalization to avoid problems with abandoned conftest.py files in /tmp dir. Fixes #1536 --- testing/python/fixture.py | 3 ++- testing/test_conftest.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index e2bca5286..e7232a25f 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1586,7 +1586,8 @@ class TestAutouseManagement: def test_2(self): pass """) - reprec = testdir.inline_run("-v","-s") + confcut = "--confcutdir={0}".format(testdir.tmpdir) + reprec = testdir.inline_run("-v","-s", confcut) reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config l = config.pluginmanager._getconftestmodules(p)[0].l diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 6f5e77f6d..90db7835c 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -203,6 +203,7 @@ def test_conftest_import_order(testdir, monkeypatch): def impct(p): return p conftest = PytestPluginManager() + conftest._confcutdir = testdir.tmpdir monkeypatch.setattr(conftest, '_importconftest', impct) assert conftest._getconftestmodules(sub) == [ct1, ct2] From 3328cd26203f2086b445c009eeee2fa4e0d847ef Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Jul 2016 18:37:59 -0300 Subject: [PATCH 23/34] Make assert in test_str_args_deprecated more resilient This attempts to fix CI which broke because of this test. Other warnings introduced in the future could break this test. --- testing/acceptance_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 58887712e..2e495e246 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -807,5 +807,7 @@ def test_str_args_deprecated(tmpdir, testdir): ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) testdir.delete_loaded_modules() - assert warnings == ['passing a string to pytest.main() is deprecated, pass a list of arguments instead.'] + msg = ('passing a string to pytest.main() is deprecated, ' + 'pass a list of arguments instead.') + assert msg in warnings assert ret == EXIT_NOTESTSCOLLECTED From d1852a48b7d38cb1cd36135ea92f83742d8782d6 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 15 Jul 2016 00:18:50 +0100 Subject: [PATCH 24/34] Remove assertion reinterpretation The assertion reinterpretation is an old backwards compatibility mode which was no longer being maintained on feature-parity with the assertion rewriting mode. It was also responsible for some dubious patching of builtins and test with side-effects would suddenly start passing. Since re-writing has been the default for a long time and plugins are now also re-written it is time to retire reinterpretation. --- CHANGELOG.rst | 4 + _pytest/_code/__init__.py | 3 - _pytest/_code/code.py | 35 --- _pytest/assertion/__init__.py | 62 ++--- _pytest/assertion/reinterpret.py | 407 ------------------------------- _pytest/config.py | 28 ++- testing/code/test_code.py | 18 -- testing/code/test_excinfo.py | 48 +--- testing/test_assertinterpret.py | 274 --------------------- testing/test_assertion.py | 28 +-- 10 files changed, 44 insertions(+), 863 deletions(-) delete mode 100644 _pytest/assertion/reinterpret.py delete mode 100644 testing/test_assertinterpret.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0bc0e0102..66cf70c30 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,10 @@ A number of incompatible changes were made in this release, with the intent of removing features deprecated for a long time or change existing behaviors in order to make them less surprising/more useful. +* Reinterpretation mode has now been removed. Only plain and rewrite + mode are available, consequently the ``--assert=reinterp`` option is + no longer available. Thanks `@flub`_ for the PR. + * The following deprecated commandline options were removed: * ``--genscript``: no longer supported; diff --git a/_pytest/_code/__init__.py b/_pytest/_code/__init__.py index c046b9716..3463c11ea 100644 --- a/_pytest/_code/__init__.py +++ b/_pytest/_code/__init__.py @@ -4,9 +4,6 @@ from .code import ExceptionInfo # noqa from .code import Frame # noqa from .code import Traceback # noqa from .code import getrawcode # noqa -from .code import patch_builtins # noqa -from .code import unpatch_builtins # noqa from .source import Source # noqa from .source import compile_ as compile # noqa from .source import getfslineno # noqa - diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 0f1ffb918..0af5a3465 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -179,18 +179,6 @@ class TracebackEntry(object): return self.frame.f_locals locals = property(getlocals, None, None, "locals of underlaying frame") - def reinterpret(self): - """Reinterpret the failing statement and returns a detailed information - about what operations are performed.""" - from _pytest.assertion.reinterpret import reinterpret - if self.exprinfo is None: - source = py.builtin._totext(self.statement).strip() - x = reinterpret(source, self.frame, should_fail=True) - if not py.builtin._istext(x): - raise TypeError("interpret returned non-string %r" % (x,)) - self.exprinfo = x - return self.exprinfo - def getfirstlinesource(self): # on Jython this firstlineno can be -1 apparently return max(self.frame.code.firstlineno, 0) @@ -830,29 +818,6 @@ class ReprFuncArgs(TerminalRepr): tw.line("") - -oldbuiltins = {} - -def patch_builtins(assertion=True, compile=True): - """ put compile and AssertionError builtins to Python's builtins. """ - if assertion: - from _pytest.assertion import reinterpret - l = oldbuiltins.setdefault('AssertionError', []) - l.append(py.builtin.builtins.AssertionError) - py.builtin.builtins.AssertionError = reinterpret.AssertionError - if compile: - import _pytest._code - l = oldbuiltins.setdefault('compile', []) - l.append(py.builtin.builtins.compile) - py.builtin.builtins.compile = _pytest._code.compile - -def unpatch_builtins(assertion=True, compile=True): - """ remove compile and AssertionError builtins from Python builtins. """ - if assertion: - py.builtin.builtins.AssertionError = oldbuiltins['AssertionError'].pop() - if compile: - py.builtin.builtins.compile = oldbuiltins['compile'].pop() - def getrawcode(obj, trycall=True): """ return code object for given function. """ try: diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index c231a4769..bca626931 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -14,16 +14,14 @@ def pytest_addoption(parser): group.addoption('--assert', action="store", dest="assertmode", - choices=("rewrite", "reinterp", "plain",), + choices=("rewrite", "plain",), default="rewrite", metavar="MODE", - help="""control assertion debugging tools. 'plain' - performs no assertion debugging. 'reinterp' - reinterprets assert statements after they failed - to provide assertion expression information. - 'rewrite' (the default) rewrites assert - statements in test modules on import to - provide assert expression information. """) + help="""Control assertion debugging tools. 'plain' + performs no assertion debugging. 'rewrite' + (the default) rewrites assert statements in + test modules on import to provide assert + expression information.""") def pytest_namespace(): @@ -60,37 +58,21 @@ class AssertionState: def __init__(self, config, mode): self.mode = mode self.trace = config.trace.root.get("assertion") + self.hook = None -def install_importhook(config, mode): - if mode == "rewrite": - try: - import ast # noqa - except ImportError: - mode = "reinterp" - else: - # Both Jython and CPython 2.6.0 have AST bugs that make the - # assertion rewriting hook malfunction. - if (sys.platform.startswith('java') or - sys.version_info[:3] == (2, 6, 0)): - mode = "reinterp" +def install_importhook(config): + """Try to install the rewrite hook, raise SystemError if it fails.""" + # Both Jython and CPython 2.6.0 have AST bugs that make the + # assertion rewriting hook malfunction. + if (sys.platform.startswith('java') or + sys.version_info[:3] == (2, 6, 0)): + raise SystemError('rewrite not supported') - config._assertstate = AssertionState(config, mode) - - _load_modules(mode) - from _pytest.monkeypatch import MonkeyPatch - m = MonkeyPatch() - config._cleanup.append(m.undo) - m.setattr(py.builtin.builtins, 'AssertionError', - reinterpret.AssertionError) # noqa - - hook = None - if mode == "rewrite": - hook = rewrite.AssertionRewritingHook(config) # noqa - sys.meta_path.insert(0, hook) - - config._assertstate.hook = hook - config._assertstate.trace("configured with mode set to %r" % (mode,)) + config._assertstate = AssertionState(config, 'rewrite') + config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config) + sys.meta_path.insert(0, hook) + config._assertstate.trace('installed rewrite import hook') def undo(): hook = config._assertstate.hook if hook is not None and hook in sys.meta_path: @@ -169,13 +151,5 @@ def pytest_sessionfinish(session): assertstate.hook.set_session(None) -def _load_modules(mode): - """Lazily import assertion related code.""" - global rewrite, reinterpret - from _pytest.assertion import reinterpret # noqa - if mode == "rewrite": - from _pytest.assertion import rewrite # noqa - - # Expose this plugin's implementation for the pytest_assertrepr_compare hook pytest_assertrepr_compare = util.assertrepr_compare diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py deleted file mode 100644 index f4262c3ac..000000000 --- a/_pytest/assertion/reinterpret.py +++ /dev/null @@ -1,407 +0,0 @@ -""" -Find intermediate evalutation results in assert statements through builtin AST. -""" -import ast -import sys - -import _pytest._code -import py -from _pytest.assertion import util -u = py.builtin._totext - - -class AssertionError(util.BuiltinAssertionError): - def __init__(self, *args): - util.BuiltinAssertionError.__init__(self, *args) - if args: - # on Python2.6 we get len(args)==2 for: assert 0, (x,y) - # on Python2.7 and above we always get len(args) == 1 - # with args[0] being the (x,y) tuple. - if len(args) > 1: - toprint = args - else: - toprint = args[0] - try: - self.msg = u(toprint) - except Exception: - self.msg = u( - "<[broken __repr__] %s at %0xd>" - % (toprint.__class__, id(toprint))) - else: - f = _pytest._code.Frame(sys._getframe(1)) - try: - source = f.code.fullsource - if source is not None: - try: - source = source.getstatement(f.lineno, assertion=True) - except IndexError: - source = None - else: - source = str(source.deindent()).strip() - except py.error.ENOENT: - source = None - # this can also occur during reinterpretation, when the - # co_filename is set to "". - if source: - self.msg = reinterpret(source, f, should_fail=True) - else: - self.msg = "" - if not self.args: - self.args = (self.msg,) - -if sys.version_info > (3, 0): - AssertionError.__module__ = "builtins" - -if sys.platform.startswith("java"): - # See http://bugs.jython.org/issue1497 - _exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict", - "ListComp", "GeneratorExp", "Yield", "Compare", "Call", - "Repr", "Num", "Str", "Attribute", "Subscript", "Name", - "List", "Tuple") - _stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign", - "AugAssign", "Print", "For", "While", "If", "With", "Raise", - "TryExcept", "TryFinally", "Assert", "Import", "ImportFrom", - "Exec", "Global", "Expr", "Pass", "Break", "Continue") - _expr_nodes = set(getattr(ast, name) for name in _exprs) - _stmt_nodes = set(getattr(ast, name) for name in _stmts) - def _is_ast_expr(node): - return node.__class__ in _expr_nodes - def _is_ast_stmt(node): - return node.__class__ in _stmt_nodes -else: - def _is_ast_expr(node): - return isinstance(node, ast.expr) - def _is_ast_stmt(node): - return isinstance(node, ast.stmt) - -try: - _Starred = ast.Starred -except AttributeError: - # Python 2. Define a dummy class so isinstance() will always be False. - class _Starred(object): pass - - -class Failure(Exception): - """Error found while interpreting AST.""" - - def __init__(self, explanation=""): - self.cause = sys.exc_info() - self.explanation = explanation - - -def reinterpret(source, frame, should_fail=False): - mod = ast.parse(source) - visitor = DebugInterpreter(frame) - try: - visitor.visit(mod) - except Failure: - failure = sys.exc_info()[1] - return getfailure(failure) - if should_fail: - return ("(assertion failed, but when it was re-run for " - "printing intermediate values, it did not fail. Suggestions: " - "compute assert expression before the assert or use --assert=plain)") - -def run(offending_line, frame=None): - if frame is None: - frame = _pytest._code.Frame(sys._getframe(1)) - return reinterpret(offending_line, frame) - -def getfailure(e): - explanation = util.format_explanation(e.explanation) - value = e.cause[1] - if str(value): - lines = explanation.split('\n') - lines[0] += " << %s" % (value,) - explanation = '\n'.join(lines) - text = "%s: %s" % (e.cause[0].__name__, explanation) - if text.startswith('AssertionError: assert '): - text = text[16:] - return text - -operator_map = { - ast.BitOr : "|", - ast.BitXor : "^", - ast.BitAnd : "&", - ast.LShift : "<<", - ast.RShift : ">>", - ast.Add : "+", - ast.Sub : "-", - ast.Mult : "*", - ast.Div : "/", - ast.FloorDiv : "//", - ast.Mod : "%", - ast.Eq : "==", - ast.NotEq : "!=", - ast.Lt : "<", - ast.LtE : "<=", - ast.Gt : ">", - ast.GtE : ">=", - ast.Pow : "**", - ast.Is : "is", - ast.IsNot : "is not", - ast.In : "in", - ast.NotIn : "not in" -} - -unary_map = { - ast.Not : "not %s", - ast.Invert : "~%s", - ast.USub : "-%s", - ast.UAdd : "+%s" -} - - -class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information. """ - - def __init__(self, frame): - self.frame = frame - - def generic_visit(self, node): - # Fallback when we don't have a special implementation. - if _is_ast_expr(node): - mod = ast.Expression(node) - co = self._compile(mod) - try: - result = self.frame.eval(co) - except Exception: - raise Failure() - explanation = self.frame.repr(result) - return explanation, result - elif _is_ast_stmt(node): - mod = ast.Module([node]) - co = self._compile(mod, "exec") - try: - self.frame.exec_(co) - except Exception: - raise Failure() - return None, None - else: - raise AssertionError("can't handle %s" %(node,)) - - def _compile(self, source, mode="eval"): - return compile(source, "", mode) - - def visit_Expr(self, expr): - return self.visit(expr.value) - - def visit_Module(self, mod): - for stmt in mod.body: - self.visit(stmt) - - def visit_Name(self, name): - explanation, result = self.generic_visit(name) - # See if the name is local. - source = "%r in locals() is not globals()" % (name.id,) - co = self._compile(source) - try: - local = self.frame.eval(co) - except Exception: - # have to assume it isn't - local = None - if local is None or not self.frame.is_true(local): - return name.id, result - return explanation, result - - def visit_Compare(self, comp): - left = comp.left - left_explanation, left_result = self.visit(left) - for op, next_op in zip(comp.ops, comp.comparators): - next_explanation, next_result = self.visit(next_op) - op_symbol = operator_map[op.__class__] - explanation = "%s %s %s" % (left_explanation, op_symbol, - next_explanation) - source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_left=left_result, - __exprinfo_right=next_result) - except Exception: - raise Failure(explanation) - try: - if not self.frame.is_true(result): - break - except KeyboardInterrupt: - raise - except: - break - left_explanation, left_result = next_explanation, next_result - - if util._reprcompare is not None: - res = util._reprcompare(op_symbol, left_result, next_result) - if res: - explanation = res - return explanation, result - - def visit_BoolOp(self, boolop): - is_or = isinstance(boolop.op, ast.Or) - explanations = [] - for operand in boolop.values: - explanation, result = self.visit(operand) - explanations.append(explanation) - if result == is_or: - break - name = is_or and " or " or " and " - explanation = "(" + name.join(explanations) + ")" - return explanation, result - - def visit_UnaryOp(self, unary): - pattern = unary_map[unary.op.__class__] - operand_explanation, operand_result = self.visit(unary.operand) - explanation = pattern % (operand_explanation,) - co = self._compile(pattern % ("__exprinfo_expr",)) - try: - result = self.frame.eval(co, __exprinfo_expr=operand_result) - except Exception: - raise Failure(explanation) - return explanation, result - - def visit_BinOp(self, binop): - left_explanation, left_result = self.visit(binop.left) - right_explanation, right_result = self.visit(binop.right) - symbol = operator_map[binop.op.__class__] - explanation = "(%s %s %s)" % (left_explanation, symbol, - right_explanation) - source = "__exprinfo_left %s __exprinfo_right" % (symbol,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_left=left_result, - __exprinfo_right=right_result) - except Exception: - raise Failure(explanation) - return explanation, result - - def visit_Call(self, call): - func_explanation, func = self.visit(call.func) - arg_explanations = [] - ns = {"__exprinfo_func" : func} - arguments = [] - for arg in call.args: - arg_explanation, arg_result = self.visit(arg) - if isinstance(arg, _Starred): - arg_name = "__exprinfo_star" - ns[arg_name] = arg_result - arguments.append("*%s" % (arg_name,)) - arg_explanations.append("*%s" % (arg_explanation,)) - else: - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - arguments.append(arg_name) - arg_explanations.append(arg_explanation) - for keyword in call.keywords: - arg_explanation, arg_result = self.visit(keyword.value) - if keyword.arg: - arg_name = "__exprinfo_%s" % (len(ns),) - keyword_source = "%s=%%s" % (keyword.arg) - arguments.append(keyword_source % (arg_name,)) - arg_explanations.append(keyword_source % (arg_explanation,)) - else: - arg_name = "__exprinfo_kwds" - arguments.append("**%s" % (arg_name,)) - arg_explanations.append("**%s" % (arg_explanation,)) - - ns[arg_name] = arg_result - - if getattr(call, 'starargs', None): - arg_explanation, arg_result = self.visit(call.starargs) - arg_name = "__exprinfo_star" - ns[arg_name] = arg_result - arguments.append("*%s" % (arg_name,)) - arg_explanations.append("*%s" % (arg_explanation,)) - - if getattr(call, 'kwargs', None): - arg_explanation, arg_result = self.visit(call.kwargs) - arg_name = "__exprinfo_kwds" - ns[arg_name] = arg_result - arguments.append("**%s" % (arg_name,)) - arg_explanations.append("**%s" % (arg_explanation,)) - args_explained = ", ".join(arg_explanations) - explanation = "%s(%s)" % (func_explanation, args_explained) - args = ", ".join(arguments) - source = "__exprinfo_func(%s)" % (args,) - co = self._compile(source) - try: - result = self.frame.eval(co, **ns) - except Exception: - raise Failure(explanation) - pattern = "%s\n{%s = %s\n}" - rep = self.frame.repr(result) - explanation = pattern % (rep, rep, explanation) - return explanation, result - - def _is_builtin_name(self, name): - pattern = "%r not in globals() and %r not in locals()" - source = pattern % (name.id, name.id) - co = self._compile(source) - try: - return self.frame.eval(co) - except Exception: - return False - - def visit_Attribute(self, attr): - if not isinstance(attr.ctx, ast.Load): - return self.generic_visit(attr) - source_explanation, source_result = self.visit(attr.value) - explanation = "%s.%s" % (source_explanation, attr.attr) - source = "__exprinfo_expr.%s" % (attr.attr,) - co = self._compile(source) - try: - try: - result = self.frame.eval(co, __exprinfo_expr=source_result) - except AttributeError: - # Maybe the attribute name needs to be mangled? - if not attr.attr.startswith("__") or attr.attr.endswith("__"): - raise - source = "getattr(__exprinfo_expr.__class__, '__name__', '')" - co = self._compile(source) - class_name = self.frame.eval(co, __exprinfo_expr=source_result) - mangled_attr = "_" + class_name + attr.attr - source = "__exprinfo_expr.%s" % (mangled_attr,) - co = self._compile(source) - result = self.frame.eval(co, __exprinfo_expr=source_result) - except Exception: - raise Failure(explanation) - explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result), - self.frame.repr(result), - source_explanation, attr.attr) - # Check if the attr is from an instance. - source = "%r in getattr(__exprinfo_expr, '__dict__', {})" - source = source % (attr.attr,) - co = self._compile(source) - try: - from_instance = self.frame.eval(co, __exprinfo_expr=source_result) - except Exception: - from_instance = None - if from_instance is None or self.frame.is_true(from_instance): - rep = self.frame.repr(result) - pattern = "%s\n{%s = %s\n}" - explanation = pattern % (rep, rep, explanation) - return explanation, result - - def visit_Assert(self, assrt): - test_explanation, test_result = self.visit(assrt.test) - explanation = "assert %s" % (test_explanation,) - if not self.frame.is_true(test_result): - try: - raise util.BuiltinAssertionError - except Exception: - raise Failure(explanation) - return explanation, test_result - - def visit_Assign(self, assign): - value_explanation, value_result = self.visit(assign.value) - explanation = "... = %s" % (value_explanation,) - name = ast.Name("__exprinfo_expr", ast.Load(), - lineno=assign.value.lineno, - col_offset=assign.value.col_offset) - new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno, - col_offset=assign.col_offset) - mod = ast.Module([new_assign]) - co = self._compile(mod, "exec") - try: - self.frame.exec_(co, __exprinfo_expr=value_result) - except Exception: - raise Failure(explanation) - return explanation, value_result - diff --git a/_pytest/config.py b/_pytest/config.py index aad8e0d04..68a3dafbe 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -941,10 +941,12 @@ class Config(object): """ ns, unknown_args = self._parser.parse_known_and_unknown_args(args) mode = ns.assertmode - self._warn_about_missing_assertion(mode) - if mode != 'plain': - hook = _pytest.assertion.install_importhook(self, mode) - if hook: + if mode == 'rewrite': + try: + hook = _pytest.assertion.install_importhook(self) + except SystemError: + mode = 'plain' + else: self.pluginmanager.rewrite_hook = hook for entrypoint in pkg_resources.iter_entry_points('pytest11'): for entry in entrypoint.dist._get_metadata('RECORD'): @@ -957,6 +959,7 @@ class Config(object): elif is_package: package_name = os.path.dirname(fn) hook.mark_rewrite(package_name) + self._warn_about_missing_assertion(mode) def _warn_about_missing_assertion(self, mode): try: @@ -964,15 +967,16 @@ class Config(object): except AssertionError: pass else: - if mode == "rewrite": - specifically = ("assertions not in test modules or plugins" - "will be ignored") + if mode == 'plain': + sys.stderr.write("WARNING: ASSERTIONS ARE NOT EXECUTED" + " and FAILING TESTS WILL PASS. Are you" + " using python -O?") else: - specifically = "failing tests may report as passing" - sys.stderr.write("WARNING: " + specifically + - " because assert statements are not executed " - "by the underlying Python interpreter " - "(are you using python -O?)\n") + sys.stderr.write("WARNING: assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n") def _preparse(self, args, addopts=True): self._initini(args) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 0db4ad2ab..6f1d9d3cc 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -66,24 +66,6 @@ def test_code_from_func(): assert co.path - -def test_builtin_patch_unpatch(monkeypatch): - cpy_builtin = py.builtin.builtins - comp = cpy_builtin.compile - def mycompile(*args, **kwargs): - return comp(*args, **kwargs) - class Sub(AssertionError): - pass - monkeypatch.setattr(cpy_builtin, 'AssertionError', Sub) - monkeypatch.setattr(cpy_builtin, 'compile', mycompile) - _pytest._code.patch_builtins() - assert cpy_builtin.AssertionError != Sub - assert cpy_builtin.compile != mycompile - _pytest._code.unpatch_builtins() - assert cpy_builtin.AssertionError is Sub - assert cpy_builtin.compile == mycompile - - def test_unicode_handling(): value = py.builtin._totext('\xc4\x85\xc4\x87\n', 'utf-8').encode('utf8') def f(): diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 2ccdd7028..64d1ff89e 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -274,18 +274,6 @@ class TestTraceback_f_g_h: assert entry.lineno == co.firstlineno + 2 assert entry.frame.code.name == 'g' -def hello(x): - x + 5 - -def test_tbentry_reinterpret(): - try: - hello("hello") - except TypeError: - excinfo = _pytest._code.ExceptionInfo() - tbentry = excinfo.traceback[-1] - msg = tbentry.reinterpret() - assert msg.startswith("TypeError: ('hello' + 5)") - def test_excinfo_exconly(): excinfo = pytest.raises(ValueError, h) assert excinfo.exconly().startswith('ValueError') @@ -431,7 +419,7 @@ class TestFormattedExcinfo: assert lines == [ ' def f():', '> assert 0', - 'E assert 0' + 'E AssertionError' ] @@ -770,23 +758,6 @@ raise ValueError() assert reprtb.extraline == "!!! Recursion detected (same locals & position)" assert str(reprtb) - def test_tb_entry_AssertionError(self, importasmod): - # probably this test is a bit redundant - # as py/magic/testing/test_assertion.py - # already tests correctness of - # assertion-reinterpretation logic - mod = importasmod(""" - def somefunc(): - x = 1 - assert x == 2 - """) - excinfo = pytest.raises(AssertionError, mod.somefunc) - - p = FormattedExcinfo() - reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - lines = reprentry.lines - assert lines[-1] == "E assert 1 == 2" - def test_reprexcinfo_getrepr(self, importasmod): mod = importasmod(""" def f(x): @@ -935,21 +906,6 @@ raise ValueError() repr.toterminal(tw) assert tw.stringio.getvalue() - - def test_native_style(self): - excinfo = self.excinfo_from_exec(""" - assert 0 - """) - repr = excinfo.getrepr(style='native') - assert "assert 0" in str(repr.reprcrash) - s = str(repr) - assert s.startswith('Traceback (most recent call last):\n File') - assert s.endswith('\nAssertionError: assert 0') - assert 'exec (source.compile())' in s - # python 2.4 fails to get the source line for the assert - if py.std.sys.version_info >= (2, 5): - assert s.count('assert 0') == 2 - def test_traceback_repr_style(self, importasmod): mod = importasmod(""" def f(): @@ -1079,4 +1035,4 @@ def test_cwd_deleted(testdir): """) result = testdir.runpytest() result.stdout.fnmatch_lines(['* 1 failed in *']) - assert 'INTERNALERROR' not in result.stdout.str() + result.stderr.str() \ No newline at end of file + assert 'INTERNALERROR' not in result.stdout.str() + result.stderr.str() diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py deleted file mode 100644 index 67a352ce7..000000000 --- a/testing/test_assertinterpret.py +++ /dev/null @@ -1,274 +0,0 @@ -"PYTEST_DONT_REWRITE" -import py -import pytest -from _pytest.assertion import util - - -def exvalue(): - return py.std.sys.exc_info()[1] - -def f(): - return 2 - -def test_not_being_rewritten(): - assert "@py_builtins" not in globals() - -def test_assert(): - try: - assert f() == 3 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 == 3\n') - -def test_assert_with_explicit_message(): - try: - assert f() == 3, "hello" - except AssertionError: - e = exvalue() - assert e.msg == 'hello' - -def test_assert_within_finally(): - excinfo = pytest.raises(ZeroDivisionError, """ - try: - 1/0 - finally: - i = 42 - """) - s = excinfo.exconly() - assert py.std.re.search("division.+by zero", s) is not None - - #def g(): - # A.f() - #excinfo = getexcinfo(TypeError, g) - #msg = getmsg(excinfo) - #assert msg.find("must be called with A") != -1 - - -def test_assert_multiline_1(): - try: - assert (f() == - 3) - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 == 3\n') - -def test_assert_multiline_2(): - try: - assert (f() == (4, - 3)[-1]) - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 ==') - -def test_in(): - try: - assert "hi" in [1, 2] - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 'hi' in") - -def test_is(): - try: - assert 1 is 2 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 1 is 2") - - -def test_attrib(): - class Foo(object): - b = 1 - i = Foo() - try: - assert i.b == 2 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 1 == 2") - -def test_attrib_inst(): - class Foo(object): - b = 1 - try: - assert Foo().b == 2 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 1 == 2") - -def test_len(): - l = list(range(42)) - try: - assert len(l) == 100 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 42 == 100") - assert "where 42 = len([" in s - -def test_assert_non_string_message(): - class A: - def __str__(self): - return "hello" - try: - assert 0 == 1, A() - except AssertionError: - e = exvalue() - assert e.msg == "hello" - -def test_assert_keyword_arg(): - def f(x=3): - return False - try: - assert f(x=5) - except AssertionError: - e = exvalue() - assert "x=5" in e.msg - -def test_private_class_variable(): - class X: - def __init__(self): - self.__v = 41 - def m(self): - assert self.__v == 42 - try: - X().m() - except AssertionError: - e = exvalue() - assert "== 42" in e.msg - -# These tests should both fail, but should fail nicely... -class WeirdRepr: - def __repr__(self): - return '' - -def bug_test_assert_repr(): - v = WeirdRepr() - try: - assert v == 1 - except AssertionError: - e = exvalue() - assert e.msg.find('WeirdRepr') != -1 - assert e.msg.find('second line') != -1 - assert 0 - -def test_assert_non_string(): - try: - assert 0, ['list'] - except AssertionError: - e = exvalue() - assert e.msg.find("list") != -1 - -def test_assert_implicit_multiline(): - try: - x = [1,2,3] - assert x != [1, - 2, 3] - except AssertionError: - e = exvalue() - assert e.msg.find('assert [1, 2, 3] !=') != -1 - - -def test_assert_with_brokenrepr_arg(): - class BrokenRepr: - def __repr__(self): 0 / 0 - e = AssertionError(BrokenRepr()) - if e.msg.find("broken __repr__") == -1: - pytest.fail("broken __repr__ not handle correctly") - -def test_multiple_statements_per_line(): - try: - a = 1; assert a == 2 - except AssertionError: - e = exvalue() - assert "assert 1 == 2" in e.msg - -def test_power(): - try: - assert 2**3 == 7 - except AssertionError: - e = exvalue() - assert "assert (2 ** 3) == 7" in e.msg - - -def test_assert_customizable_reprcompare(monkeypatch): - monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') - try: - assert 3 == 4 - except AssertionError: - e = exvalue() - s = str(e) - assert "hello" in s - -def test_assert_long_source_1(): - try: - assert len == [ - (None, ['somet text', 'more text']), - ] - except AssertionError: - e = exvalue() - s = str(e) - assert 're-run' not in s - assert 'somet text' in s - -def test_assert_long_source_2(): - try: - assert(len == [ - (None, ['somet text', 'more text']), - ]) - except AssertionError: - e = exvalue() - s = str(e) - assert 're-run' not in s - assert 'somet text' in s - -def test_assert_raise_alias(testdir): - testdir.makepyfile(""" - "PYTEST_DONT_REWRITE" - import sys - EX = AssertionError - def test_hello(): - raise EX("hello" - "multi" - "line") - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*def test_hello*", - "*raise EX*", - "*1 failed*", - ]) - - -def test_assert_raise_subclass(): - class SomeEx(AssertionError): - def __init__(self, *args): - super(SomeEx, self).__init__() - try: - raise SomeEx("hello") - except AssertionError: - s = str(exvalue()) - assert 're-run' not in s - assert 'could not determine' in s - -def test_assert_raises_in_nonzero_of_object_pytest_issue10(): - class A(object): - def __nonzero__(self): - raise ValueError(42) - def __lt__(self, other): - return A() - def __repr__(self): - return "" - def myany(x): - return True - try: - assert not(myany(A() < 0)) - except AssertionError: - e = exvalue() - s = str(e) - assert " < 0" in s diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 215d3e419..63d6ac98e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -3,10 +3,8 @@ import sys import textwrap import _pytest.assertion as plugin -import _pytest._code import py import pytest -from _pytest.assertion import reinterpret from _pytest.assertion import util PY3 = sys.version_info >= (3, 0) @@ -23,14 +21,10 @@ def mock_config(): return Config() -def interpret(expr): - return reinterpret.reinterpret(expr, _pytest._code.Frame(sys._getframe(1))) - - class TestImportHookInstallation: @pytest.mark.parametrize('initial_conftest', [True, False]) - @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) + @pytest.mark.parametrize('mode', ['plain', 'rewrite']) def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode): """Test that conftest files are using assertion rewrite on import. (#1619) @@ -57,13 +51,11 @@ class TestImportHookInstallation: expected = 'E AssertionError' elif mode == 'rewrite': expected = '*assert 10 == 30*' - elif mode == 'reinterp': - expected = '*AssertionError:*was re-run*' else: assert 0 result.stdout.fnmatch_lines([expected]) - @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) + @pytest.mark.parametrize('mode', ['plain', 'rewrite']) def test_pytest_plugins_rewrite(self, testdir, mode): contents = { 'conftest.py': """ @@ -88,13 +80,11 @@ class TestImportHookInstallation: expected = 'E AssertionError' elif mode == 'rewrite': expected = '*assert 10 == 30*' - elif mode == 'reinterp': - expected = '*AssertionError:*was re-run*' else: assert 0 result.stdout.fnmatch_lines([expected]) - @pytest.mark.parametrize('mode', ['plain', 'rewrite', 'reinterp']) + @pytest.mark.parametrize('mode', ['plain', 'rewrite']) def test_installed_plugin_rewrite(self, testdir, mode): # Make sure the hook is installed early enough so that plugins # installed via setuptools are re-written. @@ -161,8 +151,6 @@ class TestImportHookInstallation: expected = 'E AssertionError' elif mode == 'rewrite': expected = '*assert 10 == 30*' - elif mode == 'reinterp': - expected = '*AssertionError:*was re-run*' else: assert 0 result.stdout.fnmatch_lines([expected]) @@ -206,7 +194,7 @@ class TestImportHookInstallation: result.stdout.fnmatch_lines(['>*assert a == b*', 'E*assert 2 == 3*', '>*assert l.pop() == 3*', - 'E*AssertionError*re-run*']) + 'E*AssertionError']) class TestBinReprIntegration: @@ -663,14 +651,6 @@ def test_assertion_options(testdir): result = testdir.runpytest_subprocess("--assert=plain") assert "3 == 4" not in result.stdout.str() -def test_old_assert_mode(testdir): - testdir.makepyfile(""" - def test_in_old_mode(): - assert "@py_builtins" not in globals() - """) - result = testdir.runpytest_subprocess("--assert=reinterp") - assert result.ret == 0 - def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" def test_hello(): From ff8fb4950e0caa662b6b43d0f7e190dd23368056 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Jul 2016 21:28:59 -0300 Subject: [PATCH 25/34] setup_* and teardown_* functions argument now optional setup_module, setup_function and setup_method extra argument are now optional and may be omitted. Fix #1728 --- CHANGELOG.rst | 5 +++ _pytest/python.py | 85 +++++++++++++++++++----------------- doc/en/xunit_setup.rst | 37 ++++++++++------ testing/test_runner.py | 7 +-- testing/test_runner_xunit.py | 52 ++++++++++++++++++++++ 5 files changed, 129 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0bc0e0102..8a8389548 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -159,6 +159,10 @@ time or change existing behaviors in order to make them less surprising/more use automatically generated id for that argument will be used. Thanks `@palaviv`_ for the complete PR (`#1468`_). +* The parameter to xunit-style setup/teardown methods (``setup_method``, + ``setup_module``, etc.) is now optional and may be omitted. + Thanks `@okken`_ for bringing this to attention and `@nicoddemus`_ for the PR. + * Improved automatic id generation selection in case of duplicate ids in parametrize. Thanks `@palaviv`_ for the complete PR (`#1474`_). @@ -308,6 +312,7 @@ time or change existing behaviors in order to make them less surprising/more use .. _@nikratio: https://github.com/nikratio .. _@novas0x2a: https://github.com/novas0x2a .. _@obestwalter: https://github.com/obestwalter +.. _@okken: https://github.com/okken .. _@olegpidsadnyi: https://github.com/olegpidsadnyi .. _@omarkohl: https://github.com/omarkohl .. _@palaviv: https://github.com/palaviv diff --git a/_pytest/python.py b/_pytest/python.py index 4a4cd5dc4..fb374381d 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -25,10 +25,6 @@ cutdir2 = py.path.local(_pytest.__file__).dirpath() cutdir1 = py.path.local(pluggy.__file__.rstrip("oc")) -def _has_positional_arg(func): - return func.__code__.co_argcount - - def filter_traceback(entry): # entry.path might sometimes return a str object when the entry # points to dynamically generated code @@ -439,34 +435,51 @@ class Module(pytest.File, PyCollector): "decorator) is not allowed. Use @pytest.mark.skip or " "@pytest.mark.skipif instead." ) - #print "imported test module", mod self.config.pluginmanager.consider_module(mod) return mod def setup(self): - setup_module = xunitsetup(self.obj, "setUpModule") + setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule") if setup_module is None: - setup_module = xunitsetup(self.obj, "setup_module") + setup_module = _get_xunit_setup_teardown(self.obj, "setup_module") if setup_module is not None: - #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, its probably a pytest style one - # so we pass the current module object - if _has_positional_arg(setup_module): - setup_module(self.obj) - else: - setup_module() - fin = getattr(self.obj, 'tearDownModule', None) - if fin is None: - fin = getattr(self.obj, 'teardown_module', None) - if fin is not None: - #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, it's probably a pytest style one - # so we pass the current module object - if _has_positional_arg(fin): - finalizer = lambda: fin(self.obj) - else: - finalizer = fin - self.addfinalizer(finalizer) + setup_module() + + teardown_module = _get_xunit_setup_teardown(self.obj, 'tearDownModule') + if teardown_module is None: + teardown_module = _get_xunit_setup_teardown(self.obj, 'teardown_module') + if teardown_module is not None: + self.addfinalizer(teardown_module) + + +def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): + """ + Return a callable to perform xunit-style setup or teardown if + the function exists in the ``holder`` object. + The ``param_obj`` parameter is the parameter which will be passed to the function + when the callable is called without arguments, defaults to the ``holder`` object. + Return ``None`` if a suitable callable is not found. + """ + param_obj = param_obj if param_obj is not None else holder + result = _get_xunit_func(holder, attr_name) + if result is not None: + arg_count = result.__code__.co_argcount + if inspect.ismethod(result): + arg_count -= 1 + if arg_count: + return lambda: result(param_obj) + else: + return result + + +def _get_xunit_func(obj, name): + """Return the attribute from the given object to be used as a setup/teardown + xunit-style function, but only if not marked as a fixture to + avoid calling it twice. + """ + meth = getattr(obj, name, None) + if fixtures.getfixturemarker(meth) is None: + return meth class Class(PyCollector): @@ -479,7 +492,7 @@ class Class(PyCollector): return [self._getcustomclass("Instance")(name="()", parent=self)] def setup(self): - setup_class = xunitsetup(self.obj, 'setup_class') + setup_class = _get_xunit_func(self.obj, 'setup_class') if setup_class is not None: setup_class = getattr(setup_class, 'im_func', setup_class) setup_class = getattr(setup_class, '__func__', setup_class) @@ -523,12 +536,12 @@ class FunctionMixin(PyobjMixin): else: setup_name = 'setup_function' teardown_name = 'teardown_function' - setup_func_or_method = xunitsetup(obj, setup_name) + setup_func_or_method = _get_xunit_setup_teardown(obj, setup_name, param_obj=self.obj) if setup_func_or_method is not None: - setup_func_or_method(self.obj) - fin = getattr(obj, teardown_name, None) - if fin is not None: - self.addfinalizer(lambda: fin(self.obj)) + setup_func_or_method() + teardown_func_or_method = _get_xunit_setup_teardown(obj, teardown_name, param_obj=self.obj) + if teardown_func_or_method is not None: + self.addfinalizer(teardown_func_or_method) def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: @@ -1494,11 +1507,3 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr): fixtures.fillfixtures(self) - - - - -def xunitsetup(obj, name): - meth = getattr(obj, name, None) - if fixtures.getfixturemarker(meth) is None: - return meth diff --git a/doc/en/xunit_setup.rst b/doc/en/xunit_setup.rst index 7a80f1299..148fb1209 100644 --- a/doc/en/xunit_setup.rst +++ b/doc/en/xunit_setup.rst @@ -7,21 +7,20 @@ classic xunit-style setup This section describes a classic and popular way how you can implement fixtures (setup and teardown test state) on a per-module/class/function basis. -pytest started supporting these methods around 2005 and subsequently -nose and the standard library introduced them (under slightly different -names). While these setup/teardown methods are and will remain fully -supported you may also use pytest's more powerful :ref:`fixture mechanism -` which leverages the concept of dependency injection, allowing -for a more modular and more scalable approach for managing test state, -especially for larger projects and for functional testing. You can -mix both fixture mechanisms in the same file but unittest-based -test methods cannot receive fixture arguments. + .. note:: - As of pytest-2.4, teardownX functions are not called if - setupX existed and failed/was skipped. This harmonizes - behaviour across all major python testing tools. + While these setup/teardown methods are simple and familiar to those + coming from a ``unittest`` or nose ``background``, you may also consider + using pytest's more powerful :ref:`fixture mechanism + ` which leverages the concept of dependency injection, allowing + for a more modular and more scalable approach for managing test state, + especially for larger projects and for functional testing. You can + mix both fixture mechanisms in the same file but + test methods of ``unittest.TestCase`` subclasses + cannot receive fixture arguments. + Module level setup/teardown -------------------------------------- @@ -38,6 +37,8 @@ which will usually be called once for all the functions:: method. """ +As of pytest-3.0, the ``module`` parameter is optional. + Class level setup/teardown ---------------------------------- @@ -71,6 +72,8 @@ Similarly, the following methods are called around each method invocation:: call. """ +As of pytest-3.0, the ``method`` parameter is optional. + If you would rather define test functions directly at module level you can also use the following functions to implement fixtures:: @@ -84,7 +87,13 @@ you can also use the following functions to implement fixtures:: call. """ -Note that it is possible for setup/teardown pairs to be invoked multiple times -per testing process. +As of pytest-3.0, the ``function`` parameter is optional. + +Remarks: + +* It is possible for setup/teardown pairs to be invoked multiple times + per testing process. +* teardown functions are not called if the corresponding setup function existed + and failed/was skipped. .. _`unittest.py module`: http://docs.python.org/library/unittest.html diff --git a/testing/test_runner.py b/testing/test_runner.py index 5826d9601..bc3ff6c89 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -229,11 +229,12 @@ class BaseFunctionalTests: assert reps[5].failed def test_exact_teardown_issue1206(self, testdir): + """issue shadowing error with wrong number of arguments on teardown_method.""" rec = testdir.inline_runsource(""" import pytest class TestClass: - def teardown_method(self): + def teardown_method(self, x, y, z): pass def test_method(self): @@ -256,9 +257,9 @@ class BaseFunctionalTests: assert reps[2].when == "teardown" assert reps[2].longrepr.reprcrash.message in ( # python3 error - 'TypeError: teardown_method() takes 1 positional argument but 2 were given', + "TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'", # python2 error - 'TypeError: teardown_method() takes exactly 1 argument (2 given)' + 'TypeError: teardown_method() takes exactly 4 arguments (2 given)' ) def test_failure_in_setup_function_ignores_custom_repr(self, testdir): diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index dc8ae9992..e1f0924c6 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -1,6 +1,8 @@ # # test correct setup/teardowns at # module, class, and instance level +import pytest + def test_module_and_function_setup(testdir): reprec = testdir.inline_runsource(""" @@ -251,3 +253,53 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir): "*2 error*" ]) assert "xyz43" not in result.stdout.str() + + +@pytest.mark.parametrize('arg', ['', 'arg']) +def test_setup_teardown_function_level_with_optional_argument(testdir, monkeypatch, arg): + """parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" + import sys + trace_setups_teardowns = [] + monkeypatch.setattr(sys, 'trace_setups_teardowns', trace_setups_teardowns, raising=False) + p = testdir.makepyfile(""" + import pytest + import sys + + trace = sys.trace_setups_teardowns.append + + def setup_module({arg}): trace('setup_module') + def teardown_module({arg}): trace('teardown_module') + + def setup_function({arg}): trace('setup_function') + def teardown_function({arg}): trace('teardown_function') + + def test_function_1(): pass + def test_function_2(): pass + + class Test: + def setup_method(self, {arg}): trace('setup_method') + def teardown_method(self, {arg}): trace('teardown_method') + + def test_method_1(self): pass + def test_method_2(self): pass + """.format(arg=arg)) + result = testdir.inline_run(p) + result.assertoutcome(passed=4) + + expected = [ + 'setup_module', + + 'setup_function', + 'teardown_function', + 'setup_function', + 'teardown_function', + + 'setup_method', + 'teardown_method', + + 'setup_method', + 'teardown_method', + + 'teardown_module', + ] + assert trace_setups_teardowns == expected From 0a53797fa31cb6bce5b9a2787da8a43e69ffc02e Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sun, 17 Jul 2016 12:30:21 +0100 Subject: [PATCH 26/34] Document the re-writing of plugins --- doc/en/writing_plugins.rst | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index bcd795d82..af47bad09 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -176,6 +176,63 @@ If a package is installed this way, ``pytest`` will load to make it easy for users to find your plugin. +Assertion Rewriting +------------------- + +One of the main features of ``pytest`` is the use of plain assert +statements and the detailed introspection of expressions upon +assertion failures. This is provided by "assertion rewriting" which +modifies the parsed AST before it gets compiled to bytecode. This is +done via a :pep:`302` import hook which gets installed early on when +``pytest`` starts up and will perform this re-writing when modules get +imported. However since we do not want to test different bytecode +then you will run in production this hook only re-writes test modules +themselves as well as any modules which are part of plugins. Any +other imported module will not be re-written and normal assertion +behaviour will happen. + +If you have assertion helpers in other modules where you would need +assertion rewriting to be enabled you need to ask ``pytest`` +explicitly to re-write this module before it gets imported. + +.. autofunction:: pytest.register_assert_rewrite + +This is especially important when you write a pytest plugin which is +created using a package. The import hook only treats ``conftest.py`` +files and any modules which are listed in the ``pytest11`` entrypoint +as plugins. As an example consider the following package:: + + pytest_foo/__init__.py + pytest_foo/plugin.py + pytest_foo/helper.py + +With the following typical ``setup.py`` extract: + +.. code-block:: python + + setup( + ... + entry_points={'pytest11': ['foo = pytest_foo.plugin']}, + ... + ) + +In this case only ``pytest_foo/plugin.py`` will be re-written. If the +helper module also contains assert statements which need to be +re-written it needs to be marked as such, before it gets imported. +This is easiest by marking it for re-writing inside the +``__init__.py`` module, which will always be imported first when a +module inside a package is imported. This way ``plugin.py`` can still +import ``helper.py`` normally. The contents of +``pytest_foo/__init__.py`` will then need to look like this: + +.. code-block:: python + + import pytest + + pytest.register_assert_rewrite('pytest_foo.helper') + + + Requiring/Loading plugins in a test module or conftest file ----------------------------------------------------------- @@ -190,6 +247,16 @@ will be loaded as well. You can also use dotted path like this:: which will import the specified module as a ``pytest`` plugin. +Plugins imported like this will automatically be marked to require +assertion rewriting using the :func:`pytest.register_assert_rewrite` +mechanism. However for this to have any effect the module must not be +imported already, it it was already imported at the time the +``pytest_plugins`` statement is processed a warning will result and +assertions inside the plugin will not be re-written. To fix this you +can either call :func:`pytest.register_assert_rewrite` yourself before +the module is imported, or you can arrange the code to delay the +importing until after the plugin is registered. + Accessing another plugin by name -------------------------------- From 58a8150bc5eec20eef15d06971ff6e40b310c674 Mon Sep 17 00:00:00 2001 From: Oliver Bestwalter Date: Sun, 17 Jul 2016 21:14:16 +0200 Subject: [PATCH 27/34] add backwards compatibility policy --- doc/en/backwards-compatibility.rst | 12 ++++++++++++ doc/en/contents.rst | 1 + 2 files changed, 13 insertions(+) create mode 100644 doc/en/backwards-compatibility.rst diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst new file mode 100644 index 000000000..8ceada52d --- /dev/null +++ b/doc/en/backwards-compatibility.rst @@ -0,0 +1,12 @@ +.. _backwards-compatibility: + +Backwards Compatibility Policy +============================== + +Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary. + +With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around. + +To communicate changes we are already issuing deprecation warnings, but they are not displayed by default. In pytest 3.0 we changed the default setting so that pytest deprecation warnings are displayed if not explicitly silenced (with ``--disable-pytest-warnings``). + +We will only remove deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we will not remove it in 4.0 but in 5.0). diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 48c3471b5..b007e7de8 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -20,6 +20,7 @@ Full pytest documentation cache plugins + backwards-compatibility contributing talks From 317b3f257da727419118b8c8bd5e3280dd47e8c0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 19 Jul 2016 10:20:41 +0200 Subject: [PATCH 28/34] optparse compatibility - add float and complex also documents the implementation quality fixes #457 --- _pytest/config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 68a3dafbe..17d0837cc 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -552,11 +552,18 @@ class ArgumentError(Exception): class Argument: - """class that mimics the necessary behaviour of optparse.Option """ + """class that mimics the necessary behaviour of optparse.Option + + its currently a least effort implementation + and ignoring choices and integer prefixes + https://docs.python.org/3/library/optparse.html#optparse-standard-option-types + """ _typ_map = { 'int': int, 'string': str, - } + 'float': float, + 'complex': complex, + } # enable after some grace period for plugin writers TYPE_WARN = False From 61cc5c4d4e661bf7a8d3e679f3999c7f2302d861 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 19 Jul 2016 10:33:25 +0200 Subject: [PATCH 29/34] argument parsing: always warn for string types fix #1741 --- _pytest/config.py | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 17d0837cc..a667df75d 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -564,8 +564,6 @@ class Argument: 'float': float, 'complex': complex, } - # enable after some grace period for plugin writers - TYPE_WARN = False def __init__(self, *names, **attrs): """store parms in private vars for use in add_argument""" @@ -573,17 +571,12 @@ class Argument: self._short_opts = [] self._long_opts = [] self.dest = attrs.get('dest') - if self.TYPE_WARN: - try: - help = attrs['help'] - if '%default' in help: - warnings.warn( - 'pytest now uses argparse. "%default" should be' - ' changed to "%(default)s" ', - FutureWarning, - stacklevel=3) - except KeyError: - pass + if '%default' in (attrs.get('help') or ''): + warnings.warn( + 'pytest now uses argparse. "%default" should be' + ' changed to "%(default)s" ', + DeprecationWarning, + stacklevel=3) try: typ = attrs['type'] except KeyError: @@ -592,25 +585,23 @@ class Argument: # this might raise a keyerror as well, don't want to catch that if isinstance(typ, py.builtin._basestring): if typ == 'choice': - if self.TYPE_WARN: - warnings.warn( - 'type argument to addoption() is a string %r.' - ' For parsearg this is optional and when supplied ' - ' should be a type.' - ' (options: %s)' % (typ, names), - FutureWarning, - stacklevel=3) + warnings.warn( + 'type argument to addoption() is a string %r.' + ' For parsearg this is optional and when supplied ' + ' should be a type.' + ' (options: %s)' % (typ, names), + DeprecationWarning, + stacklevel=3) # argparse expects a type here take it from # the type of the first element attrs['type'] = type(attrs['choices'][0]) else: - if self.TYPE_WARN: - warnings.warn( - 'type argument to addoption() is a string %r.' - ' For parsearg this should be a type.' - ' (options: %s)' % (typ, names), - FutureWarning, - stacklevel=3) + warnings.warn( + 'type argument to addoption() is a string %r.' + ' For parsearg this should be a type.' + ' (options: %s)' % (typ, names), + DeprecationWarning, + stacklevel=3) attrs['type'] = Argument._typ_map[typ] # used in test_parseopt -> test_parse_defaultgetter self.type = attrs['type'] From 9af872a2305c7306175991fd64721741b87223f1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 19 Jul 2016 12:42:08 +0200 Subject: [PATCH 30/34] update changelog --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3ff14be7f..f86cdce71 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -220,6 +220,10 @@ time or change existing behaviors in order to make them less surprising/more use still present but is now considered deprecated. Thanks to `@RedBeardCode`_ and `@tomviner`_ for the PR (`#1626`_). +* ``optparse`` type usage now triggers DeprecationWarnings (`#1740`_). + +* ``optparse`` backward compatibility supports float/complex types (`#457`_). + * * @@ -256,6 +260,8 @@ time or change existing behaviors in order to make them less surprising/more use .. _#372: https://github.com/pytest-dev/pytest/issues/372 .. _#460: https://github.com/pytest-dev/pytest/pull/460 +.. _#457: https://github.com/pytest-dev/pytest/issues/457 +.. _#1740: https://github.com/pytest-dev/pytest/issues/1740 .. _#607: https://github.com/pytest-dev/pytest/issues/607 .. _#925: https://github.com/pytest-dev/pytest/issues/925 .. _#1235: https://github.com/pytest-dev/pytest/issues/1235 From 2a05c311e90a82ee547fa16fdcd77e12327eb5e0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 20 Jul 2016 17:15:29 +0200 Subject: [PATCH 31/34] implement exitfirst as store_const option this makes it possible to override with a later maxfail --- _pytest/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index 845d5dd00..6119fcb03 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -34,8 +34,8 @@ def pytest_addoption(parser): # "**/test_*.py", "**/*_test.py"] #) group = parser.getgroup("general", "running and selection options") - group._addoption('-x', '--exitfirst', action="store_true", default=False, - dest="exitfirst", + group._addoption('-x', '--exitfirst', action="store_const", + dest="maxfail", const=1, help="exit instantly on first error or failed test."), group._addoption('--maxfail', metavar="num", action="store", type=int, dest="maxfail", default=0, @@ -74,10 +74,10 @@ def pytest_namespace(): collect = dict(Item=Item, Collector=Collector, File=File, Session=Session) return dict(collect=collect) + def pytest_configure(config): pytest.config = config # compatibiltiy - if config.option.exitfirst: - config.option.maxfail = 1 + def wrap_session(config, doit): """Skeleton command line program""" From 3fd8257c170591078ff1a8eb489ccf82de7dd8f0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 20 Jul 2016 17:20:10 +0200 Subject: [PATCH 32/34] add test for --maxfail=NUM overiding -x --- testing/test_session.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/test_session.py b/testing/test_session.py index e3b9f0fcc..a7dcb27a4 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -197,6 +197,14 @@ class TestNewSession(SessionTests): colfail = [x for x in finished if x.failed] assert len(colfail) == 1 + def test_minus_x_overriden_by_maxfail(self, testdir): + testdir.makepyfile(__init__="") + testdir.makepyfile(test_one="xxxx", test_two="yyyy", test_third="zzz") + reprec = testdir.inline_run("-x", "--maxfail=2", testdir.tmpdir) + finished = reprec.getreports("pytest_collectreport") + colfail = [x for x in finished if x.failed] + assert len(colfail) == 2 + def test_plugin_specify(testdir): pytest.raises(ImportError, """ From 0403266bf0e7180d31a4c394a6f207ba4affe6b0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 20 Jul 2016 17:23:07 +0200 Subject: [PATCH 33/34] record --exitfirst change in changelog --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f86cdce71..ec7ad5fea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -51,6 +51,9 @@ time or change existing behaviors in order to make them less surprising/more use * ``_pytest.monkeypatch.monkeypatch`` class has been renamed to ``_pytest.monkeypatch.MonkeyPatch`` so it doesn't conflict with the ``monkeypatch`` fixture. +* ``--exitfirst / -x`` can now be overridden by a following ``--maxfail=N`` + and is just a synonym for ``--maxfail=1``. + * * From 9dadaa8a4122d38e0e55b5f44a15e45674fd346d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 20 Jul 2016 17:45:20 +0200 Subject: [PATCH 34/34] skipping plugin: remove python2.5 compat code --- _pytest/skipping.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 18e038d2c..1d557a3fe 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -108,11 +108,7 @@ class MarkEvaluator: def _getglobals(self): d = {'os': os, 'sys': sys, 'config': self.item.config} - func = self.item.obj - try: - d.update(func.__globals__) - except AttributeError: - d.update(func.func_globals) + d.update(self.item.obj.__globals__) return d def _istrue(self):