From beaa8e55bd6453a929c458570678c4d4b0d4bed3 Mon Sep 17 00:00:00 2001 From: Galaczi Endre Date: Mon, 21 Sep 2015 14:18:29 +0100 Subject: [PATCH 01/69] Fixes #653 use deprecated_call as context_manager --- AUTHORS | 1 + CHANGELOG | 1 + _pytest/recwarn.py | 11 ++++++++++- doc/en/recwarn.rst | 6 ++++++ testing/test_recwarn.py | 11 +++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 0b0f10052..49bd6ca85 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,6 +28,7 @@ Dave Hunt David Mohr Edison Gustavo Muenz Eduardo Schettino +Endre Galaczi Elizaveta Shashkova Eric Hunsberger Eric Siegerman diff --git a/CHANGELOG b/CHANGELOG index 9cc6d628c..465ca2f0e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ - Fix issue #411: Add __eq__ method to assertion comparison example. Thanks Ben Webb. +- Fix issue #653: deprecated_call can be used as context manager. 2.8.0 ----------------------------- diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index abefdfac1..a4e2d763c 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -28,9 +28,18 @@ def pytest_namespace(): 'warns': warns} -def deprecated_call(func, *args, **kwargs): +def deprecated_call(func=None, *args, **kwargs): """Assert that ``func(*args, **kwargs)`` triggers a DeprecationWarning. + + This function can be used as a context manager:: + + >>> with deprecated_call(): + ... myobject.deprecated_method() """ + if not func: + warnings.simplefilter('always') + return WarningsChecker(expected_warning=DeprecationWarning) + wrec = WarningsRecorder() with wrec: warnings.simplefilter('always') # ensure all warnings are triggered diff --git a/doc/en/recwarn.rst b/doc/en/recwarn.rst index c2a1e65fa..ead162f4e 100644 --- a/doc/en/recwarn.rst +++ b/doc/en/recwarn.rst @@ -114,3 +114,9 @@ command ``warnings.simplefilter('always')``:: warnings.warn("deprecated", DeprecationWarning) assert len(recwarn) == 1 assert recwarn.pop(DeprecationWarning) + +You can also use it as a contextmanager:: + + def test_global(): + with pytest.deprecated_call(): + myobject.deprecated_method() diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 644b09ef7..f441187a6 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -79,6 +79,7 @@ def dep_explicit(i): filename="hello", lineno=3) class TestDeprecatedCall(object): + def test_deprecated_call_raises(self): excinfo = pytest.raises(AssertionError, "pytest.deprecated_call(dep, 3)") @@ -111,6 +112,16 @@ class TestDeprecatedCall(object): pytest.deprecated_call(dep_explicit, 0) pytest.deprecated_call(dep_explicit, 0) + def test_deprecated_call_as_context_manager_no_warning(self): + with pytest.raises(pytest.fail.Exception) as ex: + with pytest.deprecated_call(): + dep(1) + assert str(ex.value) == "DID NOT WARN" + + def test_deprecated_call_as_context_manager(self): + with pytest.deprecated_call(): + dep(0) + class TestWarns(object): def test_strings(self): From 9f77a8507efe8e5e021c7a65342b2b01aefb6765 Mon Sep 17 00:00:00 2001 From: Galaczi Endre Date: Mon, 21 Sep 2015 15:54:50 +0100 Subject: [PATCH 02/69] removed mutation of global state, changed filter addition in WarningsRecorder --- _pytest/recwarn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index a4e2d763c..5e7cdd84b 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -37,7 +37,6 @@ def deprecated_call(func=None, *args, **kwargs): ... myobject.deprecated_method() """ if not func: - warnings.simplefilter('always') return WarningsChecker(expected_warning=DeprecationWarning) wrec = WarningsRecorder() @@ -158,8 +157,8 @@ class WarningsRecorder(object): self._module.showwarning = showwarning # allow the same warning to be raised more than once - self._module.simplefilter('always', append=True) + self._module.simplefilter('always') return self def __exit__(self, *exc_info): From d8fbb0b8e3e2d21fc30a74eb9165edc2c838893d Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 22 Sep 2015 11:00:55 +0200 Subject: [PATCH 03/69] start features branch --- CHANGELOG | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d17d29def..ed7d70b14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,16 +1,6 @@ -2.8.x ------ +2.9.0.dev +--------- -- (experimental) adapt more SEMVER style versioning and change meaning of - master branch in git repo: "master" branch now keeps the bugfixes, changes - aimed for micro releases. "features" branch will only be be released - with minor or major pytest releases. - -- Fix issue #766 by removing documentation references to distutils. - Thanks Russel Winder. - -- Fix issue #411: Add __eq__ method to assertion comparison example. - Thanks Ben Webb. 2.8.0 ----------------------------- From 97f7815febde7b5425e789fd72e9ad63e1c2f2d2 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 22 Sep 2015 11:01:33 +0200 Subject: [PATCH 04/69] also change pytest version to target 2.9.0 --- _pytest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index e1978cb35..51751401b 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.8.0' +__version__ = '2.9.0.dev1' From 8a4517fd17325fa0716ddda7a2eb7a0ad7b1e7f3 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 22 Sep 2015 11:05:19 +0200 Subject: [PATCH 05/69] re-add 2.8.x changelog so that MASTER can be merged into features wrt to the changelog --- CHANGELOG | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ed7d70b14..9d454770a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ 2.9.0.dev --------- +2.8.x +----- + +- (experimental) adapt more SEMVER style versioning and change meaning of + master branch in git repo: "master" branch now keeps the bugfixes, changes + aimed for micro releases. "features" branch will only be be released + with minor or major pytest releases. + +- Fix issue #766 by removing documentation references to distutils. + Thanks Russel Winder. + +- Fix issue #411: Add __eq__ method to assertion comparison example. + Thanks Ben Webb. 2.8.0 ----------------------------- From 7c088d11045886874e29fb413c7840c3372b0177 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 22 Sep 2015 11:23:24 +0200 Subject: [PATCH 06/69] remove nonsense line --- CHANGELOG | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4478439c6..d74db53c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,5 @@ 2.9.0.dev --------- - -2.8.x ------ 2.8.1.dev --------- From b71add27daffb0bfb2719828b6a1755486092984 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 11:32:17 +0100 Subject: [PATCH 07/69] Add MarkEvaluator for skip --- _pytest/skipping.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 36e54d7d8..2ecef01dd 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -147,10 +147,16 @@ class MarkEvaluator: @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): - evalskip = MarkEvaluator(item, 'skipif') - if evalskip.istrue(): - item._evalskip = evalskip - pytest.skip(evalskip.getexplanation()) + eval_skipif = MarkEvaluator(item, 'skipif') + eval_skip = MarkEvaluator(item, 'skip') + + if eval_skipif.istrue(): + item._evalskip = eval_skipif + pytest.skip(eval_skipif.getexplanation()) + elif eval_skip.istrue(): + item._evalskip = eval_skip + pytest.skip(eval_skip.getexplanation()) + item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) From 4e94135d361c541a579180e25a55cbfa38021565 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 12:11:20 +0100 Subject: [PATCH 08/69] Remove incorrect use of pytest.mark.skip --- testing/test_capture.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 539333525..b5b374a72 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -556,7 +556,6 @@ def test_capture_binary_output(testdir): import subprocess subprocess.call([sys.executable, __file__]) - @pytest.mark.skip def test_foo(): import os;os.write(1, b'\xc3') From f144666f8bcb468c89d938c919af4bc43c935704 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 12:20:49 +0100 Subject: [PATCH 09/69] Work towards test coverage of mark.skip --- testing/test_skipping.py | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 1048c9455..2d12af427 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -382,7 +382,48 @@ class TestXFailwithSetupTeardown: ]) -class TestSkipif: +class TestSkip(object): + def test_skip_no_reason(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip + def test_foo(): + pass + """) + rec = testdir.inline_run() + rec.assertoutcome(skipped=1) + + def test_skip_with_reason(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip(reason="for lolz") + def test_bar(): + pass + """) + # cant use assertoutcome because we cant + # also check the reason + result = testdir.runpytest('-rs') + result.stdout.fnmatch_lines([ + "*for lolz*", + "*1 skipped*", + ]) + + def test_only_skips_marked_test(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip + def test_foo(): + pass + @pytest.mark.skip(reason="none") + def test_bar() : + pass + def test_baz(): + assert True + """) + rec = testdir.inline_run() + rec.assertoutcome(passed=1, skipped=2) + +class TestSkipif(object): def test_skipif_conditional(self, testdir): item = testdir.getitem(""" import pytest From ad0b8e31b8c43d6ca0f2379703c1013982633ab2 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 15:19:29 +0100 Subject: [PATCH 10/69] Fix case where skip is assigned to as an attribute directly --- _pytest/skipping.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 2ecef01dd..2733cac16 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -5,6 +5,7 @@ import traceback import py import pytest +from _pytest.mark import MarkInfo def pytest_addoption(parser): group = parser.getgroup("general") @@ -148,14 +149,15 @@ class MarkEvaluator: @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): eval_skipif = MarkEvaluator(item, 'skipif') - eval_skip = MarkEvaluator(item, 'skip') if eval_skipif.istrue(): item._evalskip = eval_skipif pytest.skip(eval_skipif.getexplanation()) - elif eval_skip.istrue(): - item._evalskip = eval_skip - pytest.skip(eval_skip.getexplanation()) + elif isinstance(item.keywords.get('skip'), MarkInfo): + eval_skip = MarkEvaluator(item, 'skip') + if eval_skip.istrue(): + item._evalskip = eval_skip + pytest.skip(eval_skip.getexplanation()) item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) From 61b8443723101d5419a50c97e80fd2329b8197b5 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 15:29:07 +0100 Subject: [PATCH 11/69] Update docs with new skip marker --- doc/en/skipping.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index fc4f66e76..b3eff61ab 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -31,7 +31,15 @@ Marking a test function to be skipped .. versionadded:: 2.0, 2.4 -Here is an example of marking a test function to be skipped +The simplest way to skip a function is to mark it with the `skip` decorator, +which may be passed an optional `reason`: + + @pytest.mark.skip(reason="no way of currently testing this") + def test_the_unknown(): + ... + +If you wish to skip something conditionally then you can use `skipif` instead. +Here is an example of marking a test function to bwe skipped when run on a Python3.3 interpreter:: import sys From 5ec08d308182d2b9c15def469b0062e8c3deca30 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 15:29:50 +0100 Subject: [PATCH 12/69] Delete trailing whitespace --- doc/en/skipping.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index b3eff61ab..fb416d3e0 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -29,15 +29,15 @@ corresponding to the "short" letters shown in the test progress:: Marking a test function to be skipped ------------------------------------------- -.. versionadded:: 2.0, 2.4 - -The simplest way to skip a function is to mark it with the `skip` decorator, -which may be passed an optional `reason`: +The simplest way to skip a function is to mark it with the `skip` decorator +(added in 2.8) which may be passed an optional `reason`: @pytest.mark.skip(reason="no way of currently testing this") def test_the_unknown(): ... +.. versionadded:: 2.0, 2.4 + If you wish to skip something conditionally then you can use `skipif` instead. Here is an example of marking a test function to bwe skipped when run on a Python3.3 interpreter:: @@ -176,12 +176,12 @@ Running it with the report-on-xfail option gives this output:: platform linux -- Python 3.4.3, pytest-2.8.1, py-1.4.30, pluggy-0.3.1 rootdir: $REGENDOC_TMPDIR/example, inifile: collected 7 items - + xfail_demo.py xxxxxxx ======= short test summary info ======== XFAIL xfail_demo.py::test_hello XFAIL xfail_demo.py::test_hello2 - reason: [NOTRUN] + reason: [NOTRUN] XFAIL xfail_demo.py::test_hello3 condition: hasattr(os, 'sep') XFAIL xfail_demo.py::test_hello4 @@ -191,7 +191,7 @@ Running it with the report-on-xfail option gives this output:: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - + ======= 7 xfailed in 0.12 seconds ======== .. _`skip/xfail with parametrize`: From dc7153e33cc89895311637a2f57e878549d4d6ca Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 15:33:48 +0100 Subject: [PATCH 13/69] Spelling and grammar fixes --- doc/en/skipping.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index fb416d3e0..ffb5732b7 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -29,7 +29,7 @@ corresponding to the "short" letters shown in the test progress:: Marking a test function to be skipped ------------------------------------------- -The simplest way to skip a function is to mark it with the `skip` decorator +The simplest way to skip a test function is to mark it with the `skip` decorator (added in 2.8) which may be passed an optional `reason`: @pytest.mark.skip(reason="no way of currently testing this") @@ -39,7 +39,7 @@ The simplest way to skip a function is to mark it with the `skip` decorator .. versionadded:: 2.0, 2.4 If you wish to skip something conditionally then you can use `skipif` instead. -Here is an example of marking a test function to bwe skipped +Here is an example of marking a test function to be skipped when run on a Python3.3 interpreter:: import sys From 771aef9ddbd7927e30656aedc20aee5ca9fd8b8d Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Mon, 21 Sep 2015 15:47:11 +0100 Subject: [PATCH 14/69] Add a test_skip_class test --- testing/test_skipping.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 2d12af427..22e5d7302 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -383,6 +383,22 @@ class TestXFailwithSetupTeardown: class TestSkip(object): + def test_skip_class(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip + class TestSomething(object): + def test_foo(self): + pass + def test_bar(self): + pass + + def test_baz(): + pass + """) + rec = testdir.inline_run() + rec.assertoutcome(skipped=2, passed=1) + def test_skip_no_reason(self, testdir): testdir.makepyfile(""" import pytest From abc27f56fcb49bd419dfd09b0492509fa0555684 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Wed, 23 Sep 2015 15:04:04 +0100 Subject: [PATCH 15/69] Update skipping.rst with correct version marker --- doc/en/skipping.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index ffb5732b7..25e3c8116 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -29,8 +29,10 @@ corresponding to the "short" letters shown in the test progress:: Marking a test function to be skipped ------------------------------------------- +.. versionadded:: 2.9 + The simplest way to skip a test function is to mark it with the `skip` decorator -(added in 2.8) which may be passed an optional `reason`: +which may be passed an optional `reason`: @pytest.mark.skip(reason="no way of currently testing this") def test_the_unknown(): From d1628944a667c8007da1fc2ff897adaf7a80f985 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sun, 27 Sep 2015 22:49:09 +0100 Subject: [PATCH 16/69] Update skippings tests for better coverage --- testing/test_skipping.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 22e5d7302..b0d3beff3 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -399,6 +399,16 @@ class TestSkip(object): rec = testdir.inline_run() rec.assertoutcome(skipped=2, passed=1) + def test_skips_on_false_string(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip('False') + def test_foo(): + pass + """) + rec = testdir.inline_run() + rec.assertoutcome(skipped=1) + def test_skip_no_reason(self, testdir): testdir.makepyfile(""" import pytest @@ -430,14 +440,19 @@ class TestSkip(object): @pytest.mark.skip def test_foo(): pass - @pytest.mark.skip(reason="none") - def test_bar() : + @pytest.mark.skip(reason="no reason") + def test_bar(): pass def test_baz(): assert True """) - rec = testdir.inline_run() - rec.assertoutcome(passed=1, skipped=2) + # cant use assertoutcome because we cant + # also check the reason + result = testdir.runpytest('-rs') + result.stdout.fnmatch_lines([ + "*no reason*", + "*1 passed*2 skipped*", + ]) class TestSkipif(object): def test_skipif_conditional(self, testdir): From 04545f8a54c6a5e8e11932f4a045fb2f51a52013 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Thu, 1 Oct 2015 21:55:51 +0100 Subject: [PATCH 17/69] classes inherit from object --- _pytest/skipping.py | 2 +- testing/test_skipping.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 2733cac16..93faa4c9b 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -51,7 +51,7 @@ def xfail(reason=""): raise XFailed(reason) xfail.Exception = XFailed -class MarkEvaluator: +class MarkEvaluator(object): def __init__(self, item, name): self.item = item self.name = name diff --git a/testing/test_skipping.py b/testing/test_skipping.py index b0d3beff3..d7ceb5cfc 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -4,7 +4,7 @@ import sys from _pytest.skipping import MarkEvaluator, folded_skips, pytest_runtest_setup from _pytest.runner import runtestprotocol -class TestEvaluator: +class TestEvaluator(object): def test_no_marker(self, testdir): item = testdir.getitem("def test_func(): pass") evalskipif = MarkEvaluator(item, 'skipif') @@ -125,7 +125,7 @@ class TestEvaluator: assert expl == "condition: config._hackxyz" -class TestXFail: +class TestXFail(object): def test_xfail_simple(self, testdir): item = testdir.getitem(""" import pytest @@ -350,7 +350,7 @@ class TestXFail: ]) -class TestXFailwithSetupTeardown: +class TestXFailwithSetupTeardown(object): def test_failing_setup_issue9(self, testdir): testdir.makepyfile(""" import pytest From eee24138b037daa4dd662d1a1e2d7541cef25b92 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Thu, 1 Oct 2015 21:56:09 +0100 Subject: [PATCH 18/69] Fix failing test --- _pytest/skipping.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 93faa4c9b..53c7311e3 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -155,9 +155,8 @@ def pytest_runtest_setup(item): pytest.skip(eval_skipif.getexplanation()) elif isinstance(item.keywords.get('skip'), MarkInfo): eval_skip = MarkEvaluator(item, 'skip') - if eval_skip.istrue(): - item._evalskip = eval_skip - pytest.skip(eval_skip.getexplanation()) + item._evalskip = eval_skip + pytest.skip(eval_skip.getexplanation()) item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) From 1b5aa2868de5d7ab3ec99c0c4c11836dde85f9cf Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Thu, 1 Oct 2015 21:56:15 +0100 Subject: [PATCH 19/69] Check no reason displayed if none specified --- testing/test_skipping.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index d7ceb5cfc..c7a0516b4 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -416,8 +416,11 @@ class TestSkip(object): def test_foo(): pass """) - rec = testdir.inline_run() - rec.assertoutcome(skipped=1) + result = testdir.runpytest('-rs') + result.stdout.fnmatch_lines([ + "*Skipped instance*", + "*1 skipped*", + ]) def test_skip_with_reason(self, testdir): testdir.makepyfile(""" From 9e57954b0388d2830dee1e560347af581b75534a Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Thu, 1 Oct 2015 22:35:38 +0100 Subject: [PATCH 20/69] First argument in pytest.mark.skip is a reason --- _pytest/skipping.py | 25 +++++++++++++++++-------- testing/test_skipping.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 53c7311e3..491dd66a9 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -148,15 +148,24 @@ class MarkEvaluator(object): @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): - eval_skipif = MarkEvaluator(item, 'skipif') + # Check if skip or skipif are specified as pytest marks - if eval_skipif.istrue(): - item._evalskip = eval_skipif - pytest.skip(eval_skipif.getexplanation()) - elif isinstance(item.keywords.get('skip'), MarkInfo): - eval_skip = MarkEvaluator(item, 'skip') - item._evalskip = eval_skip - pytest.skip(eval_skip.getexplanation()) + skipif_info = item.keywords.get('skipif') + if isinstance(skipif_info, MarkInfo): + eval_skipif = MarkEvaluator(item, 'skipif') + if eval_skipif.istrue(): + item._evalskip = eval_skipif + pytest.skip(eval_skipif.getexplanation()) + + skip_info = item.keywords.get('skip') + if isinstance(skip_info, MarkInfo): + item._evalskip = True + if 'reason' in skip_info.kwargs: + pytest.skip(skip_info.kwargs['reason']) + elif skip_info.args: + pytest.skip(skip_info.args[0]) + else: + pytest.skip() item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index c7a0516b4..05e0683c5 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -409,6 +409,19 @@ class TestSkip(object): rec = testdir.inline_run() rec.assertoutcome(skipped=1) + def test_arg_as_reason(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skip('testing stuff') + def test_bar(): + pass + """) + result = testdir.runpytest('-rs') + result.stdout.fnmatch_lines([ + "*testing stuff*", + "*1 skipped*", + ]) + def test_skip_no_reason(self, testdir): testdir.makepyfile(""" import pytest From 213dbe7a5f28da38b62e7828d3c1d866fe996b4b Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Thu, 1 Oct 2015 22:36:43 +0100 Subject: [PATCH 21/69] newlines --- _pytest/skipping.py | 6 ++++++ testing/test_skipping.py | 1 + 2 files changed, 7 insertions(+) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 491dd66a9..c3b7f7b77 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -7,12 +7,14 @@ import py import pytest from _pytest.mark import MarkInfo + def pytest_addoption(parser): group = parser.getgroup("general") group.addoption('--runxfail', action="store_true", dest="runxfail", default=False, help="run tests even if they are marked xfail") + def pytest_configure(config): if config.option.runxfail: old = pytest.xfail @@ -39,18 +41,22 @@ def pytest_configure(config): "See http://pytest.org/latest/skipping.html" ) + def pytest_namespace(): return dict(xfail=xfail) + class XFailed(pytest.fail.Exception): """ raised from an explicit call to pytest.xfail() """ + def xfail(reason=""): """ xfail an executing test or setup functions with the given reason.""" __tracebackhide__ = True raise XFailed(reason) xfail.Exception = XFailed + class MarkEvaluator(object): def __init__(self, item, name): self.item = item diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 05e0683c5..f9cf9354f 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -4,6 +4,7 @@ import sys from _pytest.skipping import MarkEvaluator, folded_skips, pytest_runtest_setup from _pytest.runner import runtestprotocol + class TestEvaluator(object): def test_no_marker(self, testdir): item = testdir.getitem("def test_func(): pass") From 25d74a591966a4aa3852cbffab0865224a9ae0e0 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 16:55:04 +0100 Subject: [PATCH 22/69] Dont explicitly inherit from object --- _pytest/skipping.py | 2 +- testing/test_skipping.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index c3b7f7b77..5dbce75ed 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -57,7 +57,7 @@ def xfail(reason=""): xfail.Exception = XFailed -class MarkEvaluator(object): +class MarkEvaluator: def __init__(self, item, name): self.item = item self.name = name diff --git a/testing/test_skipping.py b/testing/test_skipping.py index f9cf9354f..c72fca3c0 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -5,7 +5,7 @@ from _pytest.skipping import MarkEvaluator, folded_skips, pytest_runtest_setup from _pytest.runner import runtestprotocol -class TestEvaluator(object): +class TestEvaluator: def test_no_marker(self, testdir): item = testdir.getitem("def test_func(): pass") evalskipif = MarkEvaluator(item, 'skipif') @@ -351,7 +351,7 @@ class TestXFail(object): ]) -class TestXFailwithSetupTeardown(object): +class TestXFailwithSetupTeardown: def test_failing_setup_issue9(self, testdir): testdir.makepyfile(""" import pytest @@ -383,7 +383,7 @@ class TestXFailwithSetupTeardown(object): ]) -class TestSkip(object): +class TestSkip: def test_skip_class(self, testdir): testdir.makepyfile(""" import pytest @@ -471,7 +471,7 @@ class TestSkip(object): "*1 passed*2 skipped*", ]) -class TestSkipif(object): +class TestSkipif: def test_skipif_conditional(self, testdir): item = testdir.getitem(""" import pytest From 5ff9a0ff54d0e1a88acd01fb918dec6fc7a48426 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 16:59:27 +0100 Subject: [PATCH 23/69] Remove redundant comments --- testing/test_skipping.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index c72fca3c0..edb458e91 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -443,8 +443,6 @@ class TestSkip: def test_bar(): pass """) - # cant use assertoutcome because we cant - # also check the reason result = testdir.runpytest('-rs') result.stdout.fnmatch_lines([ "*for lolz*", @@ -463,8 +461,6 @@ class TestSkip: def test_baz(): assert True """) - # cant use assertoutcome because we cant - # also check the reason result = testdir.runpytest('-rs') result.stdout.fnmatch_lines([ "*no reason*", From fc0bd9412cbcece8715449916f96fc1ddc51a71f Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 17:00:16 +0100 Subject: [PATCH 24/69] Test that "unconditional skip" is the default reason if none given --- testing/test_skipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index edb458e91..4945dbc77 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -432,7 +432,7 @@ class TestSkip: """) result = testdir.runpytest('-rs') result.stdout.fnmatch_lines([ - "*Skipped instance*", + "*unconditional skip*", "*1 skipped*", ]) From 122980ecadf7d5b3c72b5b37f40a206681fe243a Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 17:01:11 +0100 Subject: [PATCH 25/69] Add myself to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 1a2d2d572..8f348e0e2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -52,6 +52,7 @@ Marc Schlaich Mark Abramowitz Markus Unterwaditzer Martijn Faassen +Michael Aquilina Michael Droettboom Nicolas Delaby Pieter Mulder From 00d0c74657ca912f109d851961042e9e8ada9b2c Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 17:01:21 +0100 Subject: [PATCH 26/69] Update reason in test to prevent confusing with test_no_reason --- testing/test_skipping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 4945dbc77..3a6fd5c40 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -455,7 +455,7 @@ class TestSkip: @pytest.mark.skip def test_foo(): pass - @pytest.mark.skip(reason="no reason") + @pytest.mark.skip(reason="nothing in particular") def test_bar(): pass def test_baz(): @@ -463,7 +463,7 @@ class TestSkip: """) result = testdir.runpytest('-rs') result.stdout.fnmatch_lines([ - "*no reason*", + "*nothing in particular*", "*1 passed*2 skipped*", ]) From df874db817b50f42d6cdea088fa5f9d18c8a8e4a Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 17:02:18 +0100 Subject: [PATCH 27/69] Update default reason to "unconditional skip" --- _pytest/skipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 5dbce75ed..47f789efb 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -171,7 +171,7 @@ def pytest_runtest_setup(item): elif skip_info.args: pytest.skip(skip_info.args[0]) else: - pytest.skip() + pytest.skip("unconditional skip") item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) From 750442909c78a837dc291fc88889a06037af9c89 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 17:04:06 +0100 Subject: [PATCH 28/69] Add unconditional skip entry to CHANGELOG --- CHANGELOG | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4edf32298..a437530d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ -2.9.0.dev +2.9.0.dev --------- - + +* Add unconditional skip mechanism (`pytest.mark.skip`) + 2.8.1.dev 2.8.2.dev --------- @@ -29,9 +31,9 @@ "pytest-xdist" plugin, with test reports being assigned to the wrong tests. Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR. -- (experimental) adapt more SEMVER style versioning and change meaning of - master branch in git repo: "master" branch now keeps the bugfixes, changes - aimed for micro releases. "features" branch will only be be released +- (experimental) adapt more SEMVER style versioning and change meaning of + master branch in git repo: "master" branch now keeps the bugfixes, changes + aimed for micro releases. "features" branch will only be be released with minor or major pytest releases. - Fix issue #766 by removing documentation references to distutils. @@ -57,7 +59,7 @@ ----------------------------- - new ``--lf`` and ``-ff`` options to run only the last failing tests or - "failing tests first" from the last run. This functionality is provided + "failing tests first" from the last run. This functionality is provided through porting the formerly external pytest-cache plugin into pytest core. BACKWARD INCOMPAT: if you used pytest-cache's functionality to persist data between test runs be aware that we don't serialize sets anymore. @@ -163,9 +165,9 @@ - fix issue735: assertion failures on debug versions of Python 3.4+ -- new option ``--import-mode`` to allow to change test module importing - behaviour to append to sys.path instead of prepending. This better allows - to run test modules against installated versions of a package even if the +- new option ``--import-mode`` to allow to change test module importing + behaviour to append to sys.path instead of prepending. This better allows + to run test modules against installated versions of a package even if the package under test has the same import root. In this example:: testing/__init__.py @@ -173,7 +175,7 @@ pkg_under_test/ the tests will run against the installed version - of pkg_under_test when ``--import-mode=append`` is used whereas + of pkg_under_test when ``--import-mode=append`` is used whereas by default they would always pick up the local version. Thanks Holger Krekel. - pytester: add method ``TmpTestdir.delete_loaded_modules()``, and call it From 8984177448a3e0b3b493dd3112bf4d6991448df1 Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Sat, 3 Oct 2015 17:12:44 +0100 Subject: [PATCH 29/69] TestXFail also shouldnt explicitly inherit from object --- testing/test_skipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 3a6fd5c40..66bfd68c2 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -126,7 +126,7 @@ class TestEvaluator: assert expl == "condition: config._hackxyz" -class TestXFail(object): +class TestXFail: def test_xfail_simple(self, testdir): item = testdir.getitem(""" import pytest From a24126effb0caa091d820db06b5dc6d6f63ad3ef Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 3 Oct 2015 14:15:22 -0300 Subject: [PATCH 30/69] Add credit for pytest.mark.skip to the CHANGELOG --- CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a437530d0..e20532a9b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,9 @@ 2.9.0.dev --------- -* Add unconditional skip mechanism (`pytest.mark.skip`) +* New `pytest.mark.skip` mark, which unconditional skips marked tests. + Thanks Michael Aquilina for the complete PR. -2.8.1.dev 2.8.2.dev --------- From 616d8251f34bdd9a4fe7e0bac265412ab8cea961 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Thu, 24 Sep 2015 14:13:36 -0700 Subject: [PATCH 31/69] unit tests of Config.fromdictargs. currently failing --- testing/test_config.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/testing/test_config.py b/testing/test_config.py index d497200ee..601985e19 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -264,6 +264,51 @@ class TestConfigAPI: assert len(l) == 2 assert l == ["456", "123"] + +class TestConfigFromdictargs: + @pytest.mark.xfail(reason="fromdictargs currently broken #1060") + def test_basic_behavior(self): + from _pytest.config import Config + option_dict = { + 'verbose': 1e100, + 'foo': 'bar', + } + args = ['a', 'b'] + + config = Config.fromdictargs(option_dict, args) + with pytest.raises(AssertionError): + config.parse(['should to parse again']) + assert config.option.verbose == 1e100 + assert config.option.foo == 'bar' + assert config.args == args + + @pytest.mark.xfail(reason="fromdictargs currently broken #1060") + def test_origargs(self): + """Show that fromdictargs can handle args in their "orig" format""" + from _pytest.config import Config + option_dict = {} + args = ['-vvvv', 'a', 'b'] + + config = Config.fromdictargs(option_dict, args) + assert config.args == ['a', 'b'] + assert config._origargs == ['-vvvv', 'a', 'b'] + assert config.option.verbose == 4 + + @pytest.mark.xfail(reason="fromdictargs currently broken #1060") + def test_inifilename(self): + from _pytest.config import Config + inifile = '../../foo/bar.ini', + option_dict = { + 'inifilename': inifile, + } + + config = Config.fromdictargs(option_dict, ()) + assert config.option.inifilename == inifile + + # this indicates this is the file used for getting configuration values + assert config.inifile == inifile + + def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): reprec = testdir.inline_run(*opts) From 49d46a0059e5a167469a8c702564b41510ed1995 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Thu, 24 Sep 2015 16:10:01 -0700 Subject: [PATCH 32/69] an ugly patch to fix all but the most important part =/ --- _pytest/config.py | 6 +++--- testing/test_config.py | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 0495aa21f..525f0fbf2 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -879,7 +879,7 @@ class Config(object): def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ config = get_config() - config._preparse(args, addopts=False) + config.parse(args, addopts=False) config.option.__dict__.update(option_dict) for x in config.option.plugins: config.pluginmanager.consider_pluginarg(x) @@ -947,14 +947,14 @@ class Config(object): self.inicfg.config.path, self.inicfg.lineof('minversion'), minver, pytest.__version__)) - def parse(self, args): + def parse(self, args, addopts=True): # parse given cmdline arguments into this config object. assert not hasattr(self, 'args'), ( "can only parse cmdline args at most once per Config object") self._origargs = args self.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=self.pluginmanager)) - self._preparse(args) + self._preparse(args, addopts=addopts) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) args = self._parser.parse_setoption(args, self.option) diff --git a/testing/test_config.py b/testing/test_config.py index 601985e19..981138e75 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -266,7 +266,6 @@ class TestConfigAPI: class TestConfigFromdictargs: - @pytest.mark.xfail(reason="fromdictargs currently broken #1060") def test_basic_behavior(self): from _pytest.config import Config option_dict = { @@ -282,7 +281,6 @@ class TestConfigFromdictargs: assert config.option.foo == 'bar' assert config.args == args - @pytest.mark.xfail(reason="fromdictargs currently broken #1060") def test_origargs(self): """Show that fromdictargs can handle args in their "orig" format""" from _pytest.config import Config From 0e55a8793f182518aef3401b4893fe4caeea962a Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Thu, 24 Sep 2015 17:52:53 -0700 Subject: [PATCH 33/69] all tests pass --- _pytest/config.py | 30 ++++++++++++++++-------------- testing/test_config.py | 32 +++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 525f0fbf2..5ffed81dd 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -455,11 +455,11 @@ class Parser: """ self._anonymous.addoption(*opts, **attrs) - def parse(self, args): + def parse(self, args, namespace=None): from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() try_argcomplete(self.optparser) - return self.optparser.parse_args([str(x) for x in args]) + return self.optparser.parse_args([str(x) for x in args], namespace=namespace) def _getparser(self): from _pytest._argcomplete import filescompleter @@ -477,25 +477,25 @@ class Parser: optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter return optparser - def parse_setoption(self, args, option): - parsedoption = self.parse(args) + def parse_setoption(self, args, option, namespace=None): + parsedoption = self.parse(args, namespace=namespace) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) return getattr(parsedoption, FILE_OR_DIR) - def parse_known_args(self, args): + def parse_known_args(self, args, namespace=None): """parses and returns a namespace object with known arguments at this point. """ - return self.parse_known_and_unknown_args(args)[0] + return self.parse_known_and_unknown_args(args, namespace=namespace)[0] - def parse_known_and_unknown_args(self, args): + def parse_known_and_unknown_args(self, args, namespace=None): """parses and returns a namespace object with known arguments, and the remaining arguments unknown at this point. """ optparser = self._getparser() args = [str(x) for x in args] - return optparser.parse_known_args(args) + return optparser.parse_known_args(args, namespace=namespace) def addini(self, name, help, type=None, default=None): """ register an ini-file option. @@ -779,10 +779,12 @@ def _ensure_removed_sysmodule(modname): class CmdOptions(object): """ holds cmdline options as attributes.""" - def __init__(self, **kwargs): - self.__dict__.update(kwargs) + def __init__(self, values=()): + self.__dict__.update(values) def __repr__(self): return "" %(self.__dict__,) + def copy(self): + return CmdOptions(self.__dict__) class Notset: def __repr__(self): @@ -879,8 +881,8 @@ class Config(object): def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ config = get_config() - config.parse(args, addopts=False) config.option.__dict__.update(option_dict) + config.parse(args, addopts=False) for x in config.option.plugins: config.pluginmanager.consider_pluginarg(x) return config @@ -898,7 +900,7 @@ class Config(object): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) def _initini(self, args): - ns, unknown_args = self._parser.parse_known_and_unknown_args(args) + ns, unknown_args = self._parser.parse_known_and_unknown_args(args, namespace=self.option.copy()) r = determine_setup(ns.inifilename, ns.file_or_dir + unknown_args) self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info['rootdir'] = self.rootdir @@ -919,7 +921,7 @@ class Config(object): 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) + 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: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -957,7 +959,7 @@ class Config(object): self._preparse(args, addopts=addopts) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) - args = self._parser.parse_setoption(args, self.option) + args = self._parser.parse_setoption(args, self.option, namespace=self.option) if not args: cwd = os.getcwd() if cwd == self.rootdir: diff --git a/testing/test_config.py b/testing/test_config.py index 981138e75..f8492097c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -269,42 +269,56 @@ class TestConfigFromdictargs: def test_basic_behavior(self): from _pytest.config import Config option_dict = { - 'verbose': 1e100, + 'verbose': 444, 'foo': 'bar', + 'capture': 'no', } args = ['a', 'b'] config = Config.fromdictargs(option_dict, args) with pytest.raises(AssertionError): - config.parse(['should to parse again']) - assert config.option.verbose == 1e100 + config.parse(['should refuse to parse again']) + assert config.option.verbose == 444 assert config.option.foo == 'bar' + assert config.option.capture == 'no' assert config.args == args def test_origargs(self): """Show that fromdictargs can handle args in their "orig" format""" from _pytest.config import Config option_dict = {} - args = ['-vvvv', 'a', 'b'] + args = ['-vvvv', '-s', 'a', 'b'] config = Config.fromdictargs(option_dict, args) assert config.args == ['a', 'b'] - assert config._origargs == ['-vvvv', 'a', 'b'] + assert config._origargs == args assert config.option.verbose == 4 + assert config.option.capture == 'no' + + def test_inifilename(self, tmpdir): + tmpdir.join("foo/bar.ini").ensure().write(py.code.Source(""" + [pytest] + name = value + """)) - @pytest.mark.xfail(reason="fromdictargs currently broken #1060") - def test_inifilename(self): from _pytest.config import Config - inifile = '../../foo/bar.ini', + inifile = '../../foo/bar.ini' option_dict = { 'inifilename': inifile, + 'capture': 'no', } - config = Config.fromdictargs(option_dict, ()) + cwd = tmpdir.join('a/b') + with cwd.ensure(dir=True).as_cwd(): + config = Config.fromdictargs(option_dict, ()) + + assert config.args == [str(cwd)] assert config.option.inifilename == inifile + assert config.option.capture == 'no' # this indicates this is the file used for getting configuration values assert config.inifile == inifile + assert config.inicfg.get('name') == 'value' def test_options_on_small_file_do_not_blow_up(testdir): From 470e4f9e910837c5fc99b038b7ffcf653cffa061 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Thu, 8 Oct 2015 10:51:22 -0700 Subject: [PATCH 34/69] changelog entry --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e20532a9b..530ecf0ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,9 @@ * New `pytest.mark.skip` mark, which unconditional skips marked tests. Thanks Michael Aquilina for the complete PR. +* fix issue #680: the -s and -c options should now work under xdist; + `Config.fromdictargs` now represents its input much more faithfully. + Thanks to Buck Evan for the complete PR. 2.8.2.dev --------- From 67236d6de38571f2835c04f00a583aa10658b509 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Fri, 9 Oct 2015 09:57:40 -0700 Subject: [PATCH 35/69] strengthen the ini assertion --- testing/test_config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testing/test_config.py b/testing/test_config.py index f8492097c..e818dff38 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -309,6 +309,11 @@ class TestConfigFromdictargs: } cwd = tmpdir.join('a/b') + cwd.join('pytest.ini').ensure().write(py.code.Source(""" + [pytest] + name = wrong-value + should_not_be_set = true + """)) with cwd.ensure(dir=True).as_cwd(): config = Config.fromdictargs(option_dict, ()) @@ -319,6 +324,7 @@ class TestConfigFromdictargs: # this indicates this is the file used for getting configuration values assert config.inifile == inifile assert config.inicfg.get('name') == 'value' + assert config.inicfg.get('should_not_be_set') is None def test_options_on_small_file_do_not_blow_up(testdir): From 1f148a93ecb7c8aa4d33335f9251d083551a33bb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 23 Nov 2015 12:59:56 -0200 Subject: [PATCH 36/69] Mention pytest_enter_pdb in the docs --- _pytest/hookspec.py | 4 +++- doc/en/writing_plugins.rst | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index d0c3e4aad..113915d2d 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -291,4 +291,6 @@ def pytest_exception_interact(node, call, report): """ def pytest_enter_pdb(): - """ called upon pdb.set_trace()""" + """ called upon pdb.set_trace(), can be used by plugins to take special + action just before the python debugger enters in interactive mode. + """ diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 1e9807cf5..f5e4ce66c 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -501,7 +501,7 @@ reporting or interaction with exceptions: .. autofunction:: pytest_internalerror .. autofunction:: pytest_keyboard_interrupt .. autofunction:: pytest_exception_interact - +.. autofunction:: pytest_enter_pdb Reference of objects involved in hooks From b3166a538c3b35c6dbde9b52f9d48278fc7dcfed Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 23 Nov 2015 13:05:56 -0200 Subject: [PATCH 37/69] Pass pytest's config object to pytest_enter_pdb --- CHANGELOG | 3 +++ _pytest/hookspec.py | 5 ++++- _pytest/pdb.py | 3 +-- testing/test_pdb.py | 6 +++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e20532a9b..91c006553 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,9 @@ * New `pytest.mark.skip` mark, which unconditional skips marked tests. Thanks Michael Aquilina for the complete PR. +* `pytest_enter_pdb` now optionally receives the pytest config object. + Thanks Bruno Oliveira for the PR. + 2.8.2.dev --------- diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 113915d2d..a9024b1d3 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -290,7 +290,10 @@ def pytest_exception_interact(node, call, report): that is not an internal exception like "skip.Exception". """ -def pytest_enter_pdb(): +def pytest_enter_pdb(config): """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. + + :arg config: pytest config object + :type config: _pytest.config.Config """ diff --git a/_pytest/pdb.py b/_pytest/pdb.py index 78fedd373..8a615d0d3 100644 --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -37,7 +37,6 @@ class pytestPDB: """ invoke PDB set_trace debugging, dropping any IO capturing. """ import _pytest.config frame = sys._getframe().f_back - capman = None if self._pluginmanager is not None: capman = self._pluginmanager.getplugin("capturemanager") if capman: @@ -45,7 +44,7 @@ class pytestPDB: tw = _pytest.config.create_terminal_writer(self._config) tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") - self._pluginmanager.hook.pytest_enter_pdb() + self._pluginmanager.hook.pytest_enter_pdb(config=self._config) pdb.Pdb().set_trace(frame) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index a2fd4d43d..06fc056e3 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -275,8 +275,12 @@ class TestPDB: def test_enter_pdb_hook_is_called(self, testdir): testdir.makeconftest(""" - def pytest_enter_pdb(): + def pytest_enter_pdb(config): + assert config.testing_verification == 'configured' print 'enter_pdb_hook' + + def pytest_configure(config): + config.testing_verification = 'configured' """) p1 = testdir.makepyfile(""" import pytest From 84eacf3e3c73d11afcb3a194c20dc9a3d46357c4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 26 Nov 2015 14:37:55 -0200 Subject: [PATCH 38/69] Small changelog formatting fix --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 089350d8a..37a23dd87 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ * New `pytest.mark.skip` mark, which unconditional skips marked tests. Thanks Michael Aquilina for the complete PR. + * fix issue #680: the -s and -c options should now work under xdist; `Config.fromdictargs` now represents its input much more faithfully. Thanks to Buck Evan for the complete PR. From fbac9365966ce5b496637859d3a151c39bce12d6 Mon Sep 17 00:00:00 2001 From: David Vierra Date: Fri, 18 Sep 2015 20:36:43 -1000 Subject: [PATCH 39/69] Add -rp and -rP options to report passing tests. -rP is an alternative to `-s` for viewing the output of passing tests. This causes the captured stdout/stderr of passing tests to be output in the same way as that of failing tests. -rp adds a simple one-line-per-test summary for passing tests. Neither option is included by -ra. Additional changes to `pytest_capturelog` and `pytest_catchlog` are needed for this option to also output captured logs: They must be changed to use `rep.sections.add` instead of `rep.longrepr.addsection`, and to add these additional sections even if the test passes, since passing tests don't seem to have a `longrepr` at report time. --- _pytest/runner.py | 6 +++++- _pytest/skipping.py | 3 +++ _pytest/terminal.py | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/_pytest/runner.py b/_pytest/runner.py index 6e4f45d5e..22ad874d2 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -177,9 +177,13 @@ class BaseReport(object): self.__dict__.update(kw) def toterminal(self, out): - longrepr = self.longrepr if hasattr(self, 'node'): out.line(getslaveinfoline(self.node)) + + longrepr = self.longrepr + if longrepr is None: + return + if hasattr(longrepr, 'toterminal'): longrepr.toterminal(out) else: diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 47f789efb..9bd38d684 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -252,6 +252,9 @@ def pytest_terminal_summary(terminalreporter): show_skipped(terminalreporter, lines) elif char == "E": show_simple(terminalreporter, lines, 'error', "ERROR %s") + elif char == 'p': + show_simple(terminalreporter, lines, 'passed', "PASSED %s") + if lines: tr._tw.sep("=", "short test summary info") for line in lines: diff --git a/_pytest/terminal.py b/_pytest/terminal.py index ce4a88bc0..82abdbdcc 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -22,7 +22,8 @@ def pytest_addoption(parser): group._addoption('-r', action="store", dest="reportchars", default=None, 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 (a)all.") + "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings " + "(p)passed, (P)passed with output, (a)all except pP.") group._addoption('-l', '--showlocals', action="store_true", dest="showlocals", default=False, help="show locals in tracebacks (disabled by default).") @@ -367,10 +368,12 @@ class TerminalReporter: self.summary_errors() self.summary_failures() self.summary_warnings() + self.summary_passes() self.config.hook.pytest_terminal_summary(terminalreporter=self) if exitstatus == EXIT_INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo + self.summary_deselected() self.summary_stats() @@ -446,6 +449,18 @@ class TerminalReporter: self._tw.line("W%s %s %s" % (w.code, w.fslocation, w.message)) + def summary_passes(self): + if self.config.option.tbstyle != "no": + if self.hasopt("P"): + reports = self.getreports('passed') + if not reports: + return + self.write_sep("=", "PASSES") + for rep in reports: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg) + self._outrep_summary(rep) + def summary_failures(self): if self.config.option.tbstyle != "no": reports = self.getreports('failed') From 1db4cbcc9f27f10c9ddd6f91534b3cd3f27cb4ba Mon Sep 17 00:00:00 2001 From: David Vierra Date: Mon, 7 Dec 2015 12:09:03 -1000 Subject: [PATCH 40/69] Update AUTHORS and CHANGELOG --- AUTHORS | 1 + CHANGELOG | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS b/AUTHORS index 8f348e0e2..10b522cdf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,6 +26,7 @@ Daniel Grana Daniel Nuri Dave Hunt David Mohr +David Vierra Edison Gustavo Muenz Eduardo Schettino Endre Galaczi diff --git a/CHANGELOG b/CHANGELOG index 37a23dd87..dec050d5b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,9 @@ * `pytest_enter_pdb` now optionally receives the pytest config object. Thanks Bruno Oliveira for the PR. +* New `-rp` and `-rP` reporting options give the summary and full output + of passing tests, respectively. Thanks to David Vierra for the PR. + 2.8.2.dev --------- From 5ccb7b1ced6a5593b2af325fab07b43f91d1ec75 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Tue, 8 Dec 2015 11:08:33 -0800 Subject: [PATCH 41/69] update test_recwarn to new style --- testing/test_recwarn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index d8b8bbc32..14995243b 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -112,12 +112,12 @@ class TestDeprecatedCall(object): def test_deprecated_call_as_context_manager_no_warning(self): with pytest.raises(pytest.fail.Exception) as ex: with pytest.deprecated_call(): - dep(1) + self.dep(1) assert str(ex.value) == "DID NOT WARN" def test_deprecated_call_as_context_manager(self): with pytest.deprecated_call(): - dep(0) + self.dep(0) def test_deprecated_call_pending(self): def f(): From b417d7cb7992f668f67ed0cb5dd5c9aea74fe1e3 Mon Sep 17 00:00:00 2001 From: David Vierra Date: Tue, 8 Dec 2015 15:54:23 -1000 Subject: [PATCH 42/69] Add tests to test_terminal.py for -rp and -rP --- testing/test_terminal.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cf0554077..cc27a4e51 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -522,6 +522,31 @@ def test_fail_reporting_on_pass(testdir): result = testdir.runpytest('-rf') assert 'short test summary' not in result.stdout.str() +def test_pass_extra_reporting(testdir): + testdir.makepyfile("def test_this(): assert 1") + result = testdir.runpytest() + assert 'short test summary' not in result.stdout.str() + result = testdir.runpytest('-rp') + result.stdout.fnmatch_lines([ + "*test summary*", + "PASS*test_pass_extra_reporting*", + ]) + +def test_pass_reporting_on_fail(testdir): + testdir.makepyfile("def test_this(): assert 0") + result = testdir.runpytest('-rp') + assert 'short test summary' not in result.stdout.str() + +def test_pass_output_reporting(testdir): + testdir.makepyfile(""" + def test_pass_output(): + print("Four score and seven years ago...") + """) + result = testdir.runpytest('-rP') + result.stdout.fnmatch_lines([ + "Four score and seven years ago...", + ]) + def test_color_yes(testdir): testdir.makepyfile("def test_this(): assert 1") result = testdir.runpytest('--color=yes') From ccfd962170a0767ade7cb3b3f329281cdcc998ab Mon Sep 17 00:00:00 2001 From: David Vierra Date: Tue, 8 Dec 2015 17:33:03 -1000 Subject: [PATCH 43/69] Add "no -rP" case to test_pass_output_reporting --- testing/test_terminal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cc27a4e51..458750104 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -542,6 +542,8 @@ def test_pass_output_reporting(testdir): def test_pass_output(): print("Four score and seven years ago...") """) + result = testdir.runpytest() + assert 'Four score and seven years ago...' not in result.stdout.str() result = testdir.runpytest('-rP') result.stdout.fnmatch_lines([ "Four score and seven years ago...", From 8f880e1625d6ded14747ad4bf86ec8e23fe3716e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Dec 2015 19:40:45 -0200 Subject: [PATCH 44/69] Fix CHANGELOG merge --- CHANGELOG | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1da8638b3..fb08823f1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,8 +14,7 @@ * New `-rp` and `-rP` reporting options give the summary and full output of passing tests, respectively. Thanks to David Vierra for the PR. -2.8.2.dev ---------- + 2.8.5.dev0 ---------- @@ -75,8 +74,6 @@ system integrity protection (thanks Florian) - - 2.8.2 ----- From af893aab263627ce98ae842851853bc77a85debb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2015 15:55:05 -0200 Subject: [PATCH 45/69] Remove code related to support python <= 2.5 Fix #1226 --- _pytest/_argcomplete.py | 3 --- testing/acceptance_test.py | 3 --- testing/test_argcomplete.py | 4 ---- testing/test_assertinterpret.py | 19 ------------------- testing/test_assertion.py | 9 +-------- testing/test_cache.py | 3 --- testing/test_mark.py | 2 -- testing/test_nose.py | 1 - testing/test_parseopt.py | 6 ------ 9 files changed, 1 insertion(+), 49 deletions(-) diff --git a/_pytest/_argcomplete.py b/_pytest/_argcomplete.py index 4f4eaf925..955855a96 100644 --- a/_pytest/_argcomplete.py +++ b/_pytest/_argcomplete.py @@ -88,9 +88,6 @@ class FastFilesCompleter: return completion if os.environ.get('_ARGCOMPLETE'): - # argcomplete 0.5.6 is not compatible with python 2.5.6: print/with/format - if sys.version_info[:2] < (2, 6): - sys.exit(1) try: import argcomplete.completers except ImportError: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b9a3fa381..4cd731e72 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -450,19 +450,16 @@ class TestInvocationVariants: "*1 passed*", ]) - @pytest.mark.skipif("sys.version_info < (2,5)") def test_python_minus_m_invocation_ok(self, testdir): p1 = testdir.makepyfile("def test_hello(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) assert res.ret == 0 - @pytest.mark.skipif("sys.version_info < (2,5)") def test_python_minus_m_invocation_fail(self, testdir): p1 = testdir.makepyfile("def test_fail(): 0/0") res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) assert res.ret == 1 - @pytest.mark.skipif("sys.version_info < (2,5)") def test_python_pytest_package(self, testdir): p1 = testdir.makepyfile("def test_pass(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 9a218a401..ace7d8ceb 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -69,11 +69,8 @@ class FilesCompleter(object): completion += [f + '/' for f in anticomp] return completion -# the following barfs with a syntax error on py2.5 -# @pytest.mark.skipif("sys.version_info < (2,6)") class TestArgComplete: @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") - @pytest.mark.skipif("sys.version_info < (2,6)") def test_compare_with_compgen(self): from _pytest._argcomplete import FastFilesCompleter ffc = FastFilesCompleter() @@ -82,7 +79,6 @@ class TestArgComplete: assert equal_with_bash(x, ffc, fc, out=py.std.sys.stdout) @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") - @pytest.mark.skipif("sys.version_info < (2,6)") def test_remove_dir_prefix(self): """this is not compatible with compgen but it is with bash itself: ls /usr/ diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py index 209bdfd49..ad9bb5c79 100644 --- a/testing/test_assertinterpret.py +++ b/testing/test_assertinterpret.py @@ -79,7 +79,6 @@ def test_is(): assert s.startswith("assert 1 is 2") -@pytest.mark.skipif("sys.version_info < (2,6)") def test_attrib(): class Foo(object): b = 1 @@ -91,7 +90,6 @@ def test_attrib(): s = str(e) assert s.startswith("assert 1 == 2") -@pytest.mark.skipif("sys.version_info < (2,6)") def test_attrib_inst(): class Foo(object): b = 1 @@ -256,7 +254,6 @@ class TestView: assert codelines == ["4 + 5", "getitem('', 'join')", "setattr('x', 'y', 3)", "12 - 1"] -@pytest.mark.skipif("sys.version_info < (2,6)") def test_assert_customizable_reprcompare(monkeypatch): monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') try: @@ -306,7 +303,6 @@ def test_assert_raise_alias(testdir): ]) -@pytest.mark.skipif("sys.version_info < (2,5)") def test_assert_raise_subclass(): class SomeEx(AssertionError): def __init__(self, *args): @@ -334,18 +330,3 @@ def test_assert_raises_in_nonzero_of_object_pytest_issue10(): e = exvalue() s = str(e) assert " < 0" in s - -@pytest.mark.skipif("sys.version_info >= (2,6)") -def test_oldinterpret_importation(): - # we had a cyclic import there - # requires pytest on sys.path - res = py.std.subprocess.call([ - py.std.sys.executable, '-c', str(py.code.Source(""" - try: - from _pytest.assertion.newinterpret import interpret - except ImportError: - from _pytest.assertion.oldinterpret import interpret - """)) - ]) - - assert res == 0 diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 914feddf7..cf715470a 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -7,7 +7,6 @@ import _pytest.assertion as plugin from _pytest.assertion import reinterpret from _pytest.assertion import util -needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") PY3 = sys.version_info >= (3, 0) @@ -26,7 +25,6 @@ def interpret(expr): return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1))) class TestBinReprIntegration: - pytestmark = needsnewassert def test_pytest_assertrepr_compare_called(self, testdir): testdir.makeconftest(""" @@ -361,7 +359,6 @@ def test_python25_compile_issue257(testdir): *1 failed* """) -@needsnewassert def test_rewritten(testdir): testdir.makepyfile(""" def test_rewritten(): @@ -374,7 +371,6 @@ def test_reprcompare_notin(mock_config): mock_config, 'not in', 'foo', 'aaafoobbb')[1:] assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++'] -@needsnewassert def test_pytest_assertrepr_compare_integration(testdir): testdir.makepyfile(""" def test_hello(): @@ -391,7 +387,6 @@ def test_pytest_assertrepr_compare_integration(testdir): "*E*50*", ]) -@needsnewassert def test_sequence_comparison_uses_repr(testdir): testdir.makepyfile(""" def test_hello(): @@ -410,7 +405,6 @@ def test_sequence_comparison_uses_repr(testdir): ]) -@pytest.mark.xfail("sys.version_info < (2,6)") def test_assert_compare_truncate_longmessage(testdir): testdir.makepyfile(r""" def test_long(): @@ -438,7 +432,6 @@ def test_assert_compare_truncate_longmessage(testdir): ]) -@needsnewassert def test_assertrepr_loaded_per_dir(testdir): testdir.makepyfile(test_base=['def test_base(): assert 1 == 2']) a = testdir.mkdir('a') @@ -547,7 +540,7 @@ def test_traceback_failure(testdir): "*test_traceback_failure.py:4: AssertionError" ]) -@pytest.mark.skipif("sys.version_info < (2,5) or '__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" ) +@pytest.mark.skipif("'__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" ) def test_warn_missing(testdir): testdir.makepyfile("") result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h") diff --git a/testing/test_cache.py b/testing/test_cache.py index 20a6cf78a..adac4a1a6 100755 --- a/testing/test_cache.py +++ b/testing/test_cache.py @@ -129,7 +129,6 @@ def test_cache_show(testdir): class TestLastFailed: - @pytest.mark.skipif("sys.version_info < (2,6)") def test_lastfailed_usecase(self, testdir, monkeypatch): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) p = testdir.makepyfile(""" @@ -197,7 +196,6 @@ class TestLastFailed: "test_a.py*", ]) - @pytest.mark.skipif("sys.version_info < (2,6)") def test_lastfailed_difference_invocations(self, testdir, monkeypatch): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) testdir.makepyfile(test_a=""" @@ -233,7 +231,6 @@ class TestLastFailed: "*1 failed*1 desel*", ]) - @pytest.mark.skipif("sys.version_info < (2,6)") def test_lastfailed_usecase_splice(self, testdir, monkeypatch): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) testdir.makepyfile(""" diff --git a/testing/test_mark.py b/testing/test_mark.py index 1aa336183..1795928f0 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -316,7 +316,6 @@ class TestFunctional: assert 'hello' in keywords assert 'world' in keywords - @pytest.mark.skipif("sys.version_info < (2,6)") def test_mark_per_class_decorator(self, testdir): item = testdir.getitem(""" import pytest @@ -328,7 +327,6 @@ class TestFunctional: keywords = item.keywords assert 'hello' in keywords - @pytest.mark.skipif("sys.version_info < (2,6)") def test_mark_per_class_decorator_plus_existing_dec(self, testdir): item = testdir.getitem(""" import pytest diff --git a/testing/test_nose.py b/testing/test_nose.py index 6260aae47..a5162381e 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -300,7 +300,6 @@ def test_apiwrapper_problem_issue260(testdir): result = testdir.runpytest() result.assert_outcomes(passed=1) -@pytest.mark.skipif("sys.version_info < (2,6)") def test_setup_teardown_linking_issue265(testdir): # we accidentally didnt integrate nose setupstate with normal setupstate # this test ensures that won't happen again diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 15b28225c..e45ee2854 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -171,7 +171,6 @@ class TestParser: assert option.this == 42 assert option.no is False - @pytest.mark.skipif("sys.version_info < (2,5)") def test_drop_short_helper(self): parser = py.std.argparse.ArgumentParser(formatter_class=parseopt.DropShorterLongHelpFormatter) parser.add_argument('-t', '--twoword', '--duo', '--two-word', '--two', @@ -213,20 +212,17 @@ class TestParser: assert args.abc_def is False assert args.klm_hij is True - @pytest.mark.skipif("sys.version_info < (2,5)") def test_drop_short_2(self, parser): parser.addoption('--func-arg', '--doit', action='store_true') args = parser.parse(['--doit']) assert args.func_arg is True - @pytest.mark.skipif("sys.version_info < (2,5)") def test_drop_short_3(self, parser): parser.addoption('--func-arg', '--funcarg', '--doit', action='store_true') args = parser.parse(['abcd']) assert args.func_arg is False assert args.file_or_dir == ['abcd'] - @pytest.mark.skipif("sys.version_info < (2,5)") def test_drop_short_help0(self, parser, capsys): parser.addoption('--func-args', '--doit', help = 'foo', action='store_true') @@ -235,7 +231,6 @@ class TestParser: assert '--func-args, --doit foo' in help # testing would be more helpful with all help generated - @pytest.mark.skipif("sys.version_info < (2,5)") def test_drop_short_help1(self, parser, capsys): group = parser.getgroup("general") group.addoption('--doit', '--func-args', action='store_true', help='foo') @@ -246,7 +241,6 @@ class TestParser: assert '-doit, --func-args foo' in help -@pytest.mark.skipif("sys.version_info < (2,6)") def test_argcomplete(testdir, monkeypatch): if not py.path.local.sysfind('bash'): pytest.skip("bash not available") From 81588d7f63aa8350ccb9f1db16b2a0a8da4a9a28 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2015 16:05:08 -0200 Subject: [PATCH 46/69] Remove obsolete "oldinterpret" module --- _pytest/assertion/newinterpret.py | 1 - _pytest/assertion/oldinterpret.py | 566 ------------------------------ _pytest/assertion/reinterpret.py | 5 +- testing/test_assertinterpret.py | 59 ---- 4 files changed, 1 insertion(+), 630 deletions(-) delete mode 100644 _pytest/assertion/oldinterpret.py diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py index d8e741162..fbca118d1 100644 --- a/_pytest/assertion/newinterpret.py +++ b/_pytest/assertion/newinterpret.py @@ -1,6 +1,5 @@ """ Find intermediate evalutation results in assert statements through builtin AST. -This should replace oldinterpret.py eventually. """ import sys diff --git a/_pytest/assertion/oldinterpret.py b/_pytest/assertion/oldinterpret.py deleted file mode 100644 index 8ca1d8f5c..000000000 --- a/_pytest/assertion/oldinterpret.py +++ /dev/null @@ -1,566 +0,0 @@ -import traceback -import types -import py -import sys, inspect -from compiler import parse, ast, pycodegen -from _pytest.assertion.util import format_explanation, BuiltinAssertionError - -passthroughex = py.builtin._sysex - -class Failure: - def __init__(self, node): - self.exc, self.value, self.tb = sys.exc_info() - self.node = node - -class View(object): - """View base class. - - If C is a subclass of View, then C(x) creates a proxy object around - the object x. The actual class of the proxy is not C in general, - but a *subclass* of C determined by the rules below. To avoid confusion - we call view class the class of the proxy (a subclass of C, so of View) - and object class the class of x. - - Attributes and methods not found in the proxy are automatically read on x. - Other operations like setting attributes are performed on the proxy, as - determined by its view class. The object x is available from the proxy - as its __obj__ attribute. - - The view class selection is determined by the __view__ tuples and the - optional __viewkey__ method. By default, the selected view class is the - most specific subclass of C whose __view__ mentions the class of x. - If no such subclass is found, the search proceeds with the parent - object classes. For example, C(True) will first look for a subclass - of C with __view__ = (..., bool, ...) and only if it doesn't find any - look for one with __view__ = (..., int, ...), and then ..., object,... - If everything fails the class C itself is considered to be the default. - - Alternatively, the view class selection can be driven by another aspect - of the object x, instead of the class of x, by overriding __viewkey__. - See last example at the end of this module. - """ - - _viewcache = {} - __view__ = () - - def __new__(rootclass, obj, *args, **kwds): - self = object.__new__(rootclass) - self.__obj__ = obj - self.__rootclass__ = rootclass - key = self.__viewkey__() - try: - self.__class__ = self._viewcache[key] - except KeyError: - self.__class__ = self._selectsubclass(key) - return self - - def __getattr__(self, attr): - # attributes not found in the normal hierarchy rooted on View - # are looked up in the object's real class - return getattr(object.__getattribute__(self, '__obj__'), attr) - - def __viewkey__(self): - return self.__obj__.__class__ - - def __matchkey__(self, key, subclasses): - if inspect.isclass(key): - keys = inspect.getmro(key) - else: - keys = [key] - for key in keys: - result = [C for C in subclasses if key in C.__view__] - if result: - return result - return [] - - def _selectsubclass(self, key): - subclasses = list(enumsubclasses(self.__rootclass__)) - for C in subclasses: - if not isinstance(C.__view__, tuple): - C.__view__ = (C.__view__,) - choices = self.__matchkey__(key, subclasses) - if not choices: - return self.__rootclass__ - elif len(choices) == 1: - return choices[0] - else: - # combine the multiple choices - return type('?', tuple(choices), {}) - - def __repr__(self): - return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) - - -def enumsubclasses(cls): - for subcls in cls.__subclasses__(): - for subsubclass in enumsubclasses(subcls): - yield subsubclass - yield cls - - -class Interpretable(View): - """A parse tree node with a few extra methods.""" - explanation = None - - def is_builtin(self, frame): - return False - - def eval(self, frame): - # fall-back for unknown expression nodes - try: - expr = ast.Expression(self.__obj__) - expr.filename = '' - self.__obj__.filename = '' - co = pycodegen.ExpressionCodeGenerator(expr).getCode() - result = frame.eval(co) - except passthroughex: - raise - except: - raise Failure(self) - self.result = result - self.explanation = self.explanation or frame.repr(self.result) - - def run(self, frame): - # fall-back for unknown statement nodes - try: - expr = ast.Module(None, ast.Stmt([self.__obj__])) - expr.filename = '' - co = pycodegen.ModuleCodeGenerator(expr).getCode() - frame.exec_(co) - except passthroughex: - raise - except: - raise Failure(self) - - def nice_explanation(self): - return format_explanation(self.explanation) - - -class Name(Interpretable): - __view__ = ast.Name - - def is_local(self, frame): - source = '%r in locals() is not globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_global(self, frame): - source = '%r in globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_builtin(self, frame): - source = '%r not in locals() and %r not in globals()' % ( - self.name, self.name) - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - super(Name, self).eval(frame) - if not self.is_local(frame): - self.explanation = self.name - -class Compare(Interpretable): - __view__ = ast.Compare - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - for operation, expr2 in self.ops: - if hasattr(self, 'result'): - # shortcutting in chained expressions - if not frame.is_true(self.result): - break - expr2 = Interpretable(expr2) - expr2.eval(frame) - self.explanation = "%s %s %s" % ( - expr.explanation, operation, expr2.explanation) - source = "__exprinfo_left %s __exprinfo_right" % operation - try: - self.result = frame.eval(source, - __exprinfo_left=expr.result, - __exprinfo_right=expr2.result) - except passthroughex: - raise - except: - raise Failure(self) - expr = expr2 - -class And(Interpretable): - __view__ = ast.And - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if not frame.is_true(expr.result): - break - self.explanation = '(' + ' and '.join(explanations) + ')' - -class Or(Interpretable): - __view__ = ast.Or - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if frame.is_true(expr.result): - break - self.explanation = '(' + ' or '.join(explanations) + ')' - - -# == Unary operations == -keepalive = [] -for astclass, astpattern in { - ast.Not : 'not __exprinfo_expr', - ast.Invert : '(~__exprinfo_expr)', - }.items(): - - class UnaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - expr = Interpretable(self.expr) - expr.eval(frame) - self.explanation = astpattern.replace('__exprinfo_expr', - expr.explanation) - try: - self.result = frame.eval(astpattern, - __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(UnaryArith) - -# == Binary operations == -for astclass, astpattern in { - ast.Add : '(__exprinfo_left + __exprinfo_right)', - ast.Sub : '(__exprinfo_left - __exprinfo_right)', - ast.Mul : '(__exprinfo_left * __exprinfo_right)', - ast.Div : '(__exprinfo_left / __exprinfo_right)', - ast.Mod : '(__exprinfo_left % __exprinfo_right)', - ast.Power : '(__exprinfo_left ** __exprinfo_right)', - }.items(): - - class BinaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - left = Interpretable(self.left) - left.eval(frame) - right = Interpretable(self.right) - right.eval(frame) - self.explanation = (astpattern - .replace('__exprinfo_left', left .explanation) - .replace('__exprinfo_right', right.explanation)) - try: - self.result = frame.eval(astpattern, - __exprinfo_left=left.result, - __exprinfo_right=right.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(BinaryArith) - - -class CallFunc(Interpretable): - __view__ = ast.CallFunc - - def is_bool(self, frame): - source = 'isinstance(__exprinfo_value, bool)' - try: - return frame.is_true(frame.eval(source, - __exprinfo_value=self.result)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - node = Interpretable(self.node) - node.eval(frame) - explanations = [] - vars = {'__exprinfo_fn': node.result} - source = '__exprinfo_fn(' - for a in self.args: - if isinstance(a, ast.Keyword): - keyword = a.name - a = a.expr - else: - keyword = None - a = Interpretable(a) - a.eval(frame) - argname = '__exprinfo_%d' % len(vars) - vars[argname] = a.result - if keyword is None: - source += argname + ',' - explanations.append(a.explanation) - else: - source += '%s=%s,' % (keyword, argname) - explanations.append('%s=%s' % (keyword, a.explanation)) - if self.star_args: - star_args = Interpretable(self.star_args) - star_args.eval(frame) - argname = '__exprinfo_star' - vars[argname] = star_args.result - source += '*' + argname + ',' - explanations.append('*' + star_args.explanation) - if self.dstar_args: - dstar_args = Interpretable(self.dstar_args) - dstar_args.eval(frame) - argname = '__exprinfo_kwds' - vars[argname] = dstar_args.result - source += '**' + argname + ',' - explanations.append('**' + dstar_args.explanation) - self.explanation = "%s(%s)" % ( - node.explanation, ', '.join(explanations)) - if source.endswith(','): - source = source[:-1] - source += ')' - try: - self.result = frame.eval(source, **vars) - except passthroughex: - raise - except: - raise Failure(self) - if not node.is_builtin(frame) or not self.is_bool(frame): - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -class Getattr(Interpretable): - __view__ = ast.Getattr - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - source = '__exprinfo_expr.%s' % self.attrname - try: - try: - self.result = frame.eval(source, __exprinfo_expr=expr.result) - except AttributeError: - # Maybe the attribute name needs to be mangled? - if (not self.attrname.startswith("__") or - self.attrname.endswith("__")): - raise - source = "getattr(__exprinfo_expr.__class__, '__name__', '')" - class_name = frame.eval(source, __exprinfo_expr=expr.result) - mangled_attr = "_" + class_name + self.attrname - source = "__exprinfo_expr.%s" % (mangled_attr,) - self.result = frame.eval(source, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - self.explanation = '%s.%s' % (expr.explanation, self.attrname) - # if the attribute comes from the instance, its value is interesting - source = ('hasattr(__exprinfo_expr, "__dict__") and ' - '%r in __exprinfo_expr.__dict__' % self.attrname) - try: - from_instance = frame.is_true( - frame.eval(source, __exprinfo_expr=expr.result)) - except passthroughex: - raise - except: - from_instance = True - if from_instance: - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -# == Re-interpretation of full statements == - -class Assert(Interpretable): - __view__ = ast.Assert - - def run(self, frame): - test = Interpretable(self.test) - test.eval(frame) - # print the result as 'assert ' - self.result = test.result - self.explanation = 'assert ' + test.explanation - if not frame.is_true(test.result): - try: - raise BuiltinAssertionError - except passthroughex: - raise - except: - raise Failure(self) - -class Assign(Interpretable): - __view__ = ast.Assign - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = '... = ' + expr.explanation - # fall-back-run the rest of the assignment - ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr')) - mod = ast.Module(None, ast.Stmt([ass])) - mod.filename = '' - co = pycodegen.ModuleCodeGenerator(mod).getCode() - try: - frame.exec_(co, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - -class Discard(Interpretable): - __view__ = ast.Discard - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = expr.explanation - -class Stmt(Interpretable): - __view__ = ast.Stmt - - def run(self, frame): - for stmt in self.nodes: - stmt = Interpretable(stmt) - stmt.run(frame) - - -def report_failure(e): - explanation = e.node.nice_explanation() - if explanation: - explanation = ", in: " + explanation - else: - explanation = "" - sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation)) - -def check(s, frame=None): - if frame is None: - frame = sys._getframe(1) - frame = py.code.Frame(frame) - expr = parse(s, 'eval') - assert isinstance(expr, ast.Expression) - node = Interpretable(expr.node) - try: - node.eval(frame) - except passthroughex: - raise - except Failure: - e = sys.exc_info()[1] - report_failure(e) - else: - if not frame.is_true(node.result): - sys.stderr.write("assertion failed: %s\n" % node.nice_explanation()) - - -########################################################### -# API / Entry points -# ######################################################### - -def interpret(source, frame, should_fail=False): - module = Interpretable(parse(source, 'exec').node) - #print "got module", module - if isinstance(frame, types.FrameType): - frame = py.code.Frame(frame) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - return getfailure(e) - except passthroughex: - raise - except: - traceback.print_exc() - 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)") - else: - return None - -def getmsg(excinfo): - if isinstance(excinfo, tuple): - excinfo = py.code.ExceptionInfo(excinfo) - #frame, line = gettbline(tb) - #frame = py.code.Frame(frame) - #return interpret(line, frame) - - tb = excinfo.traceback[-1] - source = str(tb.statement).strip() - x = interpret(source, tb.frame, should_fail=True) - if not isinstance(x, str): - raise TypeError("interpret returned non-string %r" % (x,)) - return x - -def getfailure(e): - explanation = e.node.nice_explanation() - if str(e.value): - lines = explanation.split('\n') - lines[0] += " << %s" % (e.value,) - explanation = '\n'.join(lines) - text = "%s: %s" % (e.exc.__name__, explanation) - if text.startswith('AssertionError: assert '): - text = text[16:] - return text - -def run(s, frame=None): - if frame is None: - frame = sys._getframe(1) - frame = py.code.Frame(frame) - module = Interpretable(parse(s, 'exec').node) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - report_failure(e) - - -if __name__ == '__main__': - # example: - def f(): - return 5 - - def g(): - return 3 - - def h(x): - return 'never' - - check("f() * g() == 5") - check("not f()") - check("not (f() and g() or 0)") - check("f() == g()") - i = 4 - check("i == f()") - check("len(f()) == 0") - check("isinstance(2+3+4, float)") - - run("x = i") - check("x == 5") - - run("assert not f(), 'oops'") - run("a, b, c = 1, 2") - run("a, b, c = f()") - - check("max([f(),g()]) == 4") - check("'hello'[g()] == 'h'") - run("'guk%d' % h(f())") diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py index dfb2fec93..30a41497d 100644 --- a/_pytest/assertion/reinterpret.py +++ b/_pytest/assertion/reinterpret.py @@ -46,7 +46,4 @@ class AssertionError(BuiltinAssertionError): if sys.version_info > (3, 0): AssertionError.__module__ = "builtins" -if sys.version_info >= (2, 6) or sys.platform.startswith("java"): - from _pytest.assertion.newinterpret import interpret as reinterpret -else: - from _pytest.assertion.oldinterpret import interpret as reinterpret +from _pytest.assertion.newinterpret import interpret as reinterpret diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py index ad9bb5c79..44b4f23c4 100644 --- a/testing/test_assertinterpret.py +++ b/testing/test_assertinterpret.py @@ -195,65 +195,6 @@ def test_power(): assert "assert (2 ** 3) == 7" in e.msg -class TestView: - - def setup_class(cls): - cls.View = pytest.importorskip("_pytest.assertion.oldinterpret").View - - def test_class_dispatch(self): - ### Use a custom class hierarchy with existing instances - - class Picklable(self.View): - pass - - class Simple(Picklable): - __view__ = object - def pickle(self): - return repr(self.__obj__) - - class Seq(Picklable): - __view__ = list, tuple, dict - def pickle(self): - return ';'.join( - [Picklable(item).pickle() for item in self.__obj__]) - - class Dict(Seq): - __view__ = dict - def pickle(self): - return Seq.pickle(self) + '!' + Seq(self.values()).pickle() - - assert Picklable(123).pickle() == '123' - assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4' - assert Picklable({1:2}).pickle() == '1!2' - - def test_viewtype_class_hierarchy(self): - # Use a custom class hierarchy based on attributes of existing instances - class Operation: - "Existing class that I don't want to change." - def __init__(self, opname, *args): - self.opname = opname - self.args = args - - existing = [Operation('+', 4, 5), - Operation('getitem', '', 'join'), - Operation('setattr', 'x', 'y', 3), - Operation('-', 12, 1)] - - class PyOp(self.View): - def __viewkey__(self): - return self.opname - def generate(self): - return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args))) - - class PyBinaryOp(PyOp): - __view__ = ('+', '-', '*', '/') - def generate(self): - return '%s %s %s' % (self.args[0], self.opname, self.args[1]) - - codelines = [PyOp(op).generate() for op in existing] - assert codelines == ["4 + 5", "getitem('', 'join')", - "setattr('x', 'y', 3)", "12 - 1"] - def test_assert_customizable_reprcompare(monkeypatch): monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') try: From ec597e81a4621b381edefbd2ca2f57895b06677a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2015 16:14:36 -0200 Subject: [PATCH 47/69] Remove references to python pre-2.6 from docs --- doc/en/assert.rst | 7 +++---- doc/en/example/markers.rst | 4 ++-- doc/en/faq.rst | 5 ++--- doc/en/skipping.rst | 17 +---------------- doc/en/test/plugin/xdist.rst | 4 ++-- doc/en/usage.rst | 3 +-- doc/en/xdist.rst | 10 +++++----- 7 files changed, 16 insertions(+), 34 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 795f52586..f65225c92 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -243,10 +243,9 @@ recording the intermediate values. Which technique is used depends on the location of the assert, ``pytest`` configuration, and Python version being used to run ``pytest``. -By default, if the Python version is greater than or equal to 2.6, ``pytest`` -rewrites assert statements in test modules. Rewritten assert statements put -introspection information into the assertion failure message. ``pytest`` only -rewrites test modules directly discovered by its test collection process, so +By default, ``pytest`` rewrites assert statements in test modules. +Rewritten assert statements put introspection information into the assertion failure message. +``pytest`` only rewrites test modules directly discovered by its test collection process, so asserts in supporting modules which are not themselves test modules will not be rewritten. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index aaca80fb2..cf8cbb00d 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -234,8 +234,8 @@ For an example on how to add and work with markers from a plugin, see Marking whole classes or modules ---------------------------------------------------- -If you are programming with Python 2.6 or later you may use ``pytest.mark`` -decorators with classes to apply markers to all of its test methods:: +You may use ``pytest.mark`` decorators with classes to apply markers to all of +its test methods:: # content of test_mark_classlevel.py import pytest diff --git a/doc/en/faq.rst b/doc/en/faq.rst index dbcaf3fd4..3c019b5e1 100644 --- a/doc/en/faq.rst +++ b/doc/en/faq.rst @@ -66,9 +66,8 @@ This completely avoids previous issues of confusing assertion-reporting. It also means, that you can use Python's ``-O`` optimization without losing assertions in test modules. -``pytest`` contains a second, mostly obsolete, assert debugging technique, -invoked via ``--assert=reinterpret``, activated by default on -Python-2.5: When an ``assert`` statement fails, ``pytest`` re-interprets +``pytest`` contains a second, mostly obsolete, assert debugging technique +invoked via ``--assert=reinterpret``: When an ``assert`` statement fails, ``pytest`` re-interprets the expression part to show intermediate values. This technique suffers from a caveat that the rewriting does not: If your expression has side effects (better to avoid them anyway!) the intermediate values may not diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 7464758f5..30f059407 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -89,9 +89,7 @@ between test modules so it's no longer advertised as the primary method. Skip all test functions of a class or module --------------------------------------------- -As with all function :ref:`marking ` you can skip test functions at the -`whole class- or module level`_. If your code targets python2.6 or above you -use the skipif decorator (and any other marker) on classes:: +You can use the ``skipif`` decorator (and any other marker) on classes:: @pytest.mark.skipif(sys.platform == 'win32', reason="does not run on windows") @@ -103,19 +101,6 @@ use the skipif decorator (and any other marker) on classes:: If the condition is true, this marker will produce a skip result for each of the test methods. -If your code targets python2.5 where class-decorators are not available, -you can set the ``pytestmark`` attribute of a class:: - - class TestPosixCalls: - pytestmark = pytest.mark.skipif(sys.platform == 'win32', - reason="does not run on windows") - - def test_function(self): - "will not be setup or run under 'win32' platform" - -As with the class-decorator, the ``pytestmark`` special name tells -``pytest`` to apply it to each test function in the class. - If you want to skip all test functions of a module, you must use the ``pytestmark`` name on the global level: diff --git a/doc/en/test/plugin/xdist.rst b/doc/en/test/plugin/xdist.rst index f2c877be7..7ab6cdc8b 100644 --- a/doc/en/test/plugin/xdist.rst +++ b/doc/en/test/plugin/xdist.rst @@ -126,7 +126,7 @@ Specifying test exec environments in a conftest.py Instead of specifying command line options, you can put options values in a ``conftest.py`` file like this:: - option_tx = ['ssh=myhost//python=python2.5', 'popen//python=python2.5'] + option_tx = ['ssh=myhost//python=python2.7', 'popen//python=python2.7'] option_dist = True Any commandline ``--tx`` specifications will add to the list of @@ -163,7 +163,7 @@ command line options (default) no: run tests inprocess, don't distribute. ``--tx=xspec`` - add a test execution environment. some examples: --tx popen//python=python2.5 --tx socket=192.168.1.102:8888 --tx ssh=user@codespeak.net//chdir=testcache + add a test execution environment. some examples: --tx popen//python=python2.7 --tx socket=192.168.1.102:8888 --tx ssh=user@codespeak.net//chdir=testcache ``-d`` load-balance tests. shortcut for '--dist=load' ``--rsyncdir=dir1`` diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 2deaadb5d..b8abb87e9 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -12,8 +12,7 @@ Calling pytest through ``python -m pytest`` .. versionadded:: 2.0 -If you use Python-2.5 or later you can invoke testing through the -Python interpreter from the command line:: +You can invoke testing through the Python interpreter from the command line:: python -m pytest [...] diff --git a/doc/en/xdist.rst b/doc/en/xdist.rst index 715aa4749..ee1bd6032 100644 --- a/doc/en/xdist.rst +++ b/doc/en/xdist.rst @@ -61,16 +61,16 @@ a lot of I/O this can lead to considerable speed ups. Running tests in a Python subprocess +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -To instantiate a Python-2.4 subprocess and send tests to it, you may type:: +To instantiate a Python-2.7 subprocess and send tests to it, you may type:: - py.test -d --tx popen//python=python2.4 + py.test -d --tx popen//python=python2.7 -This will start a subprocess which is run with the "python2.4" +This will start a subprocess which is run with the "python2.7" Python interpreter, found in your system binary lookup path. If you prefix the --tx option value like this:: - py.test -d --tx 3*popen//python=python2.4 + py.test -d --tx 3*popen//python=python2.7 then three subprocesses would be created and the tests will be distributed to three subprocesses and run simultanously. @@ -170,7 +170,7 @@ For example, you could make running with three subprocesses your default:: You can also add default environments like this:: [pytest] - addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 + addopts = --tx ssh=myhost//python=python2.7 --tx ssh=myhost//python=python2.6 and then just type:: From 25c392196f6339b2a5c8e88feca4824de7a18950 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2015 16:48:49 -0200 Subject: [PATCH 48/69] Mention #1226 in the CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d719e6a9b..1c9d85158 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,10 @@ * New `-rp` and `-rP` reporting options give the summary and full output of passing tests, respectively. Thanks to David Vierra for the PR. +* fix #1226: Removed code and documentation for Python 2.5 or lower versions, + including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. + Thanks Bruno Oliveira for the PR. + 2.8.6.dev1 ---------- From 943099ddd176b258982062c0a03f060acbba5f19 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Dec 2015 18:30:58 -0200 Subject: [PATCH 49/69] Merge newinterpret into reinterpret.py --- _pytest/assertion/newinterpret.py | 364 ------------------------------ _pytest/assertion/reinterpret.py | 364 +++++++++++++++++++++++++++++- 2 files changed, 360 insertions(+), 368 deletions(-) delete mode 100644 _pytest/assertion/newinterpret.py diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py deleted file mode 100644 index fbca118d1..000000000 --- a/_pytest/assertion/newinterpret.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -Find intermediate evalutation results in assert statements through builtin AST. -""" - -import sys -import ast - -import py -from _pytest.assertion import util -from _pytest.assertion.reinterpret import BuiltinAssertionError - - -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 interpret(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 = py.code.Frame(sys._getframe(1)) - return interpret(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 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/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py index 30a41497d..213a3a00a 100644 --- a/_pytest/assertion/reinterpret.py +++ b/_pytest/assertion/reinterpret.py @@ -1,12 +1,16 @@ +""" +Find intermediate evalutation results in assert statements through builtin AST. +""" +import ast import sys import py -from _pytest.assertion.util import BuiltinAssertionError +from _pytest.assertion import util u = py.builtin._totext -class AssertionError(BuiltinAssertionError): +class AssertionError(util.BuiltinAssertionError): def __init__(self, *args): - BuiltinAssertionError.__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 @@ -46,4 +50,356 @@ class AssertionError(BuiltinAssertionError): if sys.version_info > (3, 0): AssertionError.__module__ = "builtins" -from _pytest.assertion.newinterpret import interpret as reinterpret +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 = py.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 + From 6b25fb4d6412d586b2f4c20fbba5a3ecae4d57a2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 25 Dec 2015 20:33:04 -0200 Subject: [PATCH 50/69] Separate 2.9.0 CHANGELOG into sections Fix #1275 --- CHANGELOG | 143 +++++++++++++++++++++++++++++------------------------- 1 file changed, 77 insertions(+), 66 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c9d85158..6d9ab3ec8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,18 +1,29 @@ 2.9.0.dev ---------- +========= + +New Features +------------ * New `pytest.mark.skip` mark, which unconditional skips marked tests. Thanks Michael Aquilina for the complete PR. -* fix issue #680: the -s and -c options should now work under xdist; - `Config.fromdictargs` now represents its input much more faithfully. - Thanks to Buck Evan for the complete PR. +* New `-rp` and `-rP` reporting options give the summary and full output + of passing tests, respectively. Thanks to David Vierra for the PR. + + +Changes +------- * `pytest_enter_pdb` now optionally receives the pytest config object. Thanks Bruno Oliveira for the PR. -* New `-rp` and `-rP` reporting options give the summary and full output - of passing tests, respectively. Thanks to David Vierra for the PR. + +Bug Fixes/Adjustments +--------------------- + +* fix issue #680: the -s and -c options should now work under xdist; + `Config.fromdictargs` now represents its input much more faithfully. + Thanks to Buck Evan for the complete PR. * fix #1226: Removed code and documentation for Python 2.5 or lower versions, including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. @@ -20,11 +31,11 @@ 2.8.6.dev1 ----------- +========== 2.8.5 ------ +===== - fix #1243: fixed issue where class attributes injected during collection could break pytest. PR by Alexei Kozlenok, thanks Ronny Pfannschmidt and Bruno Oliveira for the review and help. @@ -38,7 +49,7 @@ 2.8.4 ------ +===== - fix #1190: ``deprecated_call()`` now works when the deprecated function has been already called by another test in the same @@ -62,7 +73,7 @@ Thanks Bruno Oliveira for the PR. 2.8.3 ------ +===== - fix #1169: add __name__ attribute to testcases in TestCaseFunction to support the @unittest.skip decorator on functions and methods. @@ -90,7 +101,7 @@ 2.8.2 ------ +===== - fix #1085: proper handling of encoding errors when passing encoded byte strings to pytest.parametrize in Python 2. @@ -110,7 +121,7 @@ Oliveira for the PR. 2.8.1 ------ +===== - fix #1034: Add missing nodeid on pytest_logwarning call in addhook. Thanks Simon Gomizelj for the PR. @@ -157,7 +168,7 @@ - fix issue 1029: transform errors when writing cache values into pytest-warnings 2.8.0 ------------------------------ +============================= - new ``--lf`` and ``-ff`` options to run only the last failing tests or "failing tests first" from the last run. This functionality is provided @@ -347,7 +358,7 @@ Thanks Peter Lauri for the report and Bruno Oliveira for the PR. 2.7.3 (compared to 2.7.2) ------------------------------ +============================= - Allow 'dev', 'rc', or other non-integer version strings in `importorskip`. Thanks to Eric Hunsberger for the PR. @@ -390,7 +401,7 @@ Thanks Bruno Oliveira for the PR. 2.7.2 (compared to 2.7.1) ------------------------------ +============================= - fix issue767: pytest.raises value attribute does not contain the exception instance on Python 2.6. Thanks Eric Siegerman for providing the test @@ -419,7 +430,7 @@ 2.7.1 (compared to 2.7.0) ------------------------------ +============================= - fix issue731: do not get confused by the braces which may be present and unbalanced in an object's repr while collapsing False @@ -452,7 +463,7 @@ at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ------------------------------ +============================= - fix issue435: make reload() work when assert rewriting is active. Thanks Daniel Hahler. @@ -522,7 +533,7 @@ via postmortem debugging (almarklein). 2.6.4 ----------- +========== - Improve assertion failure reporting on iterables, by using ndiff and pprint. @@ -551,7 +562,7 @@ - fix issue614: fixed pastebin support. 2.6.3 ------------ +=========== - fix issue575: xunit-xml was reporting collection errors as failures instead of errors, thanks Oleg Sinyavskiy. @@ -578,7 +589,7 @@ Floris Bruynooghe. 2.6.2 ------------ +=========== - Added function pytest.freeze_includes(), which makes it easy to embed pytest into executables using tools like cx_freeze. @@ -607,7 +618,7 @@ to them. 2.6.1 ------------------------------------ +=================================== - No longer show line numbers in the --verbose output, the output is now purely the nodeid. The line number is still shown in failure reports. @@ -639,7 +650,7 @@ Thanks Bruno Oliveira. 2.6 ------------------------------------ +=================================== - Cache exceptions from fixtures according to their scope (issue 467). @@ -744,7 +755,7 @@ 2.5.2 ------------------------------------ +=================================== - fix issue409 -- better interoperate with cx_freeze by not trying to import from collections.abc which causes problems @@ -772,7 +783,7 @@ 2.5.1 ------------------------------------ +=================================== - merge new documentation styling PR from Tobias Bieniek. @@ -793,7 +804,7 @@ 2.5.0 ------------------------------------ +=================================== - dropped python2.5 from automated release testing of pytest itself which means it's probably going to break soon (but still works @@ -929,7 +940,7 @@ - fix verbose reporting for @mock'd test functions v2.4.2 ------------------------------------ +=================================== - on Windows require colorama and a newer py lib so that py.io.TerminalWriter() now uses colorama instead of its own ctypes hacks. (fixes issue365) @@ -960,7 +971,7 @@ v2.4.2 config.do_configure() for plugin-compatibility v2.4.1 ------------------------------------ +=================================== - When using parser.addoption() unicode arguments to the "type" keyword should also be converted to the respective types. @@ -976,7 +987,7 @@ v2.4.1 - merge doc typo fixes, thanks Andy Dirnberger v2.4 ------------------------------------ +=================================== known incompatibilities: @@ -1145,7 +1156,7 @@ Bug fixes: information at the end of a test run. v2.3.5 ------------------------------------ +=================================== - fix issue169: respect --tb=style with setup/teardown errors as well. @@ -1210,7 +1221,7 @@ v2.3.5 - fix issue266 - accept unicode in MarkEvaluator expressions v2.3.4 ------------------------------------ +=================================== - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to @@ -1230,7 +1241,7 @@ v2.3.4 method in a certain test class. v2.3.3 ------------------------------------ +=================================== - fix issue214 - parse modules that contain special objects like e. g. flask's request object which blows up on getattr access if no request @@ -1262,7 +1273,7 @@ v2.3.3 add a ``config.getoption(name)`` helper function for consistency. v2.3.2 ------------------------------------ +=================================== - fix issue208 and fix issue29 use new py version to avoid long pauses when printing tracebacks in long modules @@ -1295,7 +1306,7 @@ v2.3.2 bits are properly distributed for maintainers who run pytest-own tests v2.3.1 ------------------------------------ +=================================== - fix issue202 - fix regression: using "self" from fixture functions now works as expected (it's the same "self" instance that a test method @@ -1308,7 +1319,7 @@ v2.3.1 pytest.mark.* usage. v2.3.0 ------------------------------------ +=================================== - fix issue202 - better automatic names for parametrized test functions - fix issue139 - introduce @pytest.fixture which allows direct scoping @@ -1387,7 +1398,7 @@ v2.3.0 - py.test -vv will show all of assert comparisations instead of truncating v2.2.4 ------------------------------------ +=================================== - fix error message for rewritten assertions involving the % operator - fix issue 126: correctly match all invalid xml characters for junitxml @@ -1404,12 +1415,12 @@ v2.2.4 - upgrade distribute_setup.py to 0.6.27 v2.2.3 ----------------------------------------- +======================================== - fix uploaded package to only include neccesary files v2.2.2 ----------------------------------------- +======================================== - fix issue101: wrong args to unittest.TestCase test function now produce better output @@ -1429,7 +1440,7 @@ v2.2.2 with distributed testing (no upgrade of pytest-xdist needed) v2.2.1 ----------------------------------------- +======================================== - fix issue99 (in pytest and py) internallerrors with resultlog now produce better output - fixed by normalizing pytest_internalerror @@ -1446,7 +1457,7 @@ v2.2.1 to Ralf Schmitt (fixed by depending on a more recent pylib) v2.2.0 ----------------------------------------- +======================================== - fix issue90: introduce eager tearing down of test items so that teardown function are called earlier. @@ -1481,7 +1492,7 @@ v2.2.0 - add support for skip properties on unittest classes and functions v2.1.3 ----------------------------------------- +======================================== - fix issue79: assertion rewriting failed on some comparisons in boolops - correctly handle zero length arguments (a la pytest '') @@ -1490,7 +1501,7 @@ v2.1.3 - fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests v2.1.2 ----------------------------------------- +======================================== - fix assertion rewriting on files with windows newlines on some Python versions - refine test discovery by package/module name (--pyargs), thanks Florian Mayer @@ -1500,7 +1511,7 @@ v2.1.2 - don't try assertion rewriting on Jython, use reinterp v2.1.1 ----------------------------------------------- +============================================== - fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks - fix issue60 / fix error conditions involving the creation of __pycache__ @@ -1513,7 +1524,7 @@ v2.1.1 - you can now build a man page with "cd doc ; make man" v2.1.0 ----------------------------------------------- +============================================== - fix issue53 call nosestyle setup functions with correct ordering - fix issue58 and issue59: new assertion code fixes @@ -1533,7 +1544,7 @@ v2.1.0 - fix issue 35 - provide PDF doc version and download link from index page v2.0.3 ----------------------------------------------- +============================================== - fix issue38: nicer tracebacks on calls to hooks, particularly early configure/sessionstart ones @@ -1553,7 +1564,7 @@ v2.0.3 - fix issue37: avoid invalid characters in junitxml's output v2.0.2 ----------------------------------------------- +============================================== - tackle issue32 - speed up test runs of very quick test functions by reducing the relative overhead @@ -1605,7 +1616,7 @@ v2.0.2 - avoid std unittest assertion helper code in tracebacks (thanks Ronny) v2.0.1 ----------------------------------------------- +============================================== - refine and unify initial capturing so that it works nicely even if the logging module is used on an early-loaded conftest.py @@ -1654,7 +1665,7 @@ v2.0.1 mechanism, see the docs. v2.0.0 ----------------------------------------------- +============================================== - pytest-2.0 is now its own package and depends on pylib-2.0 - new ability: python -m pytest / python -m pytest.main ability @@ -1699,7 +1710,7 @@ v2.0.0 - fix strangeness: mark.* objects are now immutable, create new instances v1.3.4 ----------------------------------------------- +============================================== - fix issue111: improve install documentation for windows - fix issue119: fix custom collectability of __init__.py as a module @@ -1708,7 +1719,7 @@ v1.3.4 - fix issue118: new --tb=native for presenting cpython-standard exceptions v1.3.3 ----------------------------------------------- +============================================== - fix issue113: assertion representation problem with triple-quoted strings (and possibly other cases) @@ -1723,7 +1734,7 @@ v1.3.3 - remove trailing whitespace in all py/text distribution files v1.3.2 ----------------------------------------------- +============================================== New features ++++++++++++++++++ @@ -1798,7 +1809,7 @@ Bug fixes / Maintenance - ship distribute_setup.py version 0.6.13 v1.3.1 ---------------------------------------------- +============================================= New features ++++++++++++++++++ @@ -1870,7 +1881,7 @@ Fixes / Maintenance v1.3.0 ---------------------------------------------- +============================================= - deprecate --report option in favour of a new shorter and easier to remember -r option: it takes a string argument consisting of any @@ -1935,7 +1946,7 @@ v1.3.0 v1.2.0 ---------------------------------------------- +============================================= - refined usage and options for "py.cleanup":: @@ -1974,7 +1985,7 @@ v1.2.0 - fix plugin links v1.1.1 ---------------------------------------------- +============================================= - moved dist/looponfailing from py.test core into a new separately released pytest-xdist plugin. @@ -2058,7 +2069,7 @@ v1.1.1 v1.1.0 ---------------------------------------------- +============================================= - introduce automatic plugin registration via 'pytest11' entrypoints via setuptools' pkg_resources.iter_entry_points @@ -2077,7 +2088,7 @@ v1.1.0 report a correct location v1.0.2 ---------------------------------------------- +============================================= * adjust and improve docs @@ -2162,7 +2173,7 @@ v1.0.2 * simplified internal localpath implementation v1.0.2 -------------------------------------------- +=========================================== * fixing packaging issues, triggered by fedora redhat packaging, also added doc, examples and contrib dirs to the tarball. @@ -2170,7 +2181,7 @@ v1.0.2 * added a documentation link to the new django plugin. v1.0.1 -------------------------------------------- +=========================================== * added a 'pytest_nose' plugin which handles nose.SkipTest, nose-style function/method/generator setup/teardown and @@ -2204,13 +2215,13 @@ v1.0.1 renamed some internal methods and argnames v1.0.0 -------------------------------------------- +=========================================== * more terse reporting try to show filesystem path relatively to current dir * improve xfail output a bit v1.0.0b9 -------------------------------------------- +=========================================== * cleanly handle and report final teardown of test setup @@ -2244,7 +2255,7 @@ v1.0.0b9 v1.0.0b8 -------------------------------------------- +=========================================== * pytest_unittest-plugin is now enabled by default @@ -2273,7 +2284,7 @@ v1.0.0b8 thanks Radomir. v1.0.0b7 -------------------------------------------- +=========================================== * renamed py.test.xfail back to py.test.mark.xfail to avoid two ways to decorate for xfail @@ -2298,7 +2309,7 @@ v1.0.0b7 * make __name__ == "__channelexec__" for remote_exec code v1.0.0b3 -------------------------------------------- +=========================================== * plugin classes are removed: one now defines hooks directly in conftest.py or global pytest_*.py @@ -2315,7 +2326,7 @@ v1.0.0b3 v1.0.0b1 -------------------------------------------- +=========================================== * introduced new "funcarg" setup method, see doc/test/funcarg.txt @@ -2339,7 +2350,7 @@ v1.0.0b1 XXX lots of things missing here XXX v0.9.2 -------------------------------------------- +=========================================== * refined installation and metadata, created new setup.py, now based on setuptools/ez_setup (thanks to Ralf Schmitt @@ -2372,7 +2383,7 @@ v0.9.2 * there now is a py.__version__ attribute v0.9.1 -------------------------------------------- +=========================================== This is a fairly complete list of v0.9.1, which can serve as a reference for developers. From a0edbb75a46c95956a6a6d042a60904c077f7c1a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Dec 2015 20:55:19 -0200 Subject: [PATCH 51/69] Implement ALLOW_BYTES doctest option Fix #1287 --- _pytest/doctest.py | 58 +++++++++++++++++++++++++++-------------- testing/test_doctest.py | 47 ++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/_pytest/doctest.py b/_pytest/doctest.py index fd4a24790..d2215ad18 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -79,7 +79,7 @@ class DoctestItem(pytest.Item): lineno = test.lineno + example.lineno + 1 message = excinfo.type.__name__ reprlocation = ReprFileLocation(filename, lineno, message) - checker = _get_unicode_checker() + checker = _get_checker() REPORT_UDIFF = doctest.REPORT_UDIFF filelines = py.path.local(filename).readlines(cr=0) lines = [] @@ -118,7 +118,9 @@ def _get_flag_lookup(): ELLIPSIS=doctest.ELLIPSIS, IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, - ALLOW_UNICODE=_get_allow_unicode_flag()) + ALLOW_UNICODE=_get_allow_unicode_flag(), + ALLOW_BYTES=_get_allow_bytes_flag(), + ) def get_optionflags(parent): @@ -147,7 +149,7 @@ class DoctestTextfile(DoctestItem, pytest.Module): optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, - checker=_get_unicode_checker()) + checker=_get_checker()) parser = doctest.DocTestParser() test = parser.get_doctest(text, globs, name, filename, 0) @@ -182,7 +184,7 @@ class DoctestModule(pytest.Module): finder = doctest.DocTestFinder() optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, - checker=_get_unicode_checker()) + checker=_get_checker()) for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests yield DoctestItem(test.name, self, runner, test) @@ -204,28 +206,32 @@ def _setup_fixtures(doctest_item): return fixture_request -def _get_unicode_checker(): +def _get_checker(): """ Returns a doctest.OutputChecker subclass that takes in account the - ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful - when the same doctest should run in Python 2 and Python 3. + ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES + to strip b'' prefixes. + Useful when the same doctest should run in Python 2 and Python 3. An inner class is used to avoid importing "doctest" at the module level. """ - if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'): - return _get_unicode_checker.UnicodeOutputChecker() + if hasattr(_get_checker, 'LiteralsOutputChecker'): + return _get_checker.LiteralsOutputChecker() import doctest import re - class UnicodeOutputChecker(doctest.OutputChecker): + class LiteralsOutputChecker(doctest.OutputChecker): """ Copied from doctest_nose_plugin.py from the nltk project: https://github.com/nltk/nltk + + Further extended to also support byte literals. """ - _literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) def check_output(self, want, got, optionflags): res = doctest.OutputChecker.check_output(self, want, got, @@ -233,23 +239,27 @@ def _get_unicode_checker(): if res: return True - if not (optionflags & _get_allow_unicode_flag()): + allow_unicode = optionflags & _get_allow_unicode_flag() + allow_bytes = optionflags & _get_allow_bytes_flag() + if not allow_unicode and not allow_bytes: return False else: # pragma: no cover - # the code below will end up executed only in Python 2 in - # our tests, and our coverage check runs in Python 3 only - def remove_u_prefixes(txt): - return re.sub(self._literal_re, r'\1\2', txt) + def remove_prefixes(regex, txt): + return re.sub(regex, r'\1\2', txt) - want = remove_u_prefixes(want) - got = remove_u_prefixes(got) + if allow_unicode: + want = remove_prefixes(self._unicode_literal_re, want) + got = remove_prefixes(self._unicode_literal_re, got) + if allow_bytes: + want = remove_prefixes(self._bytes_literal_re, want) + got = remove_prefixes(self._bytes_literal_re, got) res = doctest.OutputChecker.check_output(self, want, got, optionflags) return res - _get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker - return _get_unicode_checker.UnicodeOutputChecker() + _get_checker.LiteralsOutputChecker = LiteralsOutputChecker + return _get_checker.LiteralsOutputChecker() def _get_allow_unicode_flag(): @@ -258,3 +268,11 @@ def _get_allow_unicode_flag(): """ import doctest return doctest.register_optionflag('ALLOW_UNICODE') + + +def _get_allow_bytes_flag(): + """ + Registers and returns the ALLOW_BYTES flag. + """ + import doctest + return doctest.register_optionflag('ALLOW_BYTES') diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 88d90a7bf..377664134 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -371,6 +371,9 @@ class TestDoctests: "--junit-xml=junit.xml") reprec.assertoutcome(failed=1) + +class TestLiterals: + @pytest.mark.parametrize('config_mode', ['ini', 'comment']) def test_allow_unicode(self, testdir, config_mode): """Test that doctests which output unicode work in all python versions @@ -400,6 +403,35 @@ class TestDoctests: reprec = testdir.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) + @pytest.mark.parametrize('config_mode', ['ini', 'comment']) + def test_allow_bytes(self, testdir, config_mode): + """Test that doctests which output bytes work in all python versions + tested by pytest when the ALLOW_BYTES option is used (either in + the ini file or by an inline comment)(#1287). + """ + if config_mode == 'ini': + testdir.makeini(''' + [pytest] + doctest_optionflags = ALLOW_BYTES + ''') + comment = '' + else: + comment = '#doctest: +ALLOW_BYTES' + + testdir.maketxtfile(test_doc=""" + >>> b'foo' {comment} + 'foo' + """.format(comment=comment)) + testdir.makepyfile(foo=""" + def foo(): + ''' + >>> b'foo' {comment} + 'foo' + ''' + """.format(comment=comment)) + reprec = testdir.inline_run("--doctest-modules") + reprec.assertoutcome(passed=2) + def test_unicode_string(self, testdir): """Test that doctests which output unicode fail in Python 2 when the ALLOW_UNICODE option is not used. The same test should pass @@ -413,6 +445,19 @@ class TestDoctests: passed = int(sys.version_info[0] >= 3) reprec.assertoutcome(passed=passed, failed=int(not passed)) + def test_bytes_literal(self, testdir): + """Test that doctests which output bytes fail in Python 3 when + the ALLOW_BYTES option is not used. The same test should pass + in Python 2 (#1287). + """ + testdir.maketxtfile(test_doc=""" + >>> b'foo' + 'foo' + """) + reprec = testdir.inline_run() + passed = int(sys.version_info[0] == 2) + reprec.assertoutcome(passed=passed, failed=int(not passed)) + class TestDoctestSkips: """ @@ -579,4 +624,4 @@ class TestDoctestAutoUseFixtures: """) result = testdir.runpytest('--doctest-modules') assert 'FAILURES' not in str(result.stdout.str()) - result.stdout.fnmatch_lines(['*=== 1 passed in *']) \ No newline at end of file + result.stdout.fnmatch_lines(['*=== 1 passed in *']) From 5a5b732fe1b09a409c2498551bff044d0ef9c0a8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Dec 2015 21:05:11 -0200 Subject: [PATCH 52/69] Add docs for ALLOW_BYTES doctest option Fix #1287 --- doc/en/doctest.rst | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 0befa6702..73e514782 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -67,19 +67,32 @@ when executing text doctest files. The standard ``doctest`` module provides some setting flags to configure the strictness of doctest tests. In py.test You can enable those flags those flags using the configuration file. To make pytest ignore trailing whitespaces and -ignore lengthy exception stack traces you can just write:: +ignore lengthy exception stack traces you can just write: + +.. code-block:: ini - # content of pytest.ini [pytest] doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL +py.test also introduces new options to allow doctests to run in Python 2 and +Python 3 unchanged: -py.test also introduces a new ``ALLOW_UNICODE`` option flag: when enabled, the -``u`` prefix is stripped from unicode strings in expected doctest output. This -allows doctests which use unicode to run in Python 2 and 3 unchanged. +* ``ALLOW_UNICODE``: when enabled, the ``u`` prefix is stripped from unicode + strings in expected doctest output. -As with any other option flag, this flag can be enabled in ``pytest.ini`` using -the ``doctest_optionflags`` ini option or by an inline comment in the doc test +* ``ALLOW_BYTES``: when enabled, the ``b`` prefix is stripped from byte strings + in expected doctest output. + +As with any other option flag, these flags can be enabled in ``pytest.ini`` using +the ``doctest_optionflags`` ini option: + +.. code-block:: ini + + [pytest] + doctest_optionflags = ALLOW_UNICODE ALLOW_BYTES + + +Alternatively, it can be enabled by an inline comment in the doc test itself:: # content of example.rst From 719d63085dbd79300030f2ce39b8bff37d4f64ba Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Dec 2015 21:08:25 -0200 Subject: [PATCH 53/69] Add CHANGELOG entry for ALLOW_BYTES doctest option --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6d9ab3ec8..33ffca92a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,10 @@ New Features * New `-rp` and `-rP` reporting options give the summary and full output of passing tests, respectively. Thanks to David Vierra for the PR. +* New ``ALLOW_BYTES`` doctest option strips ``b`` prefixes from byte strings + in doctest output (similar to ``ALLOW_UNICODE``). + Thanks Jason R. Coombs for the request and Bruno Oliveira for the PR (#1287). + Changes ------- From 309ecf7ab3c4a437857f9ce75baaf8624ad3df92 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 29 Dec 2015 21:09:15 -0200 Subject: [PATCH 54/69] Rename "BugFixes/Adjustments" to just "Bug Fixes" as commented elsewhere --- CHANGELOG | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 33ffca92a..5c6108cce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,18 +21,18 @@ Changes * `pytest_enter_pdb` now optionally receives the pytest config object. Thanks Bruno Oliveira for the PR. +* fix #1226: Removed code and documentation for Python 2.5 or lower versions, + including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. + Thanks Bruno Oliveira for the PR. -Bug Fixes/Adjustments ---------------------- + +Bug Fixes +--------- * fix issue #680: the -s and -c options should now work under xdist; `Config.fromdictargs` now represents its input much more faithfully. Thanks to Buck Evan for the complete PR. -* fix #1226: Removed code and documentation for Python 2.5 or lower versions, - including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. - Thanks Bruno Oliveira for the PR. - 2.8.6.dev1 ========== From 0ea8dc0d40479a3e4155e87ca1c9aa496dbd689c Mon Sep 17 00:00:00 2001 From: jab Date: Fri, 11 Dec 2015 19:58:49 +0000 Subject: [PATCH 55/69] make --doctest-glob multi-allowed --- _pytest/doctest.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/_pytest/doctest.py b/_pytest/doctest.py index fd4a24790..ed1814358 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -15,7 +15,7 @@ def pytest_addoption(parser): help="run doctests in all .py modules", dest="doctestmodules") group.addoption("--doctest-glob", - action="store", default="test*.txt", metavar="pat", + action="append", default=["test*.txt"], metavar="pat", help="doctests file matching pattern, default: test*.txt", dest="doctestglob") group.addoption("--doctest-ignore-import-errors", @@ -29,11 +29,20 @@ def pytest_collect_file(path, parent): if path.ext == ".py": if config.option.doctestmodules: return DoctestModule(path, parent) - elif (path.ext in ('.txt', '.rst') and parent.session.isinitpath(path)) or \ - path.check(fnmatch=config.getvalue("doctestglob")): + elif _is_doctest(config, path, parent): return DoctestTextfile(path, parent) +def _is_doctest(config, path, parent): + if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path): + return True + globs = config.getoption("doctestglob") + for glob in globs: + if path.check(fnmatch=glob): + return True + return False + + class ReprFailDoctest(TerminalRepr): def __init__(self, reprlocation, lines): From 1bdf71730a3a84cfed9b81d18e5beb76943c2907 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 30 Dec 2015 18:19:08 -0200 Subject: [PATCH 56/69] Complement #1255 by adding tests and docs Fix #1242 --- AUTHORS | 1 + CHANGELOG | 3 +++ _pytest/doctest.py | 4 ++-- doc/en/doctest.rst | 5 ++++- testing/test_doctest.py | 34 ++++++++++++++++++++++++++++++++-- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index a43fd69a3..17477d59c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,6 +45,7 @@ Jaap Broekhuizen Jan Balster Janne Vanhala Jason R. Coombs +Joshua Bronson Jurko Gospodnetić Katarzyna Jachim Kevin Cox diff --git a/CHANGELOG b/CHANGELOG index 6d9ab3ec8..cec4138ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,9 @@ New Features * New `pytest.mark.skip` mark, which unconditional skips marked tests. Thanks Michael Aquilina for the complete PR. +* ``--doctest-glob`` may now be passed multiple times in the command-line. + Thanks Joshua Bronson and Bruno Oliveira for the PR. + * New `-rp` and `-rP` reporting options give the summary and full output of passing tests, respectively. Thanks to David Vierra for the PR. diff --git a/_pytest/doctest.py b/_pytest/doctest.py index ed1814358..2175d8de1 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -15,7 +15,7 @@ def pytest_addoption(parser): help="run doctests in all .py modules", dest="doctestmodules") group.addoption("--doctest-glob", - action="append", default=["test*.txt"], metavar="pat", + action="append", default=[], metavar="pat", help="doctests file matching pattern, default: test*.txt", dest="doctestglob") group.addoption("--doctest-ignore-import-errors", @@ -36,7 +36,7 @@ def pytest_collect_file(path, parent): def _is_doctest(config, path, parent): if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path): return True - globs = config.getoption("doctestglob") + globs = config.getoption("doctestglob") or ['test*.txt'] for glob in globs: if path.check(fnmatch=glob): return True diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 0befa6702..1dfb73485 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -8,7 +8,10 @@ can change the pattern by issuing:: py.test --doctest-glob='*.rst' -on the command line. You can also trigger running of doctests +on the command line. Since version ``2.9``, ``--doctest-glob`` +can be given multiple times in the command-line. + +You can also trigger running of doctests from docstrings in all python modules (including regular python test modules):: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 88d90a7bf..39e51f8ee 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -87,7 +87,7 @@ class TestDoctests: reprec.assertoutcome(failed=1) def test_new_pattern(self, testdir): - p = testdir.maketxtfile(xdoc =""" + p = testdir.maketxtfile(xdoc=""" >>> x = 1 >>> x == 1 False @@ -95,6 +95,36 @@ class TestDoctests: reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1) + def test_multiple_patterns(self, testdir): + """Test support for multiple --doctest-glob arguments (#1255). + """ + testdir.maketxtfile(xdoc=""" + >>> 1 + 1 + """) + testdir.makefile('.foo', test=""" + >>> 1 + 1 + """) + testdir.maketxtfile(test_normal=""" + >>> 1 + 1 + """) + expected = set(['xdoc.txt', 'test.foo', 'test_normal.txt']) + assert set(x.basename for x in testdir.tmpdir.listdir()) == expected + args = ["--doctest-glob=xdoc*.txt", "--doctest-glob=*.foo"] + result = testdir.runpytest(*args) + result.stdout.fnmatch_lines([ + '*test.foo *', + '*xdoc.txt *', + '*2 passed*', + ]) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*test_normal.txt *', + '*1 passed*', + ]) + def test_doctest_unexpected_exception(self, testdir): testdir.maketxtfile(""" >>> i = 0 @@ -579,4 +609,4 @@ class TestDoctestAutoUseFixtures: """) result = testdir.runpytest('--doctest-modules') assert 'FAILURES' not in str(result.stdout.str()) - result.stdout.fnmatch_lines(['*=== 1 passed in *']) \ No newline at end of file + result.stdout.fnmatch_lines(['*=== 1 passed in *']) From 02dd6df6e6ba6bbb3bc03c9ec5a09eac33f91bdd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 3 Jan 2016 23:09:24 -0200 Subject: [PATCH 57/69] Changelog now in rst format (2.9.0 and onward) and add rst-lint check Fix #1274 --- CHANGELOG => CHANGELOG.rst | 63 ++++++++++++++++++++------------------ doc/en/changelog.rst | 2 +- tox.ini | 2 ++ 3 files changed, 37 insertions(+), 30 deletions(-) rename CHANGELOG => CHANGELOG.rst (98%) diff --git a/CHANGELOG b/CHANGELOG.rst similarity index 98% rename from CHANGELOG rename to CHANGELOG.rst index 044753664..30909034f 100644 --- a/CHANGELOG +++ b/CHANGELOG.rst @@ -1,40 +1,49 @@ 2.9.0.dev ========= -New Features ------------- +**New Features** -* New `pytest.mark.skip` mark, which unconditional skips marked tests. - Thanks Michael Aquilina for the complete PR. +* New ``pytest.mark.skip`` mark, which unconditional skips marked tests. + Thanks `@MichaelAquilina`_ for the complete PR (`#1040`_). * ``--doctest-glob`` may now be passed multiple times in the command-line. - Thanks Joshua Bronson and Bruno Oliveira for the PR. + Thanks `@jab`_ and `@nicoddemus`_ for the PR. -* New `-rp` and `-rP` reporting options give the summary and full output - of passing tests, respectively. Thanks to David Vierra for the PR. +* New ``-rp`` and ``-rP`` reporting options give the summary and full output + of passing tests, respectively. Thanks to `@codewarrior0`_ for the PR. * New ``ALLOW_BYTES`` doctest option strips ``b`` prefixes from byte strings in doctest output (similar to ``ALLOW_UNICODE``). - Thanks Jason R. Coombs for the request and Bruno Oliveira for the PR (#1287). + Thanks `@jaraco`_ for the request and `@nicoddemus`_ for the PR (`#1287`_). -Changes -------- +**Changes** -* `pytest_enter_pdb` now optionally receives the pytest config object. - Thanks Bruno Oliveira for the PR. +* ``pytest_enter_pdb`` now optionally receives the pytest config object. + Thanks `@nicoddemus`_ for the PR. -* fix #1226: Removed code and documentation for Python 2.5 or lower versions, +* Removed code and documentation for Python 2.5 or lower versions, including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. - Thanks Bruno Oliveira for the PR. + Thanks `@nicoddemus`_ for the PR (`#1226`_). -Bug Fixes ---------- +**Bug Fixes** -* fix issue #680: the -s and -c options should now work under xdist; - `Config.fromdictargs` now represents its input much more faithfully. - Thanks to Buck Evan for the complete PR. +* The ``-s`` and ``-c`` options should now work under ``xdist``; + ``Config.fromdictargs`` now represents its input much more faithfully. + Thanks to `@bukzor`_ for the complete PR (`#680`_). + + +.. _#1040: https://github.com/pytest-dev/pytest/pull/1040 +.. _#680: https://github.com/pytest-dev/pytest/issues/680 +.. _#1287: https://github.com/pytest-dev/pytest/pull/1287 +.. _#1226: https://github.com/pytest-dev/pytest/pull/1226 +.. _@MichaelAquilina: https://github.com/MichaelAquilina +.. _@bukzor: https://github.com/bukzor +.. _@nicoddemus: https://github.com/nicoddemus +.. _@jab: https://github.com/jab +.. _@codewarrior0: https://github.com/codewarrior0 +.. _@jaraco: https://github.com/jaraco 2.8.6.dev1 @@ -543,7 +552,7 @@ Bug Fixes it from the "decorator" case. Thanks Tom Viner. - "python_classes" and "python_functions" options now support glob-patterns - for test discovery, as discussed in issue600. Thanks Ldiary Translations. + for test discovery, as discussed in issue600. Thanks Ldiary Translations. - allow to override parametrized fixtures with non-parametrized ones and vice versa (bubenkoff). @@ -1757,8 +1766,7 @@ v1.3.3 v1.3.2 ============================================== -New features -++++++++++++++++++ +**New features** - fix issue103: introduce py.test.raises as context manager, examples:: @@ -1793,8 +1801,7 @@ New features - introduce '--junitprefix=STR' option to prepend a prefix to all reports in the junitxml file. -Bug fixes / Maintenance -++++++++++++++++++++++++++ +**Bug fixes** - make tests and the ``pytest_recwarn`` plugin in particular fully compatible to Python2.7 (if you use the ``recwarn`` funcarg warnings will be enabled so that @@ -1832,8 +1839,7 @@ Bug fixes / Maintenance v1.3.1 ============================================= -New features -++++++++++++++++++ +**New features** - issue91: introduce new py.test.xfail(reason) helper to imperatively mark a test as expected to fail. Can @@ -1871,8 +1877,7 @@ New features course requires that your application and tests are properly teared down and don't have global state. -Fixes / Maintenance -++++++++++++++++++++++ +**Bug Fixes** - improved traceback presentation: - improved and unified reporting for "--tb=short" option @@ -2108,7 +2113,7 @@ v1.1.0 - try harder to have deprecation warnings for py.compat.* accesses report a correct location -v1.0.2 +v1.0.3 ============================================= * adjust and improve docs diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 2aba6328a..a59b3c7e2 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -4,4 +4,4 @@ Changelog history ================================= -.. include:: ../../CHANGELOG +.. include:: ../../CHANGELOG.rst diff --git a/tox.ini b/tox.ini index 32336f20e..97b615833 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,9 @@ commands= py.test --genscript=pytest1 [testenv:flakes] basepython = python2.7 deps = flake8 + restructuredtext_lint commands = flake8 pytest.py _pytest testing + rst-lint CHANGELOG.rst [testenv:py27-xdist] deps=pytest-xdist>=1.13 From 3e34db50fba8b036db1508d5c7fa487be7eb2155 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Jan 2016 18:14:57 -0200 Subject: [PATCH 58/69] Rename "flakes" testenv to "linting" as requested in review --- .travis.yml | 2 +- CONTRIBUTING.rst | 4 ++-- appveyor.yml | 2 +- tox.ini | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3e301ca5..0fd2676ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ env: matrix: - TESTENV=coveralls - TESTENV=doctesting - - TESTENV=flakes + - TESTENV=linting - TESTENV=py26 - TESTENV=py27 - TESTENV=py27-cxfreeze diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4f5007a0a..2bee8b17e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -179,10 +179,10 @@ but here is a simple overview: You need to have Python 2.7 and 3.5 available in your system. Now running tests is as simple as issuing this command:: - $ python runtox.py -e py27,py35,flakes + $ python runtox.py -e linting,py27,py35 This command will run tests via the "tox" tool against Python 2.7 and 3.5 - and also perform "flakes" coding-style checks. ``runtox.py`` is + and also perform "lint" coding-style checks. ``runtox.py`` is a thin wrapper around ``tox`` which installs from a development package index where newer (not yet released to pypi) versions of dependencies (especially ``py``) might be present. diff --git a/appveyor.yml b/appveyor.yml index 73948b5b6..61bd389b0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,7 @@ build: false # Not a C# project, build stuff at the test step instead. test_script: - 'set TESTENVS= - flakes, + linting, py26, py27, py33, diff --git a/tox.ini b/tox.ini index 97b615833..747682905 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ minversion=2.0 distshare={homedir}/.tox/distshare envlist= - flakes,py26,py27,py33,py34,py35,pypy, + linting,py26,py27,py33,py34,py35,pypy, {py27,py35}-{pexpect,xdist,trial}, py27-nobyte,doctesting,py27-cxfreeze @@ -31,7 +31,7 @@ commands= [testenv:genscript] commands= py.test --genscript=pytest1 -[testenv:flakes] +[testenv:linting] basepython = python2.7 deps = flake8 restructuredtext_lint From 3daa0756ebcfd2a7b9475bf108bb243dde3226b6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Jan 2016 20:01:41 -0200 Subject: [PATCH 59/69] Add CHANGELOG.rst to MANIFEST and small format fix --- MANIFEST.in | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 9fc16c553..266a9184d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include CHANGELOG +include CHANGELOG.rst include LICENSE include AUTHORS diff --git a/tox.ini b/tox.ini index 747682905..d8472bc21 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ commands= py.test --genscript=pytest1 [testenv:linting] basepython = python2.7 deps = flake8 - restructuredtext_lint + restructuredtext_lint commands = flake8 pytest.py _pytest testing rst-lint CHANGELOG.rst From 3e5c9038ec33915aa67b6a6003ca11c9f8484b18 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 8 Jan 2016 10:58:26 +0100 Subject: [PATCH 60/69] Always show full comparison output if on CI. When you don't get enough information with a test running on a CI, it's quite frustrating, for various reasons: - It's more likely to be a flaky test, so you might not be able to reproduce the failure. - Passing -vv is quite bothersome (creating a temporary commit and reverting it) For those reasons, if something goes wrong on CI, it's good to have as much information as possible. --- CHANGELOG.rst | 5 +++++ _pytest/assertion/__init__.py | 13 +++++++++++-- testing/test_assertion.py | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30909034f..1fe341e4c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,10 @@ including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. Thanks `@nicoddemus`_ for the PR (`#1226`_). +* Comparisons now always show up in full when ``CI`` or ``BUILD_NUMBER`` is + found in the environment, even when -vv isn't used. + Thanks `@The-Compiler`_ for the PR. + **Bug Fixes** @@ -44,6 +48,7 @@ .. _@jab: https://github.com/jab .. _@codewarrior0: https://github.com/codewarrior0 .. _@jaraco: https://github.com/jaraco +.. _@The-Compiler: https://github.com/The-Compiler 2.8.6.dev1 diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index 54742347c..8e820dae2 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -2,6 +2,7 @@ support for presenting detailed information in failing assertions. """ import py +import os import sys from _pytest.monkeypatch import monkeypatch from _pytest.assertion import util @@ -86,6 +87,12 @@ def pytest_collection(session): hook.set_session(session) +def _running_on_ci(): + """Check if we're currently running on a CI system.""" + env_vars = ['CI', 'BUILD_NUMBER'] + return any(var in os.environ for var in env_vars) + + def pytest_runtest_setup(item): """Setup the pytest_assertrepr_compare hook @@ -99,7 +106,8 @@ def pytest_runtest_setup(item): This uses the first result from the hook and then ensures the following: - * Overly verbose explanations are dropped unles -vv was used. + * Overly verbose explanations are dropped unless -vv was used or + running on a CI. * Embedded newlines are escaped to help util.format_explanation() later. * If the rewrite mode is used embedded %-characters are replaced @@ -113,7 +121,8 @@ def pytest_runtest_setup(item): for new_expl in hook_result: if new_expl: if (sum(len(p) for p in new_expl[1:]) > 80*8 - and item.config.option.verbose < 2): + and item.config.option.verbose < 2 + and not _running_on_ci()): show_max = 10 truncated_lines = len(new_expl) - show_max new_expl[show_max:] = [py.builtin._totext( diff --git a/testing/test_assertion.py b/testing/test_assertion.py index cf715470a..34b900f64 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -405,7 +405,7 @@ def test_sequence_comparison_uses_repr(testdir): ]) -def test_assert_compare_truncate_longmessage(testdir): +def test_assert_compare_truncate_longmessage(monkeypatch, testdir): testdir.makepyfile(r""" def test_long(): a = list(range(200)) @@ -414,6 +414,7 @@ def test_assert_compare_truncate_longmessage(testdir): b = '\n'.join(map(str, b)) assert a == b """) + monkeypatch.delenv('CI', raising=False) result = testdir.runpytest() # without -vv, truncate the message showing a few diff lines only @@ -431,6 +432,12 @@ def test_assert_compare_truncate_longmessage(testdir): "*- 197", ]) + monkeypatch.setenv('CI', '1') + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*- 197", + ]) + def test_assertrepr_loaded_per_dir(testdir): testdir.makepyfile(test_base=['def test_base(): assert 1 == 2']) From a912d3745bcbbc49999dc9becf0d5545849d44ea Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Nov 2015 12:43:01 -0200 Subject: [PATCH 61/69] Moved py.code code over to py.test Fix #103 --- _pytest/_code/__init__.py | 17 + _pytest/_code/_assertionnew.py | 339 +++++++++ _pytest/_code/_assertionold.py | 561 ++++++++++++++ _pytest/_code/_py2traceback.py | 79 ++ _pytest/_code/assertion.py | 96 +++ _pytest/_code/code.py | 795 ++++++++++++++++++++ _pytest/_code/source.py | 421 +++++++++++ _pytest/assertion/reinterpret.py | 6 +- _pytest/assertion/util.py | 3 +- _pytest/config.py | 5 +- _pytest/doctest.py | 10 +- _pytest/main.py | 12 +- _pytest/pytester.py | 27 +- _pytest/python.py | 68 +- _pytest/recwarn.py | 4 +- _pytest/runner.py | 7 +- _pytest/skipping.py | 5 +- _pytest/unittest.py | 11 +- doc/en/assert.rst | 5 +- doc/en/example/assertion/failure_demo.py | 3 +- doc/en/example/multipython.py | 5 +- setup.py | 2 +- testing/acceptance_test.py | 7 +- testing/code/test_code.py | 163 ++++ testing/code/test_code_assertion.py | 309 ++++++++ testing/code/test_excinfo.py | 911 +++++++++++++++++++++++ testing/code/test_source.py | 657 ++++++++++++++++ testing/python/collect.py | 21 +- testing/python/fixture.py | 34 +- testing/python/integration.py | 3 +- testing/python/metafunc.py | 8 +- testing/python/raises.py | 3 +- testing/test_assertinterpret.py | 5 +- testing/test_assertion.py | 6 +- testing/test_assertrewrite.py | 7 +- testing/test_cache.py | 14 +- testing/test_capture.py | 4 +- testing/test_config.py | 9 +- testing/test_conftest.py | 17 +- testing/test_doctest.py | 8 +- testing/test_pdb.py | 7 +- testing/test_resultlog.py | 10 +- testing/test_runner.py | 16 +- testing/test_terminal.py | 14 +- testing/test_unittest.py | 3 +- 45 files changed, 4559 insertions(+), 158 deletions(-) create mode 100644 _pytest/_code/__init__.py create mode 100644 _pytest/_code/_assertionnew.py create mode 100644 _pytest/_code/_assertionold.py create mode 100644 _pytest/_code/_py2traceback.py create mode 100644 _pytest/_code/assertion.py create mode 100644 _pytest/_code/code.py create mode 100644 _pytest/_code/source.py create mode 100644 testing/code/test_code.py create mode 100644 testing/code/test_code_assertion.py create mode 100644 testing/code/test_excinfo.py create mode 100644 testing/code/test_source.py diff --git a/_pytest/_code/__init__.py b/_pytest/_code/__init__.py new file mode 100644 index 000000000..81b61e78a --- /dev/null +++ b/_pytest/_code/__init__.py @@ -0,0 +1,17 @@ +""" python inspection/code generation API """ +from .assertion import AssertionError as _AssertionError # noqa +from .assertion import _format_explanation # noqa +from .assertion import _reprcompare # noqa +from .assertion import reinterpret as _reinterpret # noqa +from .assertion import reinterpret_old as _reinterpret_old # noqa +from .code import Code # noqa +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/_assertionnew.py b/_pytest/_code/_assertionnew.py new file mode 100644 index 000000000..a3d807847 --- /dev/null +++ b/_pytest/_code/_assertionnew.py @@ -0,0 +1,339 @@ +""" +Find intermediate evalutation results in assert statements through builtin AST. +This should replace _assertionold.py eventually. +""" + +import ast +import sys + +from .assertion import _format_explanation, BuiltinAssertionError + +if sys.platform.startswith("java") and sys.version_info < (2, 5, 2): + # 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) + + +class Failure(Exception): + """Error found while interpreting AST.""" + + def __init__(self, explanation=""): + self.cause = sys.exc_info() + self.explanation = explanation + + +def interpret(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 --no-assert)") + +def run(offending_line, frame=None): + from .code import Frame + if frame is None: + frame = Frame(sys._getframe(1)) + return interpret(offending_line, frame) + +def getfailure(failure): + explanation = _format_explanation(failure.explanation) + value = failure.cause[1] + if str(value): + lines = explanation.splitlines() + if not lines: + lines.append("") + lines[0] += " << %s" % (value,) + explanation = "\n".join(lines) + text = "%s: %s" % (failure.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 = False + if not 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 result: + break + except KeyboardInterrupt: + raise + except: + break + left_explanation, left_result = next_explanation, next_result + + import _pytest._code + rcomp = _pytest._code._reprcompare + if rcomp: + res = rcomp(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) + 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) + arg_name = "__exprinfo_%s" % (len(ns),) + ns[arg_name] = arg_result + keyword_source = "%s=%%s" % (keyword.arg) + arguments.append(keyword_source % (arg_name,)) + arg_explanations.append(keyword_source % (arg_explanation,)) + if call.starargs: + 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 call.kwargs: + 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: + 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 = True + if 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) + if test_explanation.startswith("False\n{False =") and \ + test_explanation.endswith("\n"): + test_explanation = test_explanation[15:-2] + explanation = "assert %s" % (test_explanation,) + if not test_result: + try: + raise 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/_code/_assertionold.py b/_pytest/_code/_assertionold.py new file mode 100644 index 000000000..32492b25a --- /dev/null +++ b/_pytest/_code/_assertionold.py @@ -0,0 +1,561 @@ +import inspect +import sys + +from compiler import parse, ast, pycodegen + +import py +from _pytest._code.assertion import BuiltinAssertionError, _format_explanation + +passthroughex = py.builtin._sysex + +class Failure: + def __init__(self, node): + self.exc, self.value, self.tb = sys.exc_info() + self.node = node + +class View(object): + """View base class. + + If C is a subclass of View, then C(x) creates a proxy object around + the object x. The actual class of the proxy is not C in general, + but a *subclass* of C determined by the rules below. To avoid confusion + we call view class the class of the proxy (a subclass of C, so of View) + and object class the class of x. + + Attributes and methods not found in the proxy are automatically read on x. + Other operations like setting attributes are performed on the proxy, as + determined by its view class. The object x is available from the proxy + as its __obj__ attribute. + + The view class selection is determined by the __view__ tuples and the + optional __viewkey__ method. By default, the selected view class is the + most specific subclass of C whose __view__ mentions the class of x. + If no such subclass is found, the search proceeds with the parent + object classes. For example, C(True) will first look for a subclass + of C with __view__ = (..., bool, ...) and only if it doesn't find any + look for one with __view__ = (..., int, ...), and then ..., object,... + If everything fails the class C itself is considered to be the default. + + Alternatively, the view class selection can be driven by another aspect + of the object x, instead of the class of x, by overriding __viewkey__. + See last example at the end of this module. + """ + + _viewcache = {} + __view__ = () + + def __new__(rootclass, obj, *args, **kwds): + self = object.__new__(rootclass) + self.__obj__ = obj + self.__rootclass__ = rootclass + key = self.__viewkey__() + try: + self.__class__ = self._viewcache[key] + except KeyError: + self.__class__ = self._selectsubclass(key) + return self + + def __getattr__(self, attr): + # attributes not found in the normal hierarchy rooted on View + # are looked up in the object's real class + return getattr(self.__obj__, attr) + + def __viewkey__(self): + return self.__obj__.__class__ + + def __matchkey__(self, key, subclasses): + if inspect.isclass(key): + keys = inspect.getmro(key) + else: + keys = [key] + for key in keys: + result = [C for C in subclasses if key in C.__view__] + if result: + return result + return [] + + def _selectsubclass(self, key): + subclasses = list(enumsubclasses(self.__rootclass__)) + for C in subclasses: + if not isinstance(C.__view__, tuple): + C.__view__ = (C.__view__,) + choices = self.__matchkey__(key, subclasses) + if not choices: + return self.__rootclass__ + elif len(choices) == 1: + return choices[0] + else: + # combine the multiple choices + return type('?', tuple(choices), {}) + + def __repr__(self): + return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) + + +def enumsubclasses(cls): + for subcls in cls.__subclasses__(): + for subsubclass in enumsubclasses(subcls): + yield subsubclass + yield cls + + +class Interpretable(View): + """A parse tree node with a few extra methods.""" + explanation = None + + def is_builtin(self, frame): + return False + + def eval(self, frame): + # fall-back for unknown expression nodes + try: + expr = ast.Expression(self.__obj__) + expr.filename = '' + self.__obj__.filename = '' + co = pycodegen.ExpressionCodeGenerator(expr).getCode() + result = frame.eval(co) + except passthroughex: + raise + except: + raise Failure(self) + self.result = result + self.explanation = self.explanation or frame.repr(self.result) + + def run(self, frame): + # fall-back for unknown statement nodes + try: + expr = ast.Module(None, ast.Stmt([self.__obj__])) + expr.filename = '' + co = pycodegen.ModuleCodeGenerator(expr).getCode() + frame.exec_(co) + except passthroughex: + raise + except: + raise Failure(self) + + def nice_explanation(self): + return _format_explanation(self.explanation) + + +class Name(Interpretable): + __view__ = ast.Name + + def is_local(self, frame): + source = '%r in locals() is not globals()' % self.name + try: + return frame.is_true(frame.eval(source)) + except passthroughex: + raise + except: + return False + + def is_global(self, frame): + source = '%r in globals()' % self.name + try: + return frame.is_true(frame.eval(source)) + except passthroughex: + raise + except: + return False + + def is_builtin(self, frame): + source = '%r not in locals() and %r not in globals()' % ( + self.name, self.name) + try: + return frame.is_true(frame.eval(source)) + except passthroughex: + raise + except: + return False + + def eval(self, frame): + super(Name, self).eval(frame) + if not self.is_local(frame): + self.explanation = self.name + +class Compare(Interpretable): + __view__ = ast.Compare + + def eval(self, frame): + expr = Interpretable(self.expr) + expr.eval(frame) + for operation, expr2 in self.ops: + if hasattr(self, 'result'): + # shortcutting in chained expressions + if not frame.is_true(self.result): + break + expr2 = Interpretable(expr2) + expr2.eval(frame) + self.explanation = "%s %s %s" % ( + expr.explanation, operation, expr2.explanation) + source = "__exprinfo_left %s __exprinfo_right" % operation + try: + self.result = frame.eval(source, + __exprinfo_left=expr.result, + __exprinfo_right=expr2.result) + except passthroughex: + raise + except: + raise Failure(self) + expr = expr2 + +class And(Interpretable): + __view__ = ast.And + + def eval(self, frame): + explanations = [] + for expr in self.nodes: + expr = Interpretable(expr) + expr.eval(frame) + explanations.append(expr.explanation) + self.result = expr.result + if not frame.is_true(expr.result): + break + self.explanation = '(' + ' and '.join(explanations) + ')' + +class Or(Interpretable): + __view__ = ast.Or + + def eval(self, frame): + explanations = [] + for expr in self.nodes: + expr = Interpretable(expr) + expr.eval(frame) + explanations.append(expr.explanation) + self.result = expr.result + if frame.is_true(expr.result): + break + self.explanation = '(' + ' or '.join(explanations) + ')' + + +# == Unary operations == +keepalive = [] +for astclass, astpattern in { + ast.Not : 'not __exprinfo_expr', + ast.Invert : '(~__exprinfo_expr)', + }.items(): + + class UnaryArith(Interpretable): + __view__ = astclass + + def eval(self, frame, astpattern=astpattern): + expr = Interpretable(self.expr) + expr.eval(frame) + self.explanation = astpattern.replace('__exprinfo_expr', + expr.explanation) + try: + self.result = frame.eval(astpattern, + __exprinfo_expr=expr.result) + except passthroughex: + raise + except: + raise Failure(self) + + keepalive.append(UnaryArith) + +# == Binary operations == +for astclass, astpattern in { + ast.Add : '(__exprinfo_left + __exprinfo_right)', + ast.Sub : '(__exprinfo_left - __exprinfo_right)', + ast.Mul : '(__exprinfo_left * __exprinfo_right)', + ast.Div : '(__exprinfo_left / __exprinfo_right)', + ast.Mod : '(__exprinfo_left % __exprinfo_right)', + ast.Power : '(__exprinfo_left ** __exprinfo_right)', + }.items(): + + class BinaryArith(Interpretable): + __view__ = astclass + + def eval(self, frame, astpattern=astpattern): + left = Interpretable(self.left) + left.eval(frame) + right = Interpretable(self.right) + right.eval(frame) + self.explanation = (astpattern + .replace('__exprinfo_left', left .explanation) + .replace('__exprinfo_right', right.explanation)) + try: + self.result = frame.eval(astpattern, + __exprinfo_left=left.result, + __exprinfo_right=right.result) + except passthroughex: + raise + except: + raise Failure(self) + + keepalive.append(BinaryArith) + + +class CallFunc(Interpretable): + __view__ = ast.CallFunc + + def is_bool(self, frame): + source = 'isinstance(__exprinfo_value, bool)' + try: + return frame.is_true(frame.eval(source, + __exprinfo_value=self.result)) + except passthroughex: + raise + except: + return False + + def eval(self, frame): + node = Interpretable(self.node) + node.eval(frame) + explanations = [] + vars = {'__exprinfo_fn': node.result} + source = '__exprinfo_fn(' + for a in self.args: + if isinstance(a, ast.Keyword): + keyword = a.name + a = a.expr + else: + keyword = None + a = Interpretable(a) + a.eval(frame) + argname = '__exprinfo_%d' % len(vars) + vars[argname] = a.result + if keyword is None: + source += argname + ',' + explanations.append(a.explanation) + else: + source += '%s=%s,' % (keyword, argname) + explanations.append('%s=%s' % (keyword, a.explanation)) + if self.star_args: + star_args = Interpretable(self.star_args) + star_args.eval(frame) + argname = '__exprinfo_star' + vars[argname] = star_args.result + source += '*' + argname + ',' + explanations.append('*' + star_args.explanation) + if self.dstar_args: + dstar_args = Interpretable(self.dstar_args) + dstar_args.eval(frame) + argname = '__exprinfo_kwds' + vars[argname] = dstar_args.result + source += '**' + argname + ',' + explanations.append('**' + dstar_args.explanation) + self.explanation = "%s(%s)" % ( + node.explanation, ', '.join(explanations)) + if source.endswith(','): + source = source[:-1] + source += ')' + try: + self.result = frame.eval(source, **vars) + except passthroughex: + raise + except: + raise Failure(self) + if not node.is_builtin(frame) or not self.is_bool(frame): + r = frame.repr(self.result) + self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) + +class Getattr(Interpretable): + __view__ = ast.Getattr + + def eval(self, frame): + expr = Interpretable(self.expr) + expr.eval(frame) + source = '__exprinfo_expr.%s' % self.attrname + try: + self.result = frame.eval(source, __exprinfo_expr=expr.result) + except passthroughex: + raise + except: + raise Failure(self) + self.explanation = '%s.%s' % (expr.explanation, self.attrname) + # if the attribute comes from the instance, its value is interesting + source = ('hasattr(__exprinfo_expr, "__dict__") and ' + '%r in __exprinfo_expr.__dict__' % self.attrname) + try: + from_instance = frame.is_true( + frame.eval(source, __exprinfo_expr=expr.result)) + except passthroughex: + raise + except: + from_instance = True + if from_instance: + r = frame.repr(self.result) + self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) + +# == Re-interpretation of full statements == + +class Assert(Interpretable): + __view__ = ast.Assert + + def run(self, frame): + test = Interpretable(self.test) + test.eval(frame) + # simplify 'assert False where False = ...' + if (test.explanation.startswith('False\n{False = ') and + test.explanation.endswith('\n}')): + test.explanation = test.explanation[15:-2] + # print the result as 'assert ' + self.result = test.result + self.explanation = 'assert ' + test.explanation + if not frame.is_true(test.result): + try: + raise BuiltinAssertionError + except passthroughex: + raise + except: + raise Failure(self) + +class Assign(Interpretable): + __view__ = ast.Assign + + def run(self, frame): + expr = Interpretable(self.expr) + expr.eval(frame) + self.result = expr.result + self.explanation = '... = ' + expr.explanation + # fall-back-run the rest of the assignment + ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr')) + mod = ast.Module(None, ast.Stmt([ass])) + mod.filename = '' + co = pycodegen.ModuleCodeGenerator(mod).getCode() + try: + frame.exec_(co, __exprinfo_expr=expr.result) + except passthroughex: + raise + except: + raise Failure(self) + +class Discard(Interpretable): + __view__ = ast.Discard + + def run(self, frame): + expr = Interpretable(self.expr) + expr.eval(frame) + self.result = expr.result + self.explanation = expr.explanation + +class Stmt(Interpretable): + __view__ = ast.Stmt + + def run(self, frame): + for stmt in self.nodes: + stmt = Interpretable(stmt) + stmt.run(frame) + + +def report_failure(e): + explanation = e.node.nice_explanation() + if explanation: + explanation = ", in: " + explanation + else: + explanation = "" + sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation)) + +def check(s, frame=None): + from _pytest._code import Frame + if frame is None: + frame = sys._getframe(1) + frame = Frame(frame) + expr = parse(s, 'eval') + assert isinstance(expr, ast.Expression) + node = Interpretable(expr.node) + try: + node.eval(frame) + except passthroughex: + raise + except Failure: + e = sys.exc_info()[1] + report_failure(e) + else: + if not frame.is_true(node.result): + sys.stderr.write("assertion failed: %s\n" % node.nice_explanation()) + + +########################################################### +# API / Entry points +# ######################################################### + +def interpret(source, frame, should_fail=False): + module = Interpretable(parse(source, 'exec').node) + import _pytest._code + if isinstance(frame, py.std.types.FrameType): + frame = _pytest._code.Frame(frame) + try: + module.run(frame) + except Failure: + e = sys.exc_info()[1] + return getfailure(e) + except passthroughex: + raise + except: + import traceback + traceback.print_exc() + 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 --nomagic)") + else: + return None + +def getmsg(excinfo): + import _pytest._code + if isinstance(excinfo, tuple): + excinfo = _pytest._code.ExceptionInfo(excinfo) + #frame, line = gettbline(tb) + #frame = pytest.code.Frame(frame) + #return interpret(line, frame) + + tb = excinfo.traceback[-1] + source = str(tb.statement).strip() + x = interpret(source, tb.frame, should_fail=True) + if not isinstance(x, str): + raise TypeError("interpret returned non-string %r" % (x,)) + return x + +def getfailure(e): + explanation = e.node.nice_explanation() + if str(e.value): + lines = explanation.split('\n') + lines[0] += " << %s" % (e.value,) + explanation = '\n'.join(lines) + text = "%s: %s" % (e.exc.__name__, explanation) + if text.startswith('AssertionError: assert '): + text = text[16:] + return text + +def run(s, frame=None): + import _pytest._code + if frame is None: + frame = sys._getframe(1) + frame = _pytest._code.Frame(frame) + module = Interpretable(parse(s, 'exec').node) + try: + module.run(frame) + except Failure: + e = sys.exc_info()[1] + report_failure(e) + + +if __name__ == '__main__': + # example: + def f(): + return 5 + def g(): + return 3 + def h(x): + return 'never' + check("f() * g() == 5") + check("not f()") + check("not (f() and g() or 0)") + check("f() == g()") + i = 4 + check("i == f()") + check("len(f()) == 0") + check("isinstance(2+3+4, float)") + + run("x = i") + check("x == 5") + + run("assert not f(), 'oops'") + run("a, b, c = 1, 2") + run("a, b, c = f()") + + check("max([f(),g()]) == 4") + check("'hello'[g()] == 'h'") + run("'guk%d' % h(f())") diff --git a/_pytest/_code/_py2traceback.py b/_pytest/_code/_py2traceback.py new file mode 100644 index 000000000..d65e27cb7 --- /dev/null +++ b/_pytest/_code/_py2traceback.py @@ -0,0 +1,79 @@ +# copied from python-2.7.3's traceback.py +# CHANGES: +# - some_str is replaced, trying to create unicode strings +# +import types + +def format_exception_only(etype, value): + """Format the exception part of a traceback. + + The arguments are the exception type and value such as given by + sys.last_type and sys.last_value. The return value is a list of + strings, each ending in a newline. + + Normally, the list contains a single string; however, for + SyntaxError exceptions, it contains several lines that (when + printed) display detailed information about where the syntax + error occurred. + + The message indicating which exception occurred is always the last + string in the list. + + """ + + # An instance should not have a meaningful value parameter, but + # sometimes does, particularly for string exceptions, such as + # >>> raise string1, string2 # deprecated + # + # Clear these out first because issubtype(string1, SyntaxError) + # would throw another exception and mask the original problem. + if (isinstance(etype, BaseException) or + isinstance(etype, types.InstanceType) or + etype is None or type(etype) is str): + return [_format_final_exc_line(etype, value)] + + stype = etype.__name__ + + if not issubclass(etype, SyntaxError): + return [_format_final_exc_line(stype, value)] + + # It was a syntax error; show exactly where the problem was found. + lines = [] + try: + msg, (filename, lineno, offset, badline) = value.args + except Exception: + pass + else: + filename = filename or "" + lines.append(' File "%s", line %d\n' % (filename, lineno)) + if badline is not None: + lines.append(' %s\n' % badline.strip()) + if offset is not None: + caretspace = badline.rstrip('\n')[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c.isspace() and c or ' ') for c in caretspace) + # only three spaces to account for offset1 == pos 0 + lines.append(' %s^\n' % ''.join(caretspace)) + value = msg + + lines.append(_format_final_exc_line(stype, value)) + return lines + +def _format_final_exc_line(etype, value): + """Return a list of a single line -- normal case for format_exception_only""" + valuestr = _some_str(value) + if value is None or not valuestr: + line = "%s\n" % etype + else: + line = "%s: %s\n" % (etype, valuestr) + return line + +def _some_str(value): + try: + return unicode(value) + except Exception: + try: + return str(value) + except Exception: + pass + return '' % type(value).__name__ diff --git a/_pytest/_code/assertion.py b/_pytest/_code/assertion.py new file mode 100644 index 000000000..f5e0d65c4 --- /dev/null +++ b/_pytest/_code/assertion.py @@ -0,0 +1,96 @@ +import sys + +import py +from .code import Frame + +BuiltinAssertionError = py.builtin.builtins.AssertionError + +_reprcompare = None # if set, will be called by assert reinterp for comparison ops + +def _format_explanation(explanation): + """This formats an explanation + + Normally all embedded newlines are escaped, however there are + three exceptions: \n{, \n} and \n~. The first two are intended + cover nested explanations, see function and attribute explanations + for examples (.visit_Call(), visit_Attribute()). The last one is + for when one explanation needs to span multiple lines, e.g. when + displaying diffs. + """ + raw_lines = (explanation or '').split('\n') + # escape newlines not followed by {, } and ~ + lines = [raw_lines[0]] + for l in raw_lines[1:]: + if l.startswith('{') or l.startswith('}') or l.startswith('~'): + lines.append(l) + else: + lines[-1] += '\\n' + l + + result = lines[:1] + stack = [0] + stackcnt = [0] + for line in lines[1:]: + if line.startswith('{'): + if stackcnt[-1]: + s = 'and ' + else: + s = 'where ' + stack.append(len(result)) + stackcnt[-1] += 1 + stackcnt.append(0) + result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) + elif line.startswith('}'): + assert line.startswith('}') + stack.pop() + stackcnt.pop() + result[stack[-1]] += line[1:] + else: + assert line.startswith('~') + result.append(' '*len(stack) + line[1:]) + assert len(stack) == 1 + return '\n'.join(result) + + +class AssertionError(BuiltinAssertionError): + def __init__(self, *args): + BuiltinAssertionError.__init__(self, *args) + if args: + try: + self.msg = str(args[0]) + except py.builtin._sysex: + raise + except: + self.msg = "<[broken __repr__] %s at %0xd>" %( + args[0].__class__, id(args[0])) + else: + f = 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" + reinterpret_old = "old reinterpretation not available for py3" +else: + from _pytest._code._assertionold import interpret as reinterpret_old +if sys.version_info >= (2, 6) or (sys.platform.startswith("java")): + from _pytest._code._assertionnew import interpret as reinterpret +else: + reinterpret = reinterpret_old + diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py new file mode 100644 index 000000000..c705249ba --- /dev/null +++ b/_pytest/_code/code.py @@ -0,0 +1,795 @@ +import sys +from inspect import CO_VARARGS, CO_VARKEYWORDS + +import py + +builtin_repr = repr + +reprlib = py.builtin._tryimport('repr', 'reprlib') + +if sys.version_info[0] >= 3: + from traceback import format_exception_only +else: + from ._py2traceback import format_exception_only + +class Code(object): + """ wrapper around Python code objects """ + def __init__(self, rawcode): + if not hasattr(rawcode, "co_filename"): + rawcode = getrawcode(rawcode) + try: + self.filename = rawcode.co_filename + self.firstlineno = rawcode.co_firstlineno - 1 + self.name = rawcode.co_name + except AttributeError: + raise TypeError("not a code object: %r" %(rawcode,)) + self.raw = rawcode + + def __eq__(self, other): + return self.raw == other.raw + + def __ne__(self, other): + return not self == other + + @property + def path(self): + """ return a path object pointing to source code (note that it + might not point to an actually existing file). """ + p = py.path.local(self.raw.co_filename) + # maybe don't try this checking + if not p.check(): + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? + p = self.raw.co_filename + return p + + @property + def fullsource(self): + """ return a _pytest._code.Source object for the full source file of the code + """ + from _pytest._code import source + full, _ = source.findsource(self.raw) + return full + + def source(self): + """ return a _pytest._code.Source object for the code object's source only + """ + # return source only for that part of code + import _pytest._code + return _pytest._code.Source(self.raw) + + def getargs(self, var=False): + """ return a tuple with the argument names for the code object + + if 'var' is set True also return the names of the variable and + keyword arguments when present + """ + # handfull shortcut for getting args + raw = self.raw + argcount = raw.co_argcount + if var: + argcount += raw.co_flags & CO_VARARGS + argcount += raw.co_flags & CO_VARKEYWORDS + return raw.co_varnames[:argcount] + +class Frame(object): + """Wrapper around a Python frame holding f_locals and f_globals + in which expressions can be evaluated.""" + + def __init__(self, frame): + self.lineno = frame.f_lineno - 1 + self.f_globals = frame.f_globals + self.f_locals = frame.f_locals + self.raw = frame + self.code = Code(frame.f_code) + + @property + def statement(self): + """ statement this frame is at """ + import _pytest._code + if self.code.fullsource is None: + return _pytest._code.Source("") + return self.code.fullsource.getstatement(self.lineno) + + def eval(self, code, **vars): + """ evaluate 'code' in the frame + + 'vars' are optional additional local variables + + returns the result of the evaluation + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + return eval(code, self.f_globals, f_locals) + + def exec_(self, code, **vars): + """ exec 'code' in the frame + + 'vars' are optiona; additional local variables + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + py.builtin.exec_(code, self.f_globals, f_locals ) + + def repr(self, object): + """ return a 'safe' (non-recursive, one-line) string repr for 'object' + """ + return py.io.saferepr(object) + + def is_true(self, object): + return object + + def getargs(self, var=False): + """ return a list of tuples (name, value) for all arguments + + if 'var' is set True also include the variable and keyword + arguments when present + """ + retval = [] + for arg in self.code.getargs(var): + try: + retval.append((arg, self.f_locals[arg])) + except KeyError: + pass # this can occur when using Psyco + return retval + +class TracebackEntry(object): + """ a single entry in a traceback """ + + _repr_style = None + exprinfo = None + + def __init__(self, rawentry): + self._rawentry = rawentry + self.lineno = rawentry.tb_lineno - 1 + + def set_repr_style(self, mode): + assert mode in ("short", "long") + self._repr_style = mode + + @property + def frame(self): + import _pytest._code + return _pytest._code.Frame(self._rawentry.tb_frame) + + @property + def relline(self): + return self.lineno - self.frame.code.firstlineno + + def __repr__(self): + return "" %(self.frame.code.path, self.lineno+1) + + @property + def statement(self): + """ _pytest._code.Source object for the current statement """ + source = self.frame.code.fullsource + return source.getstatement(self.lineno) + + @property + def path(self): + """ path to the source code """ + return self.frame.code.path + + def getlocals(self): + 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.""" + import _pytest._code + if self.exprinfo is None: + source = str(self.statement).strip() + x = _pytest._code._reinterpret(source, self.frame, should_fail=True) + if not isinstance(x, str): + 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) + + def getsource(self, astcache=None): + """ return failing source code. """ + # we use the passed in astcache to not reparse asttrees + # within exception info printing + from _pytest._code.source import getstatementrange_ast + source = self.frame.code.fullsource + if source is None: + return None + key = astnode = None + if astcache is not None: + key = self.frame.code.path + if key is not None: + astnode = astcache.get(key, None) + start = self.getfirstlinesource() + try: + astnode, _, end = getstatementrange_ast(self.lineno, source, + astnode=astnode) + except SyntaxError: + end = self.lineno + 1 + else: + if key is not None: + astcache[key] = astnode + return source[start:end] + + source = property(getsource) + + def ishidden(self): + """ return True if the current frame has a var __tracebackhide__ + resolving to True + + mostly for internal use + """ + try: + return self.frame.f_locals['__tracebackhide__'] + except KeyError: + try: + return self.frame.f_globals['__tracebackhide__'] + except KeyError: + return False + + def __str__(self): + try: + fn = str(self.path) + except py.error.Error: + fn = '???' + name = self.frame.code.name + try: + line = str(self.statement).lstrip() + except KeyboardInterrupt: + raise + except: + line = "???" + return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line) + + def name(self): + return self.frame.code.raw.co_name + name = property(name, None, None, "co_name of underlaying code") + +class Traceback(list): + """ Traceback objects encapsulate and offer higher level + access to Traceback entries. + """ + Entry = TracebackEntry + def __init__(self, tb): + """ initialize from given python traceback object. """ + if hasattr(tb, 'tb_next'): + def f(cur): + while cur is not None: + yield self.Entry(cur) + cur = cur.tb_next + list.__init__(self, f(tb)) + else: + list.__init__(self, tb) + + def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): + """ return a Traceback instance wrapping part of this Traceback + + by provding any combination of path, lineno and firstlineno, the + first frame to start the to-be-returned traceback is determined + + this allows cutting the first part of a Traceback instance e.g. + for formatting reasons (removing some uninteresting bits that deal + with handling of the exception/traceback) + """ + for x in self: + code = x.frame.code + codepath = code.path + if ((path is None or codepath == path) and + (excludepath is None or not hasattr(codepath, 'relto') or + not codepath.relto(excludepath)) and + (lineno is None or x.lineno == lineno) and + (firstlineno is None or x.frame.code.firstlineno == firstlineno)): + return Traceback(x._rawentry) + return self + + def __getitem__(self, key): + val = super(Traceback, self).__getitem__(key) + if isinstance(key, type(slice(0))): + val = self.__class__(val) + return val + + def filter(self, fn=lambda x: not x.ishidden()): + """ return a Traceback instance with certain items removed + + fn is a function that gets a single argument, a TracebackItem + instance, and should return True when the item should be added + to the Traceback, False when not + + by default this removes all the TracebackItems which are hidden + (see ishidden() above) + """ + return Traceback(filter(fn, self)) + + def getcrashentry(self): + """ return last non-hidden traceback entry that lead + to the exception of a traceback. + """ + for i in range(-1, -len(self)-1, -1): + entry = self[i] + if not entry.ishidden(): + return entry + return self[-1] + + def recursionindex(self): + """ return the index of the frame/TracebackItem where recursion + originates if appropriate, None if no recursion occurred + """ + cache = {} + for i, entry in enumerate(self): + # id for the code.raw is needed to work around + # the strange metaprogramming in the decorator lib from pypi + # which generates code objects that have hash/value equality + #XXX needs a test + key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno + #print "checking for recursion at", key + l = cache.setdefault(key, []) + if l: + f = entry.frame + loc = f.f_locals + for otherloc in l: + if f.is_true(f.eval(co_equal, + __recursioncache_locals_1=loc, + __recursioncache_locals_2=otherloc)): + return i + l.append(entry.frame.f_locals) + return None + +co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', + '?', 'eval') + +class ExceptionInfo(object): + """ wraps sys.exc_info() objects and offers + help for navigating the traceback. + """ + _striptext = '' + def __init__(self, tup=None, exprinfo=None): + import _pytest._code + if tup is None: + tup = sys.exc_info() + if exprinfo is None and isinstance(tup[1], AssertionError): + exprinfo = getattr(tup[1], 'msg', None) + if exprinfo is None: + exprinfo = str(tup[1]) + if exprinfo and exprinfo.startswith('assert '): + self._striptext = 'AssertionError: ' + self._excinfo = tup + #: the exception class + self.type = tup[0] + #: the exception instance + self.value = tup[1] + #: the exception raw traceback + self.tb = tup[2] + #: the exception type name + self.typename = self.type.__name__ + #: the exception traceback (_pytest._code.Traceback instance) + self.traceback = _pytest._code.Traceback(self.tb) + + def __repr__(self): + return "" % (self.typename, len(self.traceback)) + + def exconly(self, tryshort=False): + """ return the exception as a string + + when 'tryshort' resolves to True, and the exception is a + _pytest._code._AssertionError, only the actual exception part of + the exception representation is returned (so 'AssertionError: ' is + removed from the beginning) + """ + lines = format_exception_only(self.type, self.value) + text = ''.join(lines) + text = text.rstrip() + if tryshort: + if text.startswith(self._striptext): + text = text[len(self._striptext):] + return text + + def errisinstance(self, exc): + """ return True if the exception is an instance of exc """ + return isinstance(self.value, exc) + + def _getreprcrash(self): + exconly = self.exconly(tryshort=True) + entry = self.traceback.getcrashentry() + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + return ReprFileLocation(path, lineno+1, exconly) + + def getrepr(self, showlocals=False, style="long", + abspath=False, tbfilter=True, funcargs=False): + """ return str()able representation of this exception info. + showlocals: show locals per traceback entry + style: long|short|no|native traceback style + tbfilter: hide entries (where __tracebackhide__ is true) + + in case of style==native, tbfilter and showlocals is ignored. + """ + if style == 'native': + return ReprExceptionInfo(ReprTracebackNative( + py.std.traceback.format_exception( + self.type, + self.value, + self.traceback[0]._rawentry, + )), self._getreprcrash()) + + fmt = FormattedExcinfo(showlocals=showlocals, style=style, + abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) + return fmt.repr_excinfo(self) + + def __str__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return str(loc) + + def __unicode__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return unicode(loc) + + +class FormattedExcinfo(object): + """ presenting information about failing Functions and Generators. """ + # for traceback entries + flow_marker = ">" + fail_marker = "E" + + def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True, funcargs=False): + self.showlocals = showlocals + self.style = style + self.tbfilter = tbfilter + self.funcargs = funcargs + self.abspath = abspath + self.astcache = {} + + def _getindent(self, source): + # figure out indent for given source + try: + s = str(source.getstatement(len(source)-1)) + except KeyboardInterrupt: + raise + except: + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise + except: + return 0 + return 4 + (len(s) - len(s.lstrip())) + + def _getentrysource(self, entry): + source = entry.getsource(self.astcache) + if source is not None: + source = source.deindent() + return source + + def _saferepr(self, obj): + return py.io.saferepr(obj) + + def repr_args(self, entry): + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(var=True): + args.append((argname, self._saferepr(argvalue))) + return ReprFuncArgs(args) + + def get_source(self, source, line_index=-1, excinfo=None, short=False): + """ return formatted and marked up source lines. """ + import _pytest._code + lines = [] + if source is None or line_index >= len(source.lines): + source = _pytest._code.Source("???") + line_index = 0 + if line_index < 0: + line_index += len(source) + space_prefix = " " + if short: + lines.append(space_prefix + source.lines[line_index].strip()) + else: + for line in source.lines[:line_index]: + lines.append(space_prefix + line) + lines.append(self.flow_marker + " " + source.lines[line_index]) + for line in source.lines[line_index+1:]: + lines.append(space_prefix + line) + if excinfo is not None: + indent = 4 if short else self._getindent(source) + lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) + return lines + + def get_exconly(self, excinfo, indent=4, markall=False): + lines = [] + indent = " " * indent + # get the real exception information out + exlines = excinfo.exconly(tryshort=True).split('\n') + failindent = self.fail_marker + indent[1:] + for line in exlines: + lines.append(failindent + line) + if not markall: + failindent = indent + return lines + + def repr_locals(self, locals): + if self.showlocals: + lines = [] + keys = [loc for loc in locals if loc[0] != "@"] + keys.sort() + for name in keys: + value = locals[name] + if name == '__builtins__': + lines.append("__builtins__ = ") + else: + # This formatting could all be handled by the + # _repr() function, which is only reprlib.Repr in + # disguise, so is very configurable. + str_repr = self._saferepr(value) + #if len(str_repr) < 70 or not isinstance(value, + # (list, tuple, dict)): + lines.append("%-10s = %s" %(name, str_repr)) + #else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # py.std.pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) + + def repr_traceback_entry(self, entry, excinfo=None): + import _pytest._code + source = self._getentrysource(entry) + if source is None: + source = _pytest._code.Source("???") + line_index = 0 + else: + # entry.getfirstlinesource() can be -1, should be 0 on jython + line_index = entry.lineno - max(entry.getfirstlinesource(), 0) + + lines = [] + style = entry._repr_style + if style is None: + style = self.style + if style in ("short", "long"): + short = style == "short" + reprargs = self.repr_args(entry) if not short else None + s = self.get_source(source, line_index, excinfo, short=short) + lines.extend(s) + if short: + message = "in %s" %(entry.name) + else: + message = excinfo and excinfo.typename or "" + path = self._makepath(entry.path) + filelocrepr = ReprFileLocation(path, entry.lineno+1, message) + localsrepr = None + if not short: + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, style) + + def _makepath(self, path): + if not self.abspath: + try: + np = py.path.local().bestrelpath(path) + except OSError: + return path + if len(np) < len(str(path)): + path = np + return path + + def repr_traceback(self, excinfo): + traceback = excinfo.traceback + if self.tbfilter: + traceback = traceback.filter() + recursionindex = None + if excinfo.errisinstance(RuntimeError): + if "maximum recursion depth exceeded" in str(excinfo.value): + recursionindex = traceback.recursionindex() + last = traceback[-1] + entries = [] + extraline = None + for index, entry in enumerate(traceback): + einfo = (last == entry) and excinfo or None + reprentry = self.repr_traceback_entry(entry, einfo) + entries.append(reprentry) + if index == recursionindex: + extraline = "!!! Recursion detected (same locals & position)" + break + return ReprTraceback(entries, extraline, style=self.style) + + def repr_excinfo(self, excinfo): + reprtraceback = self.repr_traceback(excinfo) + reprcrash = excinfo._getreprcrash() + return ReprExceptionInfo(reprtraceback, reprcrash) + +class TerminalRepr: + def __str__(self): + s = self.__unicode__() + if sys.version_info[0] < 3: + s = s.encode('utf-8') + return s + + def __unicode__(self): + # FYI this is called from pytest-xdist's serialization of exception + # information. + io = py.io.TextIO() + tw = py.io.TerminalWriter(file=io) + self.toterminal(tw) + return io.getvalue().strip() + + def __repr__(self): + return "<%s instance at %0x>" %(self.__class__, id(self)) + + +class ReprExceptionInfo(TerminalRepr): + def __init__(self, reprtraceback, reprcrash): + self.reprtraceback = reprtraceback + self.reprcrash = reprcrash + self.sections = [] + + def addsection(self, name, content, sep="-"): + self.sections.append((name, content, sep)) + + def toterminal(self, tw): + self.reprtraceback.toterminal(tw) + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + +class ReprTraceback(TerminalRepr): + entrysep = "_ " + + def __init__(self, reprentries, extraline, style): + self.reprentries = reprentries + self.extraline = extraline + self.style = style + + def toterminal(self, tw): + # the entries might have different styles + for i, entry in enumerate(self.reprentries): + if entry.style == "long": + tw.line("") + entry.toterminal(tw) + if i < len(self.reprentries) - 1: + next_entry = self.reprentries[i+1] + if entry.style == "long" or \ + entry.style == "short" and next_entry.style == "long": + tw.sep(self.entrysep) + + if self.extraline: + tw.line(self.extraline) + +class ReprTracebackNative(ReprTraceback): + def __init__(self, tblines): + self.style = "native" + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = None + +class ReprEntryNative(TerminalRepr): + style = "native" + + def __init__(self, tblines): + self.lines = tblines + + def toterminal(self, tw): + tw.write("".join(self.lines)) + +class ReprEntry(TerminalRepr): + localssep = "_ " + + def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): + self.lines = lines + self.reprfuncargs = reprfuncargs + self.reprlocals = reprlocals + self.reprfileloc = filelocrepr + self.style = style + + def toterminal(self, tw): + if self.style == "short": + self.reprfileloc.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + #tw.line("") + return + if self.reprfuncargs: + self.reprfuncargs.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + if self.reprlocals: + #tw.sep(self.localssep, "Locals") + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + if self.lines: + tw.line("") + self.reprfileloc.toterminal(tw) + + def __str__(self): + return "%s\n%s\n%s" % ("\n".join(self.lines), + self.reprlocals, + self.reprfileloc) + +class ReprFileLocation(TerminalRepr): + def __init__(self, path, lineno, message): + self.path = str(path) + self.lineno = lineno + self.message = message + + def toterminal(self, tw): + # filename and lineno output for each entry, + # using an output format that most editors unterstand + msg = self.message + i = msg.find("\n") + if i != -1: + msg = msg[:i] + tw.line("%s:%s: %s" %(self.path, self.lineno, msg)) + +class ReprLocals(TerminalRepr): + def __init__(self, lines): + self.lines = lines + + def toterminal(self, tw): + for line in self.lines: + tw.line(line) + +class ReprFuncArgs(TerminalRepr): + def __init__(self, args): + self.args = args + + def toterminal(self, tw): + if self.args: + linesofar = "" + for name, value in self.args: + ns = "%s = %s" %(name, value) + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + else: + if linesofar: + linesofar += ", " + ns + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") + + + +oldbuiltins = {} + +def patch_builtins(assertion=True, compile=True): + """ put compile and AssertionError builtins to Python's builtins. """ + if assertion: + from _pytest._code import assertion + l = oldbuiltins.setdefault('AssertionError', []) + l.append(py.builtin.builtins.AssertionError) + py.builtin.builtins.AssertionError = assertion.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: + return obj.__code__ + except AttributeError: + obj = getattr(obj, 'im_func', obj) + obj = getattr(obj, 'func_code', obj) + obj = getattr(obj, 'f_code', obj) + obj = getattr(obj, '__code__', obj) + if trycall and not hasattr(obj, 'co_firstlineno'): + if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj): + x = getrawcode(obj.__call__, trycall=False) + if hasattr(x, 'co_firstlineno'): + return x + return obj + diff --git a/_pytest/_code/source.py b/_pytest/_code/source.py new file mode 100644 index 000000000..a1521f8a2 --- /dev/null +++ b/_pytest/_code/source.py @@ -0,0 +1,421 @@ +from __future__ import generators + +from bisect import bisect_right +import sys +import inspect, tokenize +import py +from types import ModuleType +cpy_compile = compile + +try: + import _ast + from _ast import PyCF_ONLY_AST as _AST_FLAG +except ImportError: + _AST_FLAG = 0 + _ast = None + + +class Source(object): + """ a immutable object holding a source code fragment, + possibly deindenting it. + """ + _compilecounter = 0 + def __init__(self, *parts, **kwargs): + self.lines = lines = [] + de = kwargs.get('deindent', True) + rstrip = kwargs.get('rstrip', True) + for part in parts: + if not part: + partlines = [] + if isinstance(part, Source): + partlines = part.lines + elif isinstance(part, (tuple, list)): + partlines = [x.rstrip("\n") for x in part] + elif isinstance(part, py.builtin._basestring): + partlines = part.split('\n') + if rstrip: + while partlines: + if partlines[-1].strip(): + break + partlines.pop() + else: + partlines = getsource(part, deindent=de).lines + if de: + partlines = deindent(partlines) + lines.extend(partlines) + + def __eq__(self, other): + try: + return self.lines == other.lines + except AttributeError: + if isinstance(other, str): + return str(self) == other + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self.lines[key] + else: + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + return self.__getslice__(key.start, key.stop) + + def __len__(self): + return len(self.lines) + + def __getslice__(self, start, end): + newsource = Source() + newsource.lines = self.lines[start:end] + return newsource + + def strip(self): + """ return new source object with trailing + and leading blank lines removed. + """ + start, end = 0, len(self) + while start < end and not self.lines[start].strip(): + start += 1 + while end > start and not self.lines[end-1].strip(): + end -= 1 + source = Source() + source.lines[:] = self.lines[start:end] + return source + + def putaround(self, before='', after='', indent=' ' * 4): + """ return a copy of the source object with + 'before' and 'after' wrapped around it. + """ + before = Source(before) + after = Source(after) + newsource = Source() + lines = [ (indent + line) for line in self.lines] + newsource.lines = before.lines + lines + after.lines + return newsource + + def indent(self, indent=' ' * 4): + """ return a copy of the source object with + all lines indented by the given indent-string. + """ + newsource = Source() + newsource.lines = [(indent+line) for line in self.lines] + return newsource + + def getstatement(self, lineno, assertion=False): + """ return Source statement which contains the + given linenumber (counted from 0). + """ + start, end = self.getstatementrange(lineno, assertion) + return self[start:end] + + def getstatementrange(self, lineno, assertion=False): + """ return (start, end) tuple which spans the minimal + statement region which containing the given lineno. + """ + if not (0 <= lineno < len(self)): + raise IndexError("lineno out of range") + ast, start, end = getstatementrange_ast(lineno, self) + return start, end + + def deindent(self, offset=None): + """ return a new source object deindented by offset. + If offset is None then guess an indentation offset from + the first non-blank line. Subsequent lines which have a + lower indentation offset will be copied verbatim as + they are assumed to be part of multilines. + """ + # XXX maybe use the tokenizer to properly handle multiline + # strings etc.pp? + newsource = Source() + newsource.lines[:] = deindent(self.lines, offset) + return newsource + + def isparseable(self, deindent=True): + """ return True if source is parseable, heuristically + deindenting it by default. + """ + try: + import parser + except ImportError: + syntax_checker = lambda x: compile(x, 'asd', 'exec') + else: + syntax_checker = parser.suite + + if deindent: + source = str(self.deindent()) + else: + source = str(self) + try: + #compile(source+'\n', "x", "exec") + syntax_checker(source+'\n') + except KeyboardInterrupt: + raise + except Exception: + return False + else: + return True + + def __str__(self): + return "\n".join(self.lines) + + def compile(self, filename=None, mode='exec', + flag=generators.compiler_flag, + dont_inherit=0, _genframe=None): + """ return compiled code object. if filename is None + invent an artificial filename which displays + the source/line position of the caller frame. + """ + if not filename or py.path.local(filename).check(file=0): + if _genframe is None: + _genframe = sys._getframe(1) # the caller + fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno + base = "<%d-codegen " % self._compilecounter + self.__class__._compilecounter += 1 + if not filename: + filename = base + '%s:%d>' % (fn, lineno) + else: + filename = base + '%r %s:%d>' % (filename, fn, lineno) + source = "\n".join(self.lines) + '\n' + try: + co = cpy_compile(source, filename, mode, flag) + except SyntaxError: + ex = sys.exc_info()[1] + # re-represent syntax errors from parsing python strings + msglines = self.lines[:ex.lineno] + if ex.offset: + msglines.append(" "*ex.offset + '^') + msglines.append("(code was compiled probably from here: %s)" % filename) + newex = SyntaxError('\n'.join(msglines)) + newex.offset = ex.offset + newex.lineno = ex.lineno + newex.text = ex.text + raise newex + else: + if flag & _AST_FLAG: + return co + lines = [(x + "\n") for x in self.lines] + if sys.version_info[0] >= 3: + # XXX py3's inspect.getsourcefile() checks for a module + # and a pep302 __loader__ ... we don't have a module + # at code compile-time so we need to fake it here + m = ModuleType("_pycodecompile_pseudo_module") + py.std.inspect.modulesbyfile[filename] = None + py.std.sys.modules[None] = m + m.__loader__ = 1 + py.std.linecache.cache[filename] = (1, None, lines, filename) + return co + +# +# public API shortcut functions +# + +def compile_(source, filename=None, mode='exec', flags= + generators.compiler_flag, dont_inherit=0): + """ compile the given source to a raw code object, + and maintain an internal cache which allows later + retrieval of the source code for the code object + and any recursively created code objects. + """ + if _ast is not None and isinstance(source, _ast.AST): + # XXX should Source support having AST? + return cpy_compile(source, filename, mode, flags, dont_inherit) + _genframe = sys._getframe(1) # the caller + s = Source(source) + co = s.compile(filename, mode, flags, _genframe=_genframe) + return co + + +def getfslineno(obj): + """ Return source location (path, lineno) for the given object. + If the source cannot be determined return ("", -1) + """ + import _pytest._code + try: + code = _pytest._code.Code(obj) + except TypeError: + try: + fn = (py.std.inspect.getsourcefile(obj) or + py.std.inspect.getfile(obj)) + except TypeError: + return "", -1 + + fspath = fn and py.path.local(fn) or None + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except IOError: + pass + else: + fspath = code.path + lineno = code.firstlineno + assert isinstance(lineno, int) + return fspath, lineno + +# +# helper functions +# + +def findsource(obj): + try: + sourcelines, lineno = py.std.inspect.findsource(obj) + except py.builtin._sysex: + raise + except: + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + return source, lineno + +def getsource(obj, **kwargs): + import _pytest._code + obj = _pytest._code.getrawcode(obj) + try: + strsrc = inspect.getsource(obj) + except IndentationError: + strsrc = "\"Buggy python version consider upgrading, cannot get source\"" + assert isinstance(strsrc, str) + return Source(strsrc, **kwargs) + +def deindent(lines, offset=None): + if offset is None: + for line in lines: + line = line.expandtabs() + s = line.lstrip() + if s: + offset = len(line)-len(s) + break + else: + offset = 0 + if offset == 0: + return list(lines) + newlines = [] + def readline_generator(lines): + for line in lines: + yield line + '\n' + while True: + yield '' + + it = readline_generator(lines) + + try: + for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)): + if sline > len(lines): + break # End of input reached + if sline > len(newlines): + line = lines[sline - 1].expandtabs() + if line.lstrip() and line[:offset].isspace(): + line = line[offset:] # Deindent + newlines.append(line) + + for i in range(sline, eline): + # Don't deindent continuing lines of + # multiline tokens (i.e. multiline strings) + newlines.append(lines[i]) + except (IndentationError, tokenize.TokenError): + pass + # Add any lines we didn't see. E.g. if an exception was raised. + newlines.extend(lines[len(newlines):]) + return newlines + + +def get_statement_startend2(lineno, node): + import ast + # flatten all statements and except handlers into one lineno-list + # AST's line numbers start indexing at 1 + l = [] + for x in ast.walk(node): + if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler): + l.append(x.lineno - 1) + for name in "finalbody", "orelse": + val = getattr(x, name, None) + if val: + # treat the finally/orelse part as its own statement + l.append(val[0].lineno - 1 - 1) + l.sort() + insert_index = bisect_right(l, lineno) + start = l[insert_index - 1] + if insert_index >= len(l): + end = None + else: + end = l[insert_index] + return start, end + + +def getstatementrange_ast(lineno, source, assertion=False, astnode=None): + if astnode is None: + content = str(source) + if sys.version_info < (2,7): + content += "\n" + try: + astnode = compile(content, "source", "exec", 1024) # 1024 for AST + except ValueError: + start, end = getstatementrange_old(lineno, source, assertion) + return None, start, end + start, end = get_statement_startend2(lineno, astnode) + # we need to correct the end: + # - ast-parsing strips comments + # - there might be empty lines + # - we might have lesser indented code blocks at the end + if end is None: + end = len(source.lines) + + if end > start + 1: + # make sure we don't span differently indented code blocks + # by using the BlockFinder helper used which inspect.getsource() uses itself + block_finder = inspect.BlockFinder() + # if we start with an indented line, put blockfinder to "started" mode + block_finder.started = source.lines[start][0].isspace() + it = ((x + "\n") for x in source.lines[start:end]) + try: + for tok in tokenize.generate_tokens(lambda: next(it)): + block_finder.tokeneater(*tok) + except (inspect.EndOfBlock, IndentationError): + end = block_finder.last + start + except Exception: + pass + + # the end might still point to a comment or empty line, correct it + while end: + line = source.lines[end - 1].lstrip() + if line.startswith("#") or not line: + end -= 1 + else: + break + return astnode, start, end + + +def getstatementrange_old(lineno, source, assertion=False): + """ return (start, end) tuple which spans the minimal + statement region which containing the given lineno. + raise an IndexError if no such statementrange can be found. + """ + # XXX this logic is only used on python2.4 and below + # 1. find the start of the statement + from codeop import compile_command + for start in range(lineno, -1, -1): + if assertion: + line = source.lines[start] + # the following lines are not fully tested, change with care + if 'super' in line and 'self' in line and '__init__' in line: + raise IndexError("likely a subclass") + if "assert" not in line and "raise" not in line: + continue + trylines = source.lines[start:lineno+1] + # quick hack to prepare parsing an indented line with + # compile_command() (which errors on "return" outside defs) + trylines.insert(0, 'def xxx():') + trysource = '\n '.join(trylines) + # ^ space here + try: + compile_command(trysource) + except (SyntaxError, OverflowError, ValueError): + continue + + # 2. find the end of the statement + for end in range(lineno+1, len(source)+1): + trysource = source[start:end] + if trysource.isparseable(): + return start, end + raise SyntaxError("no valid source range around line %d " % (lineno,)) + + diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py index 213a3a00a..f4262c3ac 100644 --- a/_pytest/assertion/reinterpret.py +++ b/_pytest/assertion/reinterpret.py @@ -3,6 +3,8 @@ 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 @@ -26,7 +28,7 @@ class AssertionError(util.BuiltinAssertionError): "<[broken __repr__] %s at %0xd>" % (toprint.__class__, id(toprint))) else: - f = py.code.Frame(sys._getframe(1)) + f = _pytest._code.Frame(sys._getframe(1)) try: source = f.code.fullsource if source is not None: @@ -102,7 +104,7 @@ def reinterpret(source, frame, should_fail=False): def run(offending_line, frame=None): if frame is None: - frame = py.code.Frame(sys._getframe(1)) + frame = _pytest._code.Frame(sys._getframe(1)) return reinterpret(offending_line, frame) def getfailure(e): diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 401d04f10..2c1e39c02 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -1,6 +1,7 @@ """Utilities for assertion debugging""" import pprint +import _pytest._code import py try: from collections import Sequence @@ -179,7 +180,7 @@ def assertrepr_compare(config, op, left, right): explanation = [ u('(pytest_assertion plugin: representation of details failed. ' 'Probably an object has a faulty __repr__.)'), - u(py.code.ExceptionInfo())] + u(_pytest._code.ExceptionInfo())] if not explanation: return None diff --git a/_pytest/config.py b/_pytest/config.py index 5ffed81dd..761b0e52e 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -8,6 +8,7 @@ import warnings import py # DON't import pytest here because it causes import cycle troubles import sys, os +import _pytest._code import _pytest.hookspec # the extension point definitions from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker @@ -158,7 +159,7 @@ class PytestPluginManager(PluginManager): Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead. """ warning = dict(code="I2", - fslocation=py.code.getfslineno(sys._getframe(1)), + fslocation=_pytest._code.getfslineno(sys._getframe(1)), nodeid=None, message="use pluginmanager.add_hookspecs instead of " "deprecated addhooks() method.") @@ -195,7 +196,7 @@ class PytestPluginManager(PluginManager): def _verify_hook(self, hook, hookmethod): super(PytestPluginManager, self)._verify_hook(hook, hookmethod) if "__multicall__" in hookmethod.argnames: - fslineno = py.code.getfslineno(hookmethod.function) + fslineno = _pytest._code.getfslineno(hookmethod.function) warning = dict(code="I1", fslocation=fslineno, nodeid=None, diff --git a/_pytest/doctest.py b/_pytest/doctest.py index a00c0c946..c3fa94e7e 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -1,9 +1,13 @@ """ discover and run doctests in modules and test files.""" from __future__ import absolute_import + import traceback -import pytest, py + +import py +import pytest +from _pytest._code.code import TerminalRepr, ReprFileLocation, ExceptionInfo from _pytest.python import FixtureRequest -from py._code.code import TerminalRepr, ReprFileLocation + def pytest_addoption(parser): @@ -107,7 +111,7 @@ class DoctestItem(pytest.Item): lines += checker.output_difference(example, doctestfailure.got, REPORT_UDIFF).split("\n") else: - inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) + inner_excinfo = ExceptionInfo(excinfo.value.exc_info) lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] lines += traceback.format_exception(*excinfo.value.exc_info) diff --git a/_pytest/main.py b/_pytest/main.py index 6454ba2ae..70d6896cb 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -1,9 +1,13 @@ """ core implementation of testing process: init, session, runtest loop. """ +import imp +import os import re +import sys +import _pytest +import _pytest._code import py -import pytest, _pytest -import os, sys, imp +import pytest try: from collections import MutableMapping as MappingMixin except ImportError: @@ -91,11 +95,11 @@ def wrap_session(config, doit): except pytest.UsageError: raise except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = EXIT_INTERRUPTED except: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 95f92d835..faed7f581 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,19 +1,20 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ -import gc -import sys -import traceback -import os import codecs -import re -import time +import gc +import os import platform -from fnmatch import fnmatch +import re import subprocess +import sys +import time +import traceback +from fnmatch import fnmatch -import py -import pytest from py.builtin import print_ +from _pytest._code import Source +import py +import pytest from _pytest.main import Session, EXIT_OK @@ -472,7 +473,7 @@ class Testdir: ret = None for name, value in items: p = self.tmpdir.join(name).new(ext=ext) - source = py.code.Source(value) + source = Source(value) def my_totext(s, encoding="utf-8"): if py.builtin._isbytes(s): s = py.builtin._totext(s, encoding=encoding) @@ -835,7 +836,7 @@ class Testdir: to the temporarly directory to ensure it is a package. """ - kw = {self.request.function.__name__: py.code.Source(source).strip()} + kw = {self.request.function.__name__: Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") @@ -1041,8 +1042,8 @@ class LineMatcher: def _getlines(self, lines2): if isinstance(lines2, str): - lines2 = py.code.Source(lines2) - if isinstance(lines2, py.code.Source): + lines2 = Source(lines2) + if isinstance(lines2, Source): lines2 = lines2.strip().lines return lines2 diff --git a/_pytest/python.py b/_pytest/python.py index 6f3a717af..065971be6 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,14 +1,15 @@ """ Python test discovery, setup and run of test functions. """ -import re import fnmatch import functools -import py import inspect +import re import types import sys + +import py import pytest +from _pytest._code.code import TerminalRepr from _pytest.mark import MarkDecorator, MarkerError -from py._code.code import TerminalRepr try: import enum @@ -86,7 +87,7 @@ def getfslineno(obj): obj = get_real_func(obj) if hasattr(obj, 'place_as'): obj = obj.place_as - fslineno = py.code.getfslineno(obj) + fslineno = _pytest._code.getfslineno(obj) assert isinstance(fslineno[1], int), obj return fslineno @@ -331,7 +332,7 @@ def pytest_pycollect_makeitem(collector, name, obj): def is_generator(func): try: - return py.code.getrawcode(func).co_flags & 32 # generator function + return _pytest._code.getrawcode(func).co_flags & 32 # generator function except AttributeError: # builtin functions have no bytecode # assume them to not be generators return False @@ -610,7 +611,7 @@ class Module(pytest.File, PyCollector): mod = self.fspath.pyimport(ensuresyspath=importmode) except SyntaxError: raise self.CollectError( - py.code.ExceptionInfo().getrepr(style="short")) + _pytest._code.ExceptionInfo().getrepr(style="short")) except self.fspath.ImportMismatchError: e = sys.exc_info()[1] raise self.CollectError( @@ -716,7 +717,7 @@ class FunctionMixin(PyobjMixin): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(get_real_func(self.obj)) + code = _pytest._code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1202,10 +1203,10 @@ def getlocation(function, curdir): # builtin pytest.raises helper def raises(expected_exception, *args, **kwargs): - """ assert that a code block/function call raises @expected_exception + """ assert that a code block/function call raises ``expected_exception`` and raise a failure exception otherwise. - This helper produces a ``py.code.ExceptionInfo()`` object. + This helper produces a ``ExceptionInfo()`` object (see below). If using Python 2.5 or above, you may use this function as a context manager:: @@ -1221,19 +1222,19 @@ def raises(expected_exception, *args, **kwargs): Lines of code after that, within the scope of the context manager will not be executed. For example:: - >>> with raises(OSError) as err: + >>> with raises(OSError) as exc_info: assert 1 == 1 # this will execute as expected raise OSError(errno.EEXISTS, 'directory exists') - assert err.errno == errno.EEXISTS # this will not execute + assert exc_info.value.errno == errno.EEXISTS # this will not execute Instead, the following approach must be taken (note the difference in scope):: - >>> with raises(OSError) as err: + >>> with raises(OSError) as exc_info: assert 1 == 1 # this will execute as expected raise OSError(errno.EEXISTS, 'directory exists') - assert err.errno == errno.EEXISTS # this will now execute + assert exc_info.value.errno == errno.EEXISTS # this will now execute Or you can specify a callable by passing a to-be-called lambda:: @@ -1254,21 +1255,22 @@ def raises(expected_exception, *args, **kwargs): >>> raises(ZeroDivisionError, "f(0)") - Performance note: - ----------------- + .. autoclass:: _pytest._code.ExceptionInfo + :members: - Similar to caught exception objects in Python, explicitly clearing - local references to returned ``py.code.ExceptionInfo`` objects can - help the Python interpreter speed up its garbage collection. + .. note:: + Similar to caught exception objects in Python, explicitly clearing + local references to returned ``ExceptionInfo`` objects can + help the Python interpreter speed up its garbage collection. - Clearing those references breaks a reference cycle - (``ExceptionInfo`` --> caught exception --> frame stack raising - the exception --> current frame stack --> local variables --> - ``ExceptionInfo``) which makes Python keep all objects referenced - from that cycle (including all local variables in the current - frame) alive until the next cyclic garbage collection run. See the - official Python ``try`` statement documentation for more detailed - information. + Clearing those references breaks a reference cycle + (``ExceptionInfo`` --> caught exception --> frame stack raising + the exception --> current frame stack --> local variables --> + ``ExceptionInfo``) which makes Python keep all objects referenced + from that cycle (including all local variables in the current + frame) alive until the next cyclic garbage collection run. See the + official Python ``try`` statement documentation for more detailed + information. """ __tracebackhide__ = True @@ -1297,18 +1299,18 @@ def raises(expected_exception, *args, **kwargs): loc.update(kwargs) #print "raises frame scope: %r" % frame.f_locals try: - code = py.code.Source(code).compile() + code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) # XXX didn'T mean f_globals == f_locals something special? # this is destroyed here ... except expected_exception: - return py.code.ExceptionInfo() + return _pytest._code.ExceptionInfo() else: func = args[0] try: func(*args[1:], **kwargs) except expected_exception: - return py.code.ExceptionInfo() + return _pytest._code.ExceptionInfo() pytest.fail("DID NOT RAISE") class RaisesContext(object): @@ -1317,7 +1319,7 @@ class RaisesContext(object): self.excinfo = None def __enter__(self): - self.excinfo = object.__new__(py.code.ExceptionInfo) + self.excinfo = object.__new__(_pytest._code.ExceptionInfo) return self.excinfo def __exit__(self, *tp): @@ -2025,7 +2027,7 @@ class FixtureManager: def fail_fixturefunc(fixturefunc, msg): fs, lineno = getfslineno(fixturefunc) location = "%s:%s" % (fs, lineno+1) - source = py.code.Source(fixturefunc) + source = _pytest._code.Source(fixturefunc) pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) @@ -2168,14 +2170,14 @@ def getfuncargnames(function, startindex=None): startindex += num_mock_patch_args(function) function = realfunction if isinstance(function, functools.partial): - argnames = inspect.getargs(py.code.getrawcode(function.func))[0] + argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0] partial = function argnames = argnames[len(partial.args):] if partial.keywords: for kw in partial.keywords: argnames.remove(kw) else: - argnames = inspect.getargs(py.code.getrawcode(function))[0] + argnames = inspect.getargs(_pytest._code.getrawcode(function))[0] defaults = getattr(function, 'func_defaults', getattr(function, '__defaults__', None)) or () numdefaults = len(defaults) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 16723289d..a89474c03 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -1,6 +1,8 @@ """ recording warnings during test function execution. """ import inspect + +import _pytest._code import py import sys import warnings @@ -100,7 +102,7 @@ def warns(expected_warning, *args, **kwargs): loc.update(kwargs) with wcheck: - code = py.code.Source(code).compile() + code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) else: func = args[0] diff --git a/_pytest/runner.py b/_pytest/runner.py index 979757c16..a50c2d738 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -5,7 +5,8 @@ from time import time import py import pytest -from py._code.code import TerminalRepr +from _pytest._code.code import TerminalRepr, ExceptionInfo + def pytest_namespace(): return { @@ -151,7 +152,7 @@ class CallInfo: self.stop = time() raise except: - self.excinfo = py.code.ExceptionInfo() + self.excinfo = ExceptionInfo() self.stop = time() def __repr__(self): @@ -215,7 +216,7 @@ def pytest_runtest_makereport(item, call): outcome = "passed" longrepr = None else: - if not isinstance(excinfo, py.code.ExceptionInfo): + if not isinstance(excinfo, ExceptionInfo): outcome = "failed" longrepr = excinfo elif excinfo.errisinstance(pytest.skip.Exception): diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 9bd38d684..5b779c98b 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -291,9 +291,8 @@ def cached_eval(config, expr, d): try: return config._evalcache[expr] except KeyError: - #import sys - #print >>sys.stderr, ("cache-miss: %r" % expr) - exprcode = py.code.compile(expr, mode="eval") + import _pytest._code + exprcode = _pytest._code.compile(expr, mode="eval") config._evalcache[expr] = x = eval(exprcode, d) return x diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 24fa9e921..8120e94fb 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -1,13 +1,12 @@ """ discovery and running of std-library "unittest" style tests. """ from __future__ import absolute_import -import traceback + import sys +import traceback import pytest -import py - - # for transfering markers +import _pytest._code from _pytest.python import transfer_markers from _pytest.skipping import MarkEvaluator @@ -101,7 +100,7 @@ class TestCaseFunction(pytest.Function): # unwrap potential exception info (see twisted trial support below) rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo) try: - excinfo = py.code.ExceptionInfo(rawexcinfo) + excinfo = _pytest._code.ExceptionInfo(rawexcinfo) except TypeError: try: try: @@ -117,7 +116,7 @@ class TestCaseFunction(pytest.Function): except KeyboardInterrupt: raise except pytest.fail.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() self.__dict__.setdefault('_excinfo', []).append(excinfo) def addError(self, testcase, rawexcinfo): diff --git a/doc/en/assert.rst b/doc/en/assert.rst index f65225c92..beac284dc 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -81,13 +81,10 @@ and if you need to have access to the actual exception info you may use:: f() assert 'maximum recursion' in str(excinfo.value) -``excinfo`` is a `py.code.ExceptionInfo`_ instance, which is a wrapper around +``excinfo`` is a ``ExceptionInfo`` instance, which is a wrapper around the actual exception raised. The main attributes of interest are ``.type``, ``.value`` and ``.traceback``. -.. _py.code.ExceptionInfo: - http://pylib.readthedocs.org/en/latest/code.html#py-code-exceptioninfo - If you want to write test code that works on Python 2.4 as well, you may also use two other ways to test for an expected exception:: diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index ecc1cd356..a4ff758b1 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -1,4 +1,5 @@ from pytest import raises +import _pytest._code import py def otherfunc(a,b): @@ -159,7 +160,7 @@ def test_dynamic_compile_shows_nicely(): src = 'def foo():\n assert 1 == 0\n' name = 'abc-123' module = py.std.imp.new_module(name) - code = py.code.compile(src, name, 'exec') + code = _pytest._code.compile(src, name, 'exec') py.builtin.exec_(code, module.__dict__) py.std.sys.modules[name] = module module.foo() diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index b9242101b..66a368a12 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -4,6 +4,7 @@ serialization via the pickle module. """ import py import pytest +import _pytest._code pythonlist = ['python2.6', 'python2.7', 'python3.3'] @pytest.fixture(params=pythonlist) @@ -23,7 +24,7 @@ class Python: self.picklefile = picklefile def dumps(self, obj): dumpfile = self.picklefile.dirpath("dump.py") - dumpfile.write(py.code.Source(""" + dumpfile.write(_pytest._code.Source(""" import pickle f = open(%r, 'wb') s = pickle.dump(%r, f, protocol=2) @@ -33,7 +34,7 @@ class Python: def load_and_is_true(self, expression): loadfile = self.picklefile.dirpath("load.py") - loadfile.write(py.code.Source(""" + loadfile.write(_pytest._code.Source(""" import pickle f = open(%r, 'rb') obj = pickle.load(f) diff --git a/setup.py b/setup.py index ec9c9d430..6660f2160 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def main(): # the following should be enabled for release install_requires=install_requires, extras_require=extras_require, - packages=['_pytest', '_pytest.assertion', '_pytest.vendored_packages'], + packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.vendored_packages'], py_modules=['pytest'], zip_safe=False, ) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 4cd731e72..74db4425d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,5 +1,8 @@ import sys -import py, pytest + +import _pytest._code +import py +import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR @@ -197,7 +200,7 @@ class TestGeneralUsage: def test_chdir(self, testdir): testdir.tmpdir.join("py").mksymlinkto(py._pydir) p = testdir.tmpdir.join("main.py") - p.write(py.code.Source(""" + p.write(_pytest._code.Source(""" import sys, os sys.path.insert(0, '') import py diff --git a/testing/code/test_code.py b/testing/code/test_code.py new file mode 100644 index 000000000..e9c7f7ab1 --- /dev/null +++ b/testing/code/test_code.py @@ -0,0 +1,163 @@ +import sys + +import _pytest._code +import py +import pytest + + +def test_ne(): + code1 = _pytest._code.Code(compile('foo = "bar"', '', 'exec')) + assert code1 == code1 + code2 = _pytest._code.Code(compile('foo = "baz"', '', 'exec')) + assert code2 != code1 + +def test_code_gives_back_name_for_not_existing_file(): + name = 'abc-123' + co_code = compile("pass\n", name, 'exec') + assert co_code.co_filename == name + code = _pytest._code.Code(co_code) + assert str(code.path) == name + assert code.fullsource is None + +def test_code_with_class(): + class A: + pass + pytest.raises(TypeError, "_pytest._code.Code(A)") + +if True: + def x(): + pass + +def test_code_fullsource(): + code = _pytest._code.Code(x) + full = code.fullsource + assert 'test_code_fullsource()' in str(full) + +def test_code_source(): + code = _pytest._code.Code(x) + src = code.source() + expected = """def x(): + pass""" + assert str(src) == expected + +def test_frame_getsourcelineno_myself(): + def func(): + return sys._getframe(0) + f = func() + f = _pytest._code.Frame(f) + source, lineno = f.code.fullsource, f.lineno + assert source[lineno].startswith(" return sys._getframe(0)") + +def test_getstatement_empty_fullsource(): + def func(): + return sys._getframe(0) + f = func() + f = _pytest._code.Frame(f) + prop = f.code.__class__.fullsource + try: + f.code.__class__.fullsource = None + assert f.statement == _pytest._code.Source("") + finally: + f.code.__class__.fullsource = prop + +def test_code_from_func(): + co = _pytest._code.Code(test_frame_getsourcelineno_myself) + assert co.firstlineno + 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(): + raise Exception(value) + excinfo = pytest.raises(Exception, f) + str(excinfo) + if sys.version_info[0] < 3: + unicode(excinfo) + +def test_code_getargs(): + def f1(x): + pass + c1 = _pytest._code.Code(f1) + assert c1.getargs(var=True) == ('x',) + + def f2(x, *y): + pass + c2 = _pytest._code.Code(f2) + assert c2.getargs(var=True) == ('x', 'y') + + def f3(x, **z): + pass + c3 = _pytest._code.Code(f3) + assert c3.getargs(var=True) == ('x', 'z') + + def f4(x, *y, **z): + pass + c4 = _pytest._code.Code(f4) + assert c4.getargs(var=True) == ('x', 'y', 'z') + + +def test_frame_getargs(): + def f1(x): + return sys._getframe(0) + fr1 = _pytest._code.Frame(f1('a')) + assert fr1.getargs(var=True) == [('x', 'a')] + + def f2(x, *y): + return sys._getframe(0) + fr2 = _pytest._code.Frame(f2('a', 'b', 'c')) + assert fr2.getargs(var=True) == [('x', 'a'), ('y', ('b', 'c'))] + + def f3(x, **z): + return sys._getframe(0) + fr3 = _pytest._code.Frame(f3('a', b='c')) + assert fr3.getargs(var=True) == [('x', 'a'), ('z', {'b': 'c'})] + + def f4(x, *y, **z): + return sys._getframe(0) + fr4 = _pytest._code.Frame(f4('a', 'b', c='d')) + assert fr4.getargs(var=True) == [('x', 'a'), ('y', ('b',)), + ('z', {'c': 'd'})] + + +class TestExceptionInfo: + + def test_bad_getsource(self): + try: + if False: pass + else: assert False + except AssertionError: + exci = _pytest._code.ExceptionInfo() + assert exci.getrepr() + + +class TestTracebackEntry: + + def test_getsource(self): + try: + if False: pass + else: assert False + except AssertionError: + exci = _pytest._code.ExceptionInfo() + entry = exci.traceback[0] + source = entry.getsource() + assert len(source) == 4 + assert 'else: assert False' in source[3] diff --git a/testing/code/test_code_assertion.py b/testing/code/test_code_assertion.py new file mode 100644 index 000000000..13f230edb --- /dev/null +++ b/testing/code/test_code_assertion.py @@ -0,0 +1,309 @@ +import pytest, py + +def exvalue(): + return py.std.sys.exc_info()[1] + +def f(): + return 2 + +def test_assert(): + try: + assert f() == 3 + except AssertionError: + e = exvalue() + s = str(e) + assert s.startswith('assert 2 == 3\n') + + +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") + + +@pytest.mark.skipif("sys.version_info < (2,6)") +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") + +@pytest.mark.skipif("sys.version_info < (2,6)") +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_keyword_arg(): + def f(x=3): + return False + try: + assert f(x=5) + except AssertionError: + e = exvalue() + assert "x=5" 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 + + +class TestView: + + def setup_class(cls): + cls.View = pytest.importorskip("_pytest._code._assertionold").View + + def test_class_dispatch(self): + ### Use a custom class hierarchy with existing instances + + class Picklable(self.View): + pass + + class Simple(Picklable): + __view__ = object + def pickle(self): + return repr(self.__obj__) + + class Seq(Picklable): + __view__ = list, tuple, dict + def pickle(self): + return ';'.join( + [Picklable(item).pickle() for item in self.__obj__]) + + class Dict(Seq): + __view__ = dict + def pickle(self): + return Seq.pickle(self) + '!' + Seq(self.values()).pickle() + + assert Picklable(123).pickle() == '123' + assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4' + assert Picklable({1:2}).pickle() == '1!2' + + def test_viewtype_class_hierarchy(self): + # Use a custom class hierarchy based on attributes of existing instances + class Operation: + "Existing class that I don't want to change." + def __init__(self, opname, *args): + self.opname = opname + self.args = args + + existing = [Operation('+', 4, 5), + Operation('getitem', '', 'join'), + Operation('setattr', 'x', 'y', 3), + Operation('-', 12, 1)] + + class PyOp(self.View): + def __viewkey__(self): + return self.opname + def generate(self): + return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args))) + + class PyBinaryOp(PyOp): + __view__ = ('+', '-', '*', '/') + def generate(self): + return '%s %s %s' % (self.args[0], self.opname, self.args[1]) + + codelines = [PyOp(op).generate() for op in existing] + assert codelines == ["4 + 5", "getitem('', 'join')", + "setattr('x', 'y', 3)", "12 - 1"] + +def test_underscore_api(): + import _pytest._code + _pytest._code._AssertionError + _pytest._code._reinterpret_old # used by pypy + _pytest._code._reinterpret + +@pytest.mark.skipif("sys.version_info < (2,6)") +def test_assert_customizable_reprcompare(monkeypatch): + util = pytest.importorskip("_pytest.assertion.util") + 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(""" + 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*", + ]) + + +@pytest.mark.skipif("sys.version_info < (2,5)") +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/code/test_excinfo.py b/testing/code/test_excinfo.py new file mode 100644 index 000000000..2defa3103 --- /dev/null +++ b/testing/code/test_excinfo.py @@ -0,0 +1,911 @@ +# -*- coding: utf-8 -*- + +import _pytest +import py +import pytest +from _pytest._code.code import FormattedExcinfo, ReprExceptionInfo + +queue = py.builtin._tryimport('queue', 'Queue') + +failsonjython = pytest.mark.xfail("sys.platform.startswith('java')") +from test_source import astonly + +try: + import importlib +except ImportError: + invalidate_import_caches = None +else: + invalidate_import_caches = getattr(importlib, "invalidate_caches", None) + +import pytest +pytest_version_info = tuple(map(int, pytest.__version__.split(".")[:3])) + +class TWMock: + def __init__(self): + self.lines = [] + def sep(self, sep, line=None): + self.lines.append((sep, line)) + def line(self, line, **kw): + self.lines.append(line) + def markup(self, text, **kw): + return text + + fullwidth = 80 + +def test_excinfo_simple(): + try: + raise ValueError + except ValueError: + info = _pytest._code.ExceptionInfo() + assert info.type == ValueError + +def test_excinfo_getstatement(): + def g(): + raise ValueError + def f(): + g() + try: + f() + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + linenumbers = [_pytest._code.getrawcode(f).co_firstlineno - 1 + 3, + _pytest._code.getrawcode(f).co_firstlineno - 1 + 1, + _pytest._code.getrawcode(g).co_firstlineno - 1 + 1, ] + l = list(excinfo.traceback) + foundlinenumbers = [x.lineno for x in l] + assert foundlinenumbers == linenumbers + #for x in info: + # print "%s:%d %s" %(x.path.relto(root), x.lineno, x.statement) + #xxx + +# testchain for getentries test below +def f(): + # + raise ValueError + # +def g(): + # + __tracebackhide__ = True + f() + # +def h(): + # + g() + # + +class TestTraceback_f_g_h: + def setup_method(self, method): + try: + h() + except ValueError: + self.excinfo = _pytest._code.ExceptionInfo() + + def test_traceback_entries(self): + tb = self.excinfo.traceback + entries = list(tb) + assert len(tb) == 4 # maybe fragile test + assert len(entries) == 4 # maybe fragile test + names = ['f', 'g', 'h'] + for entry in entries: + try: + names.remove(entry.frame.code.name) + except ValueError: + pass + assert not names + + def test_traceback_entry_getsource(self): + tb = self.excinfo.traceback + s = str(tb[-1].getsource() ) + assert s.startswith("def f():") + assert s.endswith("raise ValueError") + + @astonly + @failsonjython + def test_traceback_entry_getsource_in_construct(self): + source = _pytest._code.Source("""\ + def xyz(): + try: + raise ValueError + except somenoname: + pass + xyz() + """) + try: + exec (source.compile()) + except NameError: + tb = _pytest._code.ExceptionInfo().traceback + print (tb[-1].getsource()) + s = str(tb[-1].getsource()) + assert s.startswith("def xyz():\n try:") + assert s.strip().endswith("except somenoname:") + + def test_traceback_cut(self): + co = _pytest._code.Code(f) + path, firstlineno = co.path, co.firstlineno + traceback = self.excinfo.traceback + newtraceback = traceback.cut(path=path, firstlineno=firstlineno) + assert len(newtraceback) == 1 + newtraceback = traceback.cut(path=path, lineno=firstlineno+2) + assert len(newtraceback) == 1 + + def test_traceback_cut_excludepath(self, testdir): + p = testdir.makepyfile("def f(): raise ValueError") + excinfo = pytest.raises(ValueError, "p.pyimport().f()") + basedir = py.path.local(pytest.__file__).dirpath() + newtraceback = excinfo.traceback.cut(excludepath=basedir) + for x in newtraceback: + if hasattr(x, 'path'): + assert not py.path.local(x.path).relto(basedir) + assert newtraceback[-1].frame.code.path == p + + def test_traceback_filter(self): + traceback = self.excinfo.traceback + ntraceback = traceback.filter() + assert len(ntraceback) == len(traceback) - 1 + + def test_traceback_recursion_index(self): + def f(n): + if n < 10: + n += 1 + f(n) + excinfo = pytest.raises(RuntimeError, f, 8) + traceback = excinfo.traceback + recindex = traceback.recursionindex() + assert recindex == 3 + + def test_traceback_only_specific_recursion_errors(self, monkeypatch): + def f(n): + if n == 0: + raise RuntimeError("hello") + f(n-1) + + excinfo = pytest.raises(RuntimeError, f, 100) + monkeypatch.delattr(excinfo.traceback.__class__, "recursionindex") + repr = excinfo.getrepr() + assert "RuntimeError: hello" in str(repr.reprcrash) + + def test_traceback_no_recursion_index(self): + def do_stuff(): + raise RuntimeError + def reraise_me(): + import sys + exc, val, tb = sys.exc_info() + py.builtin._reraise(exc, val, tb) + def f(n): + try: + do_stuff() + except: + reraise_me() + excinfo = pytest.raises(RuntimeError, f, 8) + traceback = excinfo.traceback + recindex = traceback.recursionindex() + assert recindex is None + + def test_traceback_messy_recursion(self): + #XXX: simplified locally testable version + decorator = pytest.importorskip('decorator').decorator + + def log(f, *k, **kw): + print('%s %s' % (k, kw)) + f(*k, **kw) + log = decorator(log) + + def fail(): + raise ValueError('') + + fail = log(log(fail)) + + excinfo = pytest.raises(ValueError, fail) + assert excinfo.traceback.recursionindex() is None + + + + def test_traceback_getcrashentry(self): + def i(): + __tracebackhide__ = True + raise ValueError + def h(): + i() + def g(): + __tracebackhide__ = True + h() + def f(): + g() + + excinfo = pytest.raises(ValueError, f) + tb = excinfo.traceback + entry = tb.getcrashentry() + co = _pytest._code.Code(h) + assert entry.frame.code.path == co.path + assert entry.lineno == co.firstlineno + 1 + assert entry.frame.code.name == 'h' + + def test_traceback_getcrashentry_empty(self): + def g(): + __tracebackhide__ = True + raise ValueError + def f(): + __tracebackhide__ = True + g() + + excinfo = pytest.raises(ValueError, f) + tb = excinfo.traceback + entry = tb.getcrashentry() + co = _pytest._code.Code(g) + assert entry.frame.code.path == co.path + 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') + excinfo = pytest.raises(ValueError, + "raise ValueError('hello\\nworld')") + msg = excinfo.exconly(tryshort=True) + assert msg.startswith('ValueError') + assert msg.endswith("world") + +def test_excinfo_repr(): + excinfo = pytest.raises(ValueError, h) + s = repr(excinfo) + assert s == "" + +def test_excinfo_str(): + excinfo = pytest.raises(ValueError, h) + s = str(excinfo) + assert s.startswith(__file__[:-9]) # pyc file and $py.class + assert s.endswith("ValueError") + assert len(s.split(":")) >= 3 # on windows it's 4 + +def test_excinfo_errisinstance(): + excinfo = pytest.raises(ValueError, h) + assert excinfo.errisinstance(ValueError) + +def test_excinfo_no_sourcecode(): + try: + exec ("raise ValueError()") + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + s = str(excinfo.traceback[-1]) + if py.std.sys.version_info < (2,5): + assert s == " File '':1 in ?\n ???\n" + else: + assert s == " File '':1 in \n ???\n" + +def test_excinfo_no_python_sourcecode(tmpdir): + #XXX: simplified locally testable version + tmpdir.join('test.txt').write("{{ h()}}:") + + jinja2 = pytest.importorskip('jinja2') + loader = jinja2.FileSystemLoader(str(tmpdir)) + env = jinja2.Environment(loader=loader) + template = env.get_template('test.txt') + excinfo = pytest.raises(ValueError, + template.render, h=h) + for item in excinfo.traceback: + print(item) #XXX: for some reason jinja.Template.render is printed in full + item.source # shouldnt fail + if item.path.basename == 'test.txt': + assert str(item.source) == '{{ h()}}:' + + +def test_entrysource_Queue_example(): + try: + queue.Queue().get(timeout=0.001) + except queue.Empty: + excinfo = _pytest._code.ExceptionInfo() + entry = excinfo.traceback[-1] + source = entry.getsource() + assert source is not None + s = str(source).strip() + assert s.startswith("def get") + +def test_codepath_Queue_example(): + try: + queue.Queue().get(timeout=0.001) + except queue.Empty: + excinfo = _pytest._code.ExceptionInfo() + entry = excinfo.traceback[-1] + path = entry.path + assert isinstance(path, py.path.local) + assert path.basename.lower() == "queue.py" + assert path.check() + +class TestFormattedExcinfo: + def pytest_funcarg__importasmod(self, request): + def importasmod(source): + source = _pytest._code.Source(source) + tmpdir = request.getfuncargvalue("tmpdir") + modpath = tmpdir.join("mod.py") + tmpdir.ensure("__init__.py") + modpath.write(source) + if invalidate_import_caches is not None: + invalidate_import_caches() + return modpath.pyimport() + return importasmod + + def excinfo_from_exec(self, source): + source = _pytest._code.Source(source).strip() + try: + exec (source.compile()) + except KeyboardInterrupt: + raise + except: + return _pytest._code.ExceptionInfo() + assert 0, "did not raise" + + def test_repr_source(self): + pr = FormattedExcinfo() + source = _pytest._code.Source(""" + def f(x): + pass + """).strip() + pr.flow_marker = "|" + lines = pr.get_source(source, 0) + assert len(lines) == 2 + assert lines[0] == "| def f(x):" + assert lines[1] == " pass" + + def test_repr_source_excinfo(self): + """ check if indentation is right """ + pr = FormattedExcinfo() + excinfo = self.excinfo_from_exec(""" + def f(): + assert 0 + f() + """) + pr = FormattedExcinfo() + source = pr._getentrysource(excinfo.traceback[-1]) + lines = pr.get_source(source, 1, excinfo) + assert lines == [ + ' def f():', + '> assert 0', + 'E assert 0' + ] + + + def test_repr_source_not_existing(self): + pr = FormattedExcinfo() + co = compile("raise ValueError()", "", "exec") + try: + exec (co) + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" + + def test_repr_many_line_source_not_existing(self): + pr = FormattedExcinfo() + co = compile(""" +a = 1 +raise ValueError() +""", "", "exec") + try: + exec (co) + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" + + def test_repr_source_failing_fullsource(self): + pr = FormattedExcinfo() + + class FakeCode(object): + class raw: + co_filename = '?' + path = '?' + firstlineno = 5 + + def fullsource(self): + return None + fullsource = property(fullsource) + + class FakeFrame(object): + code = FakeCode() + f_locals = {} + f_globals = {} + + class FakeTracebackEntry(_pytest._code.Traceback.Entry): + def __init__(self, tb): + self.lineno = 5+3 + + @property + def frame(self): + return FakeFrame() + + class Traceback(_pytest._code.Traceback): + Entry = FakeTracebackEntry + + class FakeExcinfo(_pytest._code.ExceptionInfo): + typename = "Foo" + def __init__(self): + pass + + def exconly(self, tryshort): + return "EXC" + def errisinstance(self, cls): + return False + + excinfo = FakeExcinfo() + class FakeRawTB(object): + tb_next = None + tb = FakeRawTB() + excinfo.traceback = Traceback(tb) + + fail = IOError() # noqa + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" + + fail = py.error.ENOENT # noqa + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" + + + def test_repr_local(self): + p = FormattedExcinfo(showlocals=True) + loc = {'y': 5, 'z': 7, 'x': 3, '@x': 2, '__builtins__': {}} + reprlocals = p.repr_locals(loc) + assert reprlocals.lines + assert reprlocals.lines[0] == '__builtins__ = ' + assert reprlocals.lines[1] == 'x = 3' + assert reprlocals.lines[2] == 'y = 5' + assert reprlocals.lines[3] == 'z = 7' + + def test_repr_tracebackentry_lines(self, importasmod): + mod = importasmod(""" + def func1(): + raise ValueError("hello\\nworld") + """) + excinfo = pytest.raises(ValueError, mod.func1) + excinfo.traceback = excinfo.traceback.filter() + p = FormattedExcinfo() + reprtb = p.repr_traceback_entry(excinfo.traceback[-1]) + + # test as intermittent entry + lines = reprtb.lines + assert lines[0] == ' def func1():' + assert lines[1] == '> raise ValueError("hello\\nworld")' + + # test as last entry + p = FormattedExcinfo(showlocals=True) + repr_entry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = repr_entry.lines + assert lines[0] == ' def func1():' + assert lines[1] == '> raise ValueError("hello\\nworld")' + assert lines[2] == 'E ValueError: hello' + assert lines[3] == 'E world' + assert not lines[4:] + + loc = repr_entry.reprlocals is not None + loc = repr_entry.reprfileloc + assert loc.path == mod.__file__ + assert loc.lineno == 3 + #assert loc.message == "ValueError: hello" + + def test_repr_tracebackentry_lines2(self, importasmod): + mod = importasmod(""" + def func1(m, x, y, z): + raise ValueError("hello\\nworld") + """) + excinfo = pytest.raises(ValueError, mod.func1, "m"*90, 5, 13, "z"*120) + excinfo.traceback = excinfo.traceback.filter() + entry = excinfo.traceback[-1] + p = FormattedExcinfo(funcargs=True) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs.args[0] == ('m', repr("m"*90)) + assert reprfuncargs.args[1] == ('x', '5') + assert reprfuncargs.args[2] == ('y', '13') + assert reprfuncargs.args[3] == ('z', repr("z" * 120)) + + p = FormattedExcinfo(funcargs=True) + repr_entry = p.repr_traceback_entry(entry) + assert repr_entry.reprfuncargs.args == reprfuncargs.args + tw = TWMock() + repr_entry.toterminal(tw) + assert tw.lines[0] == "m = " + repr('m' * 90) + assert tw.lines[1] == "x = 5, y = 13" + assert tw.lines[2] == "z = " + repr('z' * 120) + + def test_repr_tracebackentry_lines_var_kw_args(self, importasmod): + mod = importasmod(""" + def func1(x, *y, **z): + raise ValueError("hello\\nworld") + """) + excinfo = pytest.raises(ValueError, mod.func1, 'a', 'b', c='d') + excinfo.traceback = excinfo.traceback.filter() + entry = excinfo.traceback[-1] + p = FormattedExcinfo(funcargs=True) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs.args[0] == ('x', repr('a')) + assert reprfuncargs.args[1] == ('y', repr(('b',))) + assert reprfuncargs.args[2] == ('z', repr({'c': 'd'})) + + p = FormattedExcinfo(funcargs=True) + repr_entry = p.repr_traceback_entry(entry) + assert repr_entry.reprfuncargs.args == reprfuncargs.args + tw = TWMock() + repr_entry.toterminal(tw) + assert tw.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" + + def test_repr_tracebackentry_short(self, importasmod): + mod = importasmod(""" + def func1(): + raise ValueError("hello") + def entry(): + func1() + """) + excinfo = pytest.raises(ValueError, mod.entry) + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) + lines = reprtb.lines + basename = py.path.local(mod.__file__).basename + assert lines[0] == ' func1()' + assert basename in str(reprtb.reprfileloc.path) + assert reprtb.reprfileloc.lineno == 5 + + # test last entry + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = reprtb.lines + assert lines[0] == ' raise ValueError("hello")' + assert lines[1] == 'E ValueError: hello' + assert basename in str(reprtb.reprfileloc.path) + assert reprtb.reprfileloc.lineno == 3 + + def test_repr_tracebackentry_no(self, importasmod): + mod = importasmod(""" + def func1(): + raise ValueError("hello") + def entry(): + func1() + """) + excinfo = pytest.raises(ValueError, mod.entry) + p = FormattedExcinfo(style="no") + p.repr_traceback_entry(excinfo.traceback[-2]) + + p = FormattedExcinfo(style="no") + reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = reprentry.lines + assert lines[0] == 'E ValueError: hello' + assert not lines[1:] + + def test_repr_traceback_tbfilter(self, importasmod): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + p = FormattedExcinfo(tbfilter=True) + reprtb = p.repr_traceback(excinfo) + assert len(reprtb.reprentries) == 2 + p = FormattedExcinfo(tbfilter=False) + reprtb = p.repr_traceback(excinfo) + assert len(reprtb.reprentries) == 3 + + def test_traceback_short_no_source(self, importasmod, monkeypatch): + mod = importasmod(""" + def func1(): + raise ValueError("hello") + def entry(): + func1() + """) + excinfo = pytest.raises(ValueError, mod.entry) + from _pytest._code.code import Code + monkeypatch.setattr(Code, 'path', 'bogus') + excinfo.traceback[0].frame.code.path = "bogus" + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) + lines = reprtb.lines + last_p = FormattedExcinfo(style="short") + last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + last_lines = last_reprtb.lines + monkeypatch.undo() + assert lines[0] == ' func1()' + + assert last_lines[0] == ' raise ValueError("hello")' + assert last_lines[1] == 'E ValueError: hello' + + def test_repr_traceback_and_excinfo(self, importasmod): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + + for style in ("long", "short"): + p = FormattedExcinfo(style=style) + reprtb = p.repr_traceback(excinfo) + assert len(reprtb.reprentries) == 2 + assert reprtb.style == style + assert not reprtb.extraline + repr = p.repr_excinfo(excinfo) + assert repr.reprtraceback + assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries) + assert repr.reprcrash.path.endswith("mod.py") + assert repr.reprcrash.message == "ValueError: 0" + + def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + + p = FormattedExcinfo() + def raiseos(): + raise OSError(2) + monkeypatch.setattr(py.std.os, 'getcwd', raiseos) + assert p._makepath(__file__) == __file__ + p.repr_traceback(excinfo) + + def test_repr_excinfo_addouterr(self, importasmod): + mod = importasmod(""" + def entry(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.entry) + repr = excinfo.getrepr() + repr.addsection("title", "content") + twmock = TWMock() + repr.toterminal(twmock) + assert twmock.lines[-1] == "content" + assert twmock.lines[-2] == ("-", "title") + + def test_repr_excinfo_reprcrash(self, importasmod): + mod = importasmod(""" + def entry(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.entry) + repr = excinfo.getrepr() + assert repr.reprcrash.path.endswith("mod.py") + assert repr.reprcrash.lineno == 3 + assert repr.reprcrash.message == "ValueError" + assert str(repr.reprcrash).endswith("mod.py:3: ValueError") + + def test_repr_traceback_recursion(self, importasmod): + mod = importasmod(""" + def rec2(x): + return rec1(x+1) + def rec1(x): + return rec2(x-1) + def entry(): + rec1(42) + """) + excinfo = pytest.raises(RuntimeError, mod.entry) + + for style in ("short", "long", "no"): + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback(excinfo) + 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): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + + for style in ("short", "long", "no"): + for showlocals in (True, False): + repr = excinfo.getrepr(style=style, showlocals=showlocals) + assert isinstance(repr, ReprExceptionInfo) + assert repr.reprtraceback.style == style + + def test_reprexcinfo_unicode(self): + from _pytest._code.code import TerminalRepr + class MyRepr(TerminalRepr): + def toterminal(self, tw): + tw.line(py.builtin._totext("я", "utf-8")) + x = py.builtin._totext(MyRepr()) + assert x == py.builtin._totext("я", "utf-8") + + def test_toterminal_long(self, importasmod): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + excinfo.traceback = excinfo.traceback.filter() + repr = excinfo.getrepr() + tw = TWMock() + repr.toterminal(tw) + assert tw.lines[0] == "" + tw.lines.pop(0) + assert tw.lines[0] == " def f():" + assert tw.lines[1] == "> g(3)" + assert tw.lines[2] == "" + assert tw.lines[3].endswith("mod.py:5: ") + assert tw.lines[4] == ("_ ", None) + assert tw.lines[5] == "" + assert tw.lines[6] == " def g(x):" + assert tw.lines[7] == "> raise ValueError(x)" + assert tw.lines[8] == "E ValueError: 3" + assert tw.lines[9] == "" + assert tw.lines[10].endswith("mod.py:3: ValueError") + + def test_toterminal_long_missing_source(self, importasmod, tmpdir): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + tmpdir.join('mod.py').remove() + excinfo.traceback = excinfo.traceback.filter() + repr = excinfo.getrepr() + tw = TWMock() + repr.toterminal(tw) + assert tw.lines[0] == "" + tw.lines.pop(0) + assert tw.lines[0] == "> ???" + assert tw.lines[1] == "" + assert tw.lines[2].endswith("mod.py:5: ") + assert tw.lines[3] == ("_ ", None) + assert tw.lines[4] == "" + assert tw.lines[5] == "> ???" + assert tw.lines[6] == "E ValueError: 3" + assert tw.lines[7] == "" + assert tw.lines[8].endswith("mod.py:3: ValueError") + + def test_toterminal_long_incomplete_source(self, importasmod, tmpdir): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + tmpdir.join('mod.py').write('asdf') + excinfo.traceback = excinfo.traceback.filter() + repr = excinfo.getrepr() + tw = TWMock() + repr.toterminal(tw) + assert tw.lines[0] == "" + tw.lines.pop(0) + assert tw.lines[0] == "> ???" + assert tw.lines[1] == "" + assert tw.lines[2].endswith("mod.py:5: ") + assert tw.lines[3] == ("_ ", None) + assert tw.lines[4] == "" + assert tw.lines[5] == "> ???" + assert tw.lines[6] == "E ValueError: 3" + assert tw.lines[7] == "" + assert tw.lines[8].endswith("mod.py:3: ValueError") + + def test_toterminal_long_filenames(self, importasmod): + mod = importasmod(""" + def f(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.f) + tw = TWMock() + path = py.path.local(mod.__file__) + old = path.dirpath().chdir() + try: + repr = excinfo.getrepr(abspath=False) + repr.toterminal(tw) + line = tw.lines[-1] + x = py.path.local().bestrelpath(path) + if len(x) < len(str(path)): + assert line == "mod.py:3: ValueError" + + repr = excinfo.getrepr(abspath=True) + repr.toterminal(tw) + line = tw.lines[-1] + assert line == "%s:3: ValueError" %(path,) + finally: + old.chdir() + + @pytest.mark.parametrize('reproptions', [ + {'style': style, 'showlocals': showlocals, + 'funcargs': funcargs, 'tbfilter': tbfilter + } for style in ("long", "short", "no") + for showlocals in (True, False) + for tbfilter in (True, False) + for funcargs in (True, False)]) + def test_format_excinfo(self, importasmod, reproptions): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + tw = py.io.TerminalWriter(stringio=True) + repr = excinfo.getrepr(**reproptions) + 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(): + g() + def g(): + h() + def h(): + i() + def i(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.f) + excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback[1].set_repr_style("short") + excinfo.traceback[2].set_repr_style("short") + r = excinfo.getrepr(style="long") + tw = TWMock() + r.toterminal(tw) + for line in tw.lines: print (line) + assert tw.lines[0] == "" + assert tw.lines[1] == " def f():" + assert tw.lines[2] == "> g()" + assert tw.lines[3] == "" + assert tw.lines[4].endswith("mod.py:3: ") + assert tw.lines[5] == ("_ ", None) + assert tw.lines[6].endswith("in g") + assert tw.lines[7] == " h()" + assert tw.lines[8].endswith("in h") + assert tw.lines[9] == " i()" + assert tw.lines[10] == ("_ ", None) + assert tw.lines[11] == "" + assert tw.lines[12] == " def i():" + assert tw.lines[13] == "> raise ValueError()" + assert tw.lines[14] == "E ValueError" + assert tw.lines[15] == "" + assert tw.lines[16].endswith("mod.py:9: ValueError") diff --git a/testing/code/test_source.py b/testing/code/test_source.py new file mode 100644 index 000000000..c174e30bb --- /dev/null +++ b/testing/code/test_source.py @@ -0,0 +1,657 @@ +# flake8: noqa +# disable flake check on this file because some constructs are strange +# or redundant on purpose and can't be disable on a line-by-line basis +import sys + +import _pytest._code +import py +import pytest +from _pytest._code import Source +from _pytest._code.source import _ast + +if _ast is not None: + astonly = pytest.mark.nothing +else: + astonly = pytest.mark.xfail("True", reason="only works with AST-compile") + +failsonjython = pytest.mark.xfail("sys.platform.startswith('java')") + +def test_source_str_function(): + x = Source("3") + assert str(x) == "3" + + x = Source(" 3") + assert str(x) == "3" + + x = Source(""" + 3 + """, rstrip=False) + assert str(x) == "\n3\n " + + x = Source(""" + 3 + """, rstrip=True) + assert str(x) == "\n3" + +def test_unicode(): + try: + unicode + except NameError: + return + x = Source(unicode("4")) + assert str(x) == "4" + co = _pytest._code.compile(unicode('u"\xc3\xa5"', 'utf8'), mode='eval') + val = eval(co) + assert isinstance(val, unicode) + +def test_source_from_function(): + source = _pytest._code.Source(test_source_str_function) + assert str(source).startswith('def test_source_str_function():') + +def test_source_from_method(): + class TestClass: + def test_method(self): + pass + source = _pytest._code.Source(TestClass().test_method) + assert source.lines == ["def test_method(self):", + " pass"] + +def test_source_from_lines(): + lines = ["a \n", "b\n", "c"] + source = _pytest._code.Source(lines) + assert source.lines == ['a ', 'b', 'c'] + +def test_source_from_inner_function(): + def f(): + pass + source = _pytest._code.Source(f, deindent=False) + assert str(source).startswith(' def f():') + source = _pytest._code.Source(f) + assert str(source).startswith('def f():') + +def test_source_putaround_simple(): + source = Source("raise ValueError") + source = source.putaround( + "try:", """\ + except ValueError: + x = 42 + else: + x = 23""") + assert str(source)=="""\ +try: + raise ValueError +except ValueError: + x = 42 +else: + x = 23""" + +def test_source_putaround(): + source = Source() + source = source.putaround(""" + if 1: + x=1 + """) + assert str(source).strip() == "if 1:\n x=1" + +def test_source_strips(): + source = Source("") + assert source == Source() + assert str(source) == '' + assert source.strip() == source + +def test_source_strip_multiline(): + source = Source() + source.lines = ["", " hello", " "] + source2 = source.strip() + assert source2.lines == [" hello"] + +def test_syntaxerror_rerepresentation(): + ex = pytest.raises(SyntaxError, _pytest._code.compile, 'xyz xyz') + assert ex.value.lineno == 1 + assert ex.value.offset in (4,7) # XXX pypy/jython versus cpython? + assert ex.value.text.strip(), 'x x' + +def test_isparseable(): + assert Source("hello").isparseable() + assert Source("if 1:\n pass").isparseable() + assert Source(" \nif 1:\n pass").isparseable() + assert not Source("if 1:\n").isparseable() + assert not Source(" \nif 1:\npass").isparseable() + assert not Source(chr(0)).isparseable() + +class TestAccesses: + source = Source("""\ + def f(x): + pass + def g(x): + pass + """) + def test_getrange(self): + x = self.source[0:2] + assert x.isparseable() + assert len(x.lines) == 2 + assert str(x) == "def f(x):\n pass" + + def test_getline(self): + x = self.source[0] + assert x == "def f(x):" + + def test_len(self): + assert len(self.source) == 4 + + def test_iter(self): + l = [x for x in self.source] + assert len(l) == 4 + +class TestSourceParsingAndCompiling: + source = Source("""\ + def f(x): + assert (x == + 3 + + 4) + """).strip() + + def test_compile(self): + co = _pytest._code.compile("x=3") + d = {} + exec (co, d) + assert d['x'] == 3 + + def test_compile_and_getsource_simple(self): + co = _pytest._code.compile("x=3") + exec (co) + source = _pytest._code.Source(co) + assert str(source) == "x=3" + + def test_compile_and_getsource_through_same_function(self): + def gensource(source): + return _pytest._code.compile(source) + co1 = gensource(""" + def f(): + raise KeyError() + """) + co2 = gensource(""" + def f(): + raise ValueError() + """) + source1 = py.std.inspect.getsource(co1) + assert 'KeyError' in source1 + source2 = py.std.inspect.getsource(co2) + assert 'ValueError' in source2 + + def test_getstatement(self): + #print str(self.source) + ass = str(self.source[1:]) + for i in range(1, 4): + #print "trying start in line %r" % self.source[i] + s = self.source.getstatement(i) + #x = s.deindent() + assert str(s) == ass + + def test_getstatementrange_triple_quoted(self): + #print str(self.source) + source = Source("""hello(''' + ''')""") + s = source.getstatement(0) + assert s == str(source) + s = source.getstatement(1) + assert s == str(source) + + @astonly + def test_getstatementrange_within_constructs(self): + source = Source("""\ + try: + try: + raise ValueError + except SomeThing: + pass + finally: + 42 + """) + assert len(source) == 7 + # check all lineno's that could occur in a traceback + #assert source.getstatementrange(0) == (0, 7) + #assert source.getstatementrange(1) == (1, 5) + assert source.getstatementrange(2) == (2, 3) + assert source.getstatementrange(3) == (3, 4) + assert source.getstatementrange(4) == (4, 5) + #assert source.getstatementrange(5) == (0, 7) + assert source.getstatementrange(6) == (6, 7) + + def test_getstatementrange_bug(self): + source = Source("""\ + try: + x = ( + y + + z) + except: + pass + """) + assert len(source) == 6 + assert source.getstatementrange(2) == (1, 4) + + def test_getstatementrange_bug2(self): + source = Source("""\ + assert ( + 33 + == + [ + X(3, + b=1, c=2 + ), + ] + ) + """) + assert len(source) == 9 + assert source.getstatementrange(5) == (0, 9) + + def test_getstatementrange_ast_issue58(self): + source = Source("""\ + + def test_some(): + for a in [a for a in + CAUSE_ERROR]: pass + + x = 3 + """) + assert getstatement(2, source).lines == source.lines[2:3] + assert getstatement(3, source).lines == source.lines[3:4] + + @pytest.mark.skipif("sys.version_info < (2,6)") + def test_getstatementrange_out_of_bounds_py3(self): + source = Source("if xxx:\n from .collections import something") + r = source.getstatementrange(1) + assert r == (1,2) + + def test_getstatementrange_with_syntaxerror_issue7(self): + source = Source(":") + pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) + + @pytest.mark.skipif("sys.version_info < (2,6)") + def test_compile_to_ast(self): + import ast + source = Source("x = 4") + mod = source.compile(flag=ast.PyCF_ONLY_AST) + assert isinstance(mod, ast.Module) + compile(mod, "", "exec") + + def test_compile_and_getsource(self): + co = self.source.compile() + py.builtin.exec_(co, globals()) + f(7) + excinfo = pytest.raises(AssertionError, "f(6)") + frame = excinfo.traceback[-1].frame + stmt = frame.code.fullsource.getstatement(frame.lineno) + #print "block", str(block) + assert str(stmt).strip().startswith('assert') + + def test_compilefuncs_and_path_sanity(self): + def check(comp, name): + co = comp(self.source, name) + if not name: + expected = "codegen %s:%d>" %(mypath, mylineno+2+1) + else: + expected = "codegen %r %s:%d>" % (name, mypath, mylineno+2+1) + fn = co.co_filename + assert fn.endswith(expected) + + mycode = _pytest._code.Code(self.test_compilefuncs_and_path_sanity) + mylineno = mycode.firstlineno + mypath = mycode.path + + for comp in _pytest._code.compile, _pytest._code.Source.compile: + for name in '', None, 'my': + yield check, comp, name + + def test_offsetless_synerr(self): + pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode='eval') + +def test_getstartingblock_singleline(): + class A: + def __init__(self, *args): + frame = sys._getframe(1) + self.source = _pytest._code.Frame(frame).statement + + x = A('x', 'y') + + l = [i for i in x.source.lines if i.strip()] + assert len(l) == 1 + +def test_getstartingblock_multiline(): + class A: + def __init__(self, *args): + frame = sys._getframe(1) + self.source = _pytest._code.Frame(frame).statement + + x = A('x', + 'y' \ + , + 'z') + + l = [i for i in x.source.lines if i.strip()] + assert len(l) == 4 + +def test_getline_finally(): + def c(): pass + excinfo = pytest.raises(TypeError, """ + teardown = None + try: + c(1) + finally: + if teardown: + teardown() + """) + source = excinfo.traceback[-1].statement + assert str(source).strip() == 'c(1)' + +def test_getfuncsource_dynamic(): + source = """ + def f(): + raise ValueError + + def g(): pass + """ + co = _pytest._code.compile(source) + py.builtin.exec_(co, globals()) + assert str(_pytest._code.Source(f)).strip() == 'def f():\n raise ValueError' + assert str(_pytest._code.Source(g)).strip() == 'def g(): pass' + + +def test_getfuncsource_with_multine_string(): + def f(): + c = '''while True: + pass +''' + assert str(_pytest._code.Source(f)).strip() == "def f():\n c = '''while True:\n pass\n'''" + + +def test_deindent(): + from _pytest._code.source import deindent as deindent + assert deindent(['\tfoo', '\tbar', ]) == ['foo', 'bar'] + + def f(): + c = '''while True: + pass +''' + import inspect + lines = deindent(inspect.getsource(f).splitlines()) + assert lines == ["def f():", " c = '''while True:", " pass", "'''"] + + source = """ + def f(): + def g(): + pass + """ + lines = deindent(source.splitlines()) + assert lines == ['', 'def f():', ' def g():', ' pass', ' '] + +@pytest.mark.xfail("sys.version_info[:3] < (2,7,0) or " + "((3,0) <= sys.version_info[:2] < (3,2))") +def test_source_of_class_at_eof_without_newline(tmpdir): + # this test fails because the implicit inspect.getsource(A) below + # does not return the "x = 1" last line. + source = _pytest._code.Source(''' + class A(object): + def method(self): + x = 1 + ''') + path = tmpdir.join("a.py") + path.write(source) + s2 = _pytest._code.Source(tmpdir.join("a.py").pyimport().A) + assert str(source).strip() == str(s2).strip() + +if True: + def x(): + pass + +def test_getsource_fallback(): + from _pytest._code.source import getsource + expected = """def x(): + pass""" + src = getsource(x) + assert src == expected + +def test_idem_compile_and_getsource(): + from _pytest._code.source import getsource + expected = "def x(): pass" + co = _pytest._code.compile(expected) + src = getsource(co) + assert src == expected + +def test_findsource_fallback(): + from _pytest._code.source import findsource + src, lineno = findsource(x) + assert 'test_findsource_simple' in str(src) + assert src[lineno] == ' def x():' + +def test_findsource(): + from _pytest._code.source import findsource + co = _pytest._code.compile("""if 1: + def x(): + pass +""") + + src, lineno = findsource(co) + assert 'if 1:' in str(src) + + d = {} + eval(co, d) + src, lineno = findsource(d['x']) + assert 'if 1:' in str(src) + assert src[lineno] == " def x():" + + +def test_getfslineno(): + from _pytest._code import getfslineno + + def f(x): + pass + + fspath, lineno = getfslineno(f) + + assert fspath.basename == "test_source.py" + assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1 # see findsource + + class A(object): + pass + + fspath, lineno = getfslineno(A) + + _, A_lineno = py.std.inspect.findsource(A) + assert fspath.basename == "test_source.py" + assert lineno == A_lineno + + assert getfslineno(3) == ("", -1) + class B: + pass + B.__name__ = "B2" + assert getfslineno(B)[1] == -1 + +def test_code_of_object_instance_with_call(): + class A: + pass + pytest.raises(TypeError, lambda: _pytest._code.Source(A())) + class WithCall: + def __call__(self): + pass + + code = _pytest._code.Code(WithCall()) + assert 'pass' in str(code.source()) + + class Hello(object): + def __call__(self): + pass + pytest.raises(TypeError, lambda: _pytest._code.Code(Hello)) + + +def getstatement(lineno, source): + from _pytest._code.source import getstatementrange_ast + source = _pytest._code.Source(source, deindent=False) + ast, start, end = getstatementrange_ast(lineno, source) + return source[start:end] + +def test_oneline(): + source = getstatement(0, "raise ValueError") + assert str(source) == "raise ValueError" + +def test_comment_and_no_newline_at_end(): + from _pytest._code.source import getstatementrange_ast + source = Source(['def test_basic_complex():', + ' assert 1 == 2', + '# vim: filetype=pyopencl:fdm=marker']) + ast, start, end = getstatementrange_ast(1, source) + assert end == 2 + +def test_oneline_and_comment(): + source = getstatement(0, "raise ValueError\n#hello") + assert str(source) == "raise ValueError" + +def test_comments(): + source = '''def test(): + "comment 1" + x = 1 + # comment 2 + # comment 3 + + assert False + +""" +comment 4 +""" +''' + for line in range(2,6): + assert str(getstatement(line, source)) == ' x = 1' + for line in range(6,10): + assert str(getstatement(line, source)) == ' assert False' + assert str(getstatement(10, source)) == '"""' + +def test_comment_in_statement(): + source = '''test(foo=1, + # comment 1 + bar=2) +''' + for line in range(1,3): + assert str(getstatement(line, source)) == \ + 'test(foo=1,\n # comment 1\n bar=2)' + +def test_single_line_else(): + source = getstatement(1, "if False: 2\nelse: 3") + assert str(source) == "else: 3" + +def test_single_line_finally(): + source = getstatement(1, "try: 1\nfinally: 3") + assert str(source) == "finally: 3" + +def test_issue55(): + source = ('def round_trip(dinp):\n assert 1 == dinp\n' + 'def test_rt():\n round_trip("""\n""")\n') + s = getstatement(3, source) + assert str(s) == ' round_trip("""\n""")' + + +def XXXtest_multiline(): + source = getstatement(0, """\ +raise ValueError( + 23 +) +x = 3 +""") + assert str(source) == "raise ValueError(\n 23\n)" + +class TestTry: + pytestmark = astonly + source = """\ +try: + raise ValueError +except Something: + raise IndexError(1) +else: + raise KeyError() +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " raise ValueError" + + def test_except_line(self): + source = getstatement(2, self.source) + assert str(source) == "except Something:" + + def test_except_body(self): + source = getstatement(3, self.source) + assert str(source) == " raise IndexError(1)" + + def test_else(self): + source = getstatement(5, self.source) + assert str(source) == " raise KeyError()" + +class TestTryFinally: + source = """\ +try: + raise ValueError +finally: + raise IndexError(1) +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " raise ValueError" + + def test_finally(self): + source = getstatement(3, self.source) + assert str(source) == " raise IndexError(1)" + + + +class TestIf: + pytestmark = astonly + source = """\ +if 1: + y = 3 +elif False: + y = 5 +else: + y = 7 +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " y = 3" + + def test_elif_clause(self): + source = getstatement(2, self.source) + assert str(source) == "elif False:" + + def test_elif(self): + source = getstatement(3, self.source) + assert str(source) == " y = 5" + + def test_else(self): + source = getstatement(5, self.source) + assert str(source) == " y = 7" + +def test_semicolon(): + s = """\ +hello ; pytest.skip() +""" + source = getstatement(0, s) + assert str(source) == s.strip() + +def test_def_online(): + s = """\ +def func(): raise ValueError(42) + +def something(): + pass +""" + source = getstatement(0, s) + assert str(source) == "def func(): raise ValueError(42)" + +def XXX_test_expression_multiline(): + source = """\ +something +''' +'''""" + result = getstatement(1, source) + assert str(result) == "'''\n'''" + diff --git a/testing/python/collect.py b/testing/python/collect.py index bebc13318..752cd81e3 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,6 +1,9 @@ import sys from textwrap import dedent -import pytest, py + +import _pytest._code +import py +import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -598,13 +601,13 @@ class TestConftestCustomization: def test_customized_pymakemodule_issue205_subdir(self, testdir): b = testdir.mkdir("a").mkdir("b") - b.join("conftest.py").write(py.code.Source(""" + b.join("conftest.py").write(_pytest._code.Source(""" def pytest_pycollect_makemodule(__multicall__): mod = __multicall__.execute() mod.obj.hello = "world" return mod """)) - b.join("test_module.py").write(py.code.Source(""" + b.join("test_module.py").write(_pytest._code.Source(""" def test_hello(): assert hello == "world" """)) @@ -613,7 +616,7 @@ class TestConftestCustomization: def test_customized_pymakeitem(self, testdir): b = testdir.mkdir("a").mkdir("b") - b.join("conftest.py").write(py.code.Source(""" + b.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_pycollect_makeitem(): @@ -624,7 +627,7 @@ class TestConftestCustomization: for func in result: func._some123 = "world" """)) - b.join("test_module.py").write(py.code.Source(""" + b.join("test_module.py").write(_pytest._code.Source(""" import pytest @pytest.fixture() @@ -662,7 +665,7 @@ class TestConftestCustomization: def test_setup_only_available_in_subdir(testdir): sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_runtest_setup(item): assert item.fspath.purebasename == "test_in_sub1" @@ -671,7 +674,7 @@ def test_setup_only_available_in_subdir(testdir): def pytest_runtest_teardown(item): assert item.fspath.purebasename == "test_in_sub1" """)) - sub2.join("conftest.py").write(py.code.Source(""" + sub2.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_runtest_setup(item): assert item.fspath.purebasename == "test_in_sub2" @@ -787,7 +790,7 @@ class TestTracebackCutting: except ValueError: _, _, tb = sys.exc_info() - tb = py.code.Traceback(tb) + tb = _pytest._code.Traceback(tb) assert isinstance(tb[-1].path, str) assert not filter_traceback(tb[-1]) @@ -810,7 +813,7 @@ class TestTracebackCutting: _, _, tb = sys.exc_info() testdir.tmpdir.join('filter_traceback_entry_as_str.py').remove() - tb = py.code.Traceback(tb) + tb = _pytest._code.Traceback(tb) assert isinstance(tb[-1].path, str) assert filter_traceback(tb[-1]) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 203176443..506d8426e 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1,9 +1,13 @@ -import pytest, py, sys -from _pytest import python as funcargs -from _pytest.python import FixtureLookupError -from _pytest.pytester import get_public_names from textwrap import dedent +import _pytest._code +import pytest +import sys +from _pytest import python as funcargs +from _pytest.pytester import get_public_names +from _pytest.python import FixtureLookupError + + def test_getfuncargnames(): def f(): pass assert not funcargs.getfuncargnames(f) @@ -86,12 +90,12 @@ class TestFillFixtures: def test_conftest_funcargs_only_available_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_funcarg__arg1(request): pytest.raises(Exception, "request.getfuncargvalue('arg2')") """)) - sub2.join("conftest.py").write(py.code.Source(""" + sub2.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_funcarg__arg2(request): pytest.raises(Exception, "request.getfuncargvalue('arg1')") @@ -156,7 +160,7 @@ class TestFillFixtures: return 'spam' """) pkg = testdir.mkpydir("pkg") - pkg.join("conftest.py").write(py.code.Source(""" + pkg.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture @@ -164,7 +168,7 @@ class TestFillFixtures: return spam * 2 """)) testfile = pkg.join("test_spam.py") - testfile.write(py.code.Source(""" + testfile.write(_pytest._code.Source(""" def test_spam(spam): assert spam == "spamspam" """)) @@ -258,7 +262,7 @@ class TestFillFixtures: return request.param """) subdir = testdir.mkpydir('subdir') - subdir.join("conftest.py").write(py.code.Source(""" + subdir.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture @@ -266,7 +270,7 @@ class TestFillFixtures: return 'spam' """)) testfile = subdir.join("test_spam.py") - testfile.write(py.code.Source(""" + testfile.write(_pytest._code.Source(""" def test_spam(spam): assert spam == "spam" """)) @@ -312,7 +316,7 @@ class TestFillFixtures: return 'spam' """) subdir = testdir.mkpydir('subdir') - subdir.join("conftest.py").write(py.code.Source(""" + subdir.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture(params=[1, 2, 3]) @@ -320,7 +324,7 @@ class TestFillFixtures: return request.param """)) testfile = subdir.join("test_spam.py") - testfile.write(py.code.Source(""" + testfile.write(_pytest._code.Source(""" params = {'spam': 1} def test_spam(spam): @@ -609,7 +613,7 @@ class TestRequestBasic: def test_fixtures_sub_subdir_normalize_sep(self, testdir): # this tests that normalization of nodeids takes place b = testdir.mkdir("tests").mkdir("unit") - b.join("conftest.py").write(py.code.Source(""" + b.join("conftest.py").write(_pytest._code.Source(""" def pytest_funcarg__arg1(): pass """)) @@ -1349,7 +1353,7 @@ class TestAutouseDiscovery: class TestAutouseManagement: def test_autouse_conftest_mid_directory(self, testdir): pkgdir = testdir.mkpydir("xyz123") - pkgdir.join("conftest.py").write(py.code.Source(""" + pkgdir.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture(autouse=True) def app(): @@ -1357,7 +1361,7 @@ class TestAutouseManagement: sys._myapp = "hello" """)) t = pkgdir.ensure("tests", "test_app.py") - t.write(py.code.Source(""" + t.write(_pytest._code.Source(""" import sys def test_app(): assert sys._myapp == "hello" diff --git a/testing/python/integration.py b/testing/python/integration.py index 0c436e32b..dea86f942 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,6 +1,7 @@ import pytest -from _pytest import runner from _pytest import python +from _pytest import runner + class TestOEJSKITSpecials: def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 111ca615a..21869cff9 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import re -import pytest, py +import _pytest._code +import py +import pytest from _pytest import python as funcargs class TestMetafunc: @@ -838,11 +840,11 @@ class TestMetafuncFunctional: def test_generate_tests_only_done_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_1" """)) - sub2.join("conftest.py").write(py.code.Source(""" + sub2.join("conftest.py").write(_pytest._code.Source(""" def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_2" """)) diff --git a/testing/python/raises.py b/testing/python/raises.py index edeb52226..0370f982b 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -38,10 +38,11 @@ class TestRaises: testdir.makepyfile(""" from __future__ import with_statement import py, pytest + import _pytest._code def test_simple(): with pytest.raises(ZeroDivisionError) as excinfo: - assert isinstance(excinfo, py.code.ExceptionInfo) + assert isinstance(excinfo, _pytest._code.ExceptionInfo) 1/0 print (excinfo) assert excinfo.type == ZeroDivisionError diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py index 44b4f23c4..67a352ce7 100644 --- a/testing/test_assertinterpret.py +++ b/testing/test_assertinterpret.py @@ -1,8 +1,9 @@ "PYTEST_DONT_REWRITE" -import pytest, py - +import py +import pytest from _pytest.assertion import util + def exvalue(): return py.std.sys.exc_info()[1] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 34b900f64..f259f7fa1 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -2,8 +2,10 @@ import sys import textwrap -import py, pytest import _pytest.assertion as plugin +import _pytest._code +import py +import pytest from _pytest.assertion import reinterpret from _pytest.assertion import util @@ -22,7 +24,7 @@ def mock_config(): def interpret(expr): - return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1))) + return reinterpret.reinterpret(expr, _pytest._code.Frame(sys._getframe(1))) class TestBinReprIntegration: diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 544250ad5..d06cb3a00 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -10,6 +10,7 @@ if sys.platform.startswith("java"): # XXX should be xfail pytest.skip("assert rewrite does currently not work on jython") +import _pytest._code from _pytest.assertion import util from _pytest.assertion.rewrite import rewrite_asserts, PYTEST_TAG from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -17,7 +18,7 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED def setup_module(mod): mod._old_reprcompare = util._reprcompare - py.code._reprcompare = None + _pytest._code._reprcompare = None def teardown_module(mod): util._reprcompare = mod._old_reprcompare @@ -31,7 +32,7 @@ def rewrite(src): def getmsg(f, extra_ns=None, must_pass=False): """Rewrite the assertions in f, run it, and get the failure message.""" - src = '\n'.join(py.code.Code(f).source().lines) + src = '\n'.join(_pytest._code.Code(f).source().lines) mod = rewrite(src) code = compile(mod, "", "exec") ns = {} @@ -669,7 +670,7 @@ class TestAssertionRewriteHookDetails(object): """Implement optional PEP302 api (#808). """ path = testdir.mkpydir("foo") - path.join("test_foo.py").write(py.code.Source(""" + path.join("test_foo.py").write(_pytest._code.Source(""" class Test: def test_foo(self): import pkgutil diff --git a/testing/test_cache.py b/testing/test_cache.py index adac4a1a6..75557af38 100755 --- a/testing/test_cache.py +++ b/testing/test_cache.py @@ -1,8 +1,9 @@ import sys + +import _pytest import pytest import os import shutil -import py pytest_plugins = "pytester", @@ -129,6 +130,7 @@ def test_cache_show(testdir): class TestLastFailed: + def test_lastfailed_usecase(self, testdir, monkeypatch): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) p = testdir.makepyfile(""" @@ -143,7 +145,7 @@ class TestLastFailed: result.stdout.fnmatch_lines([ "*2 failed*", ]) - p.write(py.code.Source(""" + p.write(_pytest._code.Source(""" def test_1(): assert 1 @@ -175,11 +177,11 @@ class TestLastFailed: ]) def test_failedfirst_order(self, testdir): - testdir.tmpdir.join('test_a.py').write(py.code.Source(""" + testdir.tmpdir.join('test_a.py').write(_pytest._code.Source(""" def test_always_passes(): assert 1 """)) - testdir.tmpdir.join('test_b.py').write(py.code.Source(""" + testdir.tmpdir.join('test_b.py').write(_pytest._code.Source(""" def test_always_fails(): assert 0 """)) @@ -218,7 +220,7 @@ class TestLastFailed: result.stdout.fnmatch_lines([ "*1 failed*", ]) - p2.write(py.code.Source(""" + p2.write(_pytest._code.Source(""" def test_b1(): assert 1 """)) @@ -238,7 +240,7 @@ class TestLastFailed: assert 0 """) p2 = testdir.tmpdir.join("test_something.py") - p2.write(py.code.Source(""" + p2.write(_pytest._code.Source(""" def test_2(): assert 0 """)) diff --git a/testing/test_capture.py b/testing/test_capture.py index b5b374a72..73660692b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -4,6 +4,8 @@ from __future__ import with_statement import pickle import os import sys + +import _pytest._code import py import pytest import contextlib @@ -481,7 +483,7 @@ class TestCaptureFixture: def test_setup_failure_does_not_kill_capturing(testdir): sub1 = testdir.mkpydir("sub1") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" def pytest_runtest_setup(item): raise ValueError(42) """)) diff --git a/testing/test_config.py b/testing/test_config.py index e818dff38..5a984b35d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,5 +1,6 @@ import py, pytest +import _pytest._code from _pytest.config import getcfg, get_common_ancestor, determine_setup from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -7,7 +8,7 @@ class TestParseIni: def test_getcfg_and_config(self, testdir, tmpdir): sub = tmpdir.mkdir("sub") sub.chdir() - tmpdir.join("setup.cfg").write(py.code.Source(""" + tmpdir.join("setup.cfg").write(_pytest._code.Source(""" [pytest] name = value """)) @@ -21,7 +22,7 @@ class TestParseIni: def test_append_parse_args(self, testdir, tmpdir, monkeypatch): monkeypatch.setenv('PYTEST_ADDOPTS', '--color no -rs --tb="short"') - tmpdir.join("setup.cfg").write(py.code.Source(""" + tmpdir.join("setup.cfg").write(_pytest._code.Source(""" [pytest] addopts = --verbose """)) @@ -296,7 +297,7 @@ class TestConfigFromdictargs: assert config.option.capture == 'no' def test_inifilename(self, tmpdir): - tmpdir.join("foo/bar.ini").ensure().write(py.code.Source(""" + tmpdir.join("foo/bar.ini").ensure().write(_pytest._code.Source(""" [pytest] name = value """)) @@ -309,7 +310,7 @@ class TestConfigFromdictargs: } cwd = tmpdir.join('a/b') - cwd.join('pytest.ini').ensure().write(py.code.Source(""" + cwd.join('pytest.ini').ensure().write(_pytest._code.Source(""" [pytest] name = wrong-value should_not_be_set = true diff --git a/testing/test_conftest.py b/testing/test_conftest.py index a0b77cfa5..6f5e77f6d 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,5 +1,8 @@ from textwrap import dedent -import py, pytest + +import _pytest._code +import py +import pytest from _pytest.config import PytestPluginManager from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR @@ -156,7 +159,7 @@ def test_setinitial_conftest_subdirs(testdir, name): def test_conftest_confcutdir(testdir): testdir.makeconftest("assert 0") x = testdir.mkdir("x") - x.join("conftest.py").write(py.code.Source(""" + x.join("conftest.py").write(_pytest._code.Source(""" def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """)) @@ -174,7 +177,7 @@ def test_no_conftest(testdir): def test_conftest_existing_resultlog(testdir): x = testdir.mkdir("tests") - x.join("conftest.py").write(py.code.Source(""" + x.join("conftest.py").write(_pytest._code.Source(""" def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """)) @@ -184,7 +187,7 @@ def test_conftest_existing_resultlog(testdir): def test_conftest_existing_junitxml(testdir): x = testdir.mkdir("tests") - x.join("conftest.py").write(py.code.Source(""" + x.join("conftest.py").write(_pytest._code.Source(""" def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """)) @@ -361,18 +364,18 @@ def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error): root = testdir.tmpdir src = root.join('src').ensure(dir=1) src.join('pytest.ini').write('[pytest]') - src.join('conftest.py').write(py.code.Source(""" + src.join('conftest.py').write(_pytest._code.Source(""" import pytest @pytest.fixture def fix1(): pass """)) - src.join('test_foo.py').write(py.code.Source(""" + src.join('test_foo.py').write(_pytest._code.Source(""" def test_1(fix1): pass def test_2(out_of_reach): pass """)) - root.join('conftest.py').write(py.code.Source(""" + root.join('conftest.py').write(_pytest._code.Source(""" import pytest @pytest.fixture def out_of_reach(): pass diff --git a/testing/test_doctest.py b/testing/test_doctest.py index b05fef478..a4821ee4c 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,7 +1,7 @@ # encoding: utf-8 import sys +import _pytest._code from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile -import py import pytest class TestDoctests: @@ -181,7 +181,7 @@ class TestDoctests: assert 'text-line-after' not in result.stdout.str() def test_doctest_linedata_missing(self, testdir): - testdir.tmpdir.join('hello.py').write(py.code.Source(""" + testdir.tmpdir.join('hello.py').write(_pytest._code.Source(""" class Fun(object): @property def test(self): @@ -201,7 +201,7 @@ class TestDoctests: def test_doctest_unex_importerror(self, testdir): - testdir.tmpdir.join("hello.py").write(py.code.Source(""" + testdir.tmpdir.join("hello.py").write(_pytest._code.Source(""" import asdalsdkjaslkdjasd """)) testdir.maketxtfile(""" @@ -229,7 +229,7 @@ class TestDoctests: def test_doctestmodule_external_and_issue116(self, testdir): p = testdir.mkpydir("hello") - p.join("__init__.py").write(py.code.Source(""" + p.join("__init__.py").write(_pytest._code.Source(""" def somefunc(): ''' >>> i = 0 diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 092b1684e..eeddcf0ae 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1,7 +1,8 @@ - -import py import sys +import _pytest._code + + def runpdb_and_get_report(testdir, source): p = testdir.makepyfile(source) result = testdir.runpytest_inprocess("--pdb", p) @@ -27,7 +28,7 @@ class TestPDB: """) assert rep.failed assert len(pdblist) == 1 - tb = py.code.Traceback(pdblist[0][0]) + tb = _pytest._code.Traceback(pdblist[0][0]) assert tb[-1].name == "test_func" def test_pdb_on_xfail(self, testdir, pdblist): diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index ef1d6d040..74d13f643 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -1,8 +1,12 @@ -import py, pytest import os + +import _pytest._code +import py +import pytest +from _pytest.main import Node, Item, FSCollector from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure -from _pytest.main import Node, Item, FSCollector + def test_generic_path(testdir): from _pytest.main import Session @@ -140,7 +144,7 @@ class TestWithFunctionIntegration: try: raise ValueError except ValueError: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() reslog = ResultLog(None, py.io.TextIO()) reslog.pytest_internalerror(excinfo.getrepr(style=style)) entry = reslog.logfile.getvalue() diff --git a/testing/test_runner.py b/testing/test_runner.py index b01727dbb..6f4a0cee3 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1,6 +1,10 @@ from __future__ import with_statement -import pytest, py, sys, os +import _pytest._code +import os +import py +import pytest +import sys from _pytest import runner, main class TestSetupState: @@ -408,14 +412,14 @@ def test_pytest_exit(): try: pytest.exit("hello") except pytest.exit.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() assert excinfo.errisinstance(KeyboardInterrupt) def test_pytest_fail(): try: pytest.fail("hello") except pytest.fail.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() s = excinfo.exconly(tryshort=True) assert s.startswith("Failed") @@ -459,7 +463,7 @@ def test_exception_printing_skip(): try: pytest.skip("hello") except pytest.skip.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() s = excinfo.exconly(tryshort=True) assert s.startswith("Skipped") @@ -488,7 +492,7 @@ def test_importorskip(monkeypatch): mod2 = pytest.importorskip("hello123", minversion="1.3") assert mod2 == mod except pytest.skip.Exception: - print(py.code.ExceptionInfo()) + print(_pytest._code.ExceptionInfo()) pytest.fail("spurious skip") def test_importorskip_imports_last_module_part(): @@ -505,7 +509,7 @@ def test_importorskip_dev_module(monkeypatch): pytest.raises(pytest.skip.Exception, """ pytest.importorskip('mockmodule1', minversion='0.14.0')""") except pytest.skip.Exception: - print(py.code.ExceptionInfo()) + print(_pytest._code.ExceptionInfo()) pytest.fail("spurious skip") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b5166a22e..a898d9553 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2,15 +2,17 @@ terminal reporting of the full testing process. """ import collections -import pytest -import py import sys +import _pytest._pluggy as pluggy +import _pytest._code +import py +import pytest +from _pytest import runner from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.terminal import TerminalReporter, repr_pythonversion, getreportopt from _pytest.terminal import build_summary_stats_line, _plugin_nameversions -from _pytest import runner -import _pytest._pluggy as pluggy + def basic_run_report(item): runner.call_and_report(item, "setup", log=False) @@ -153,7 +155,7 @@ class TestTerminal: def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir): a = testdir.mkpydir("a123") - a.join("test_hello123.py").write(py.code.Source(""" + a.join("test_hello123.py").write(_pytest._code.Source(""" class TestClass: def test_method(self): pass @@ -268,7 +270,7 @@ class TestCollectonly: p = testdir.makepyfile("import Errlkjqweqwe") result = testdir.runpytest("--collect-only", p) assert result.ret == 1 - result.stdout.fnmatch_lines(py.code.Source(""" + result.stdout.fnmatch_lines(_pytest._code.Source(""" *ERROR* *import Errlk* *ImportError* diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 53dde6ea3..144aad79b 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -260,6 +260,7 @@ def test_testcase_custom_exception_info(testdir, type): testdir.makepyfile(""" from unittest import TestCase import py, pytest + import _pytest._code class MyTestCase(TestCase): def run(self, result): excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0) @@ -269,7 +270,7 @@ def test_testcase_custom_exception_info(testdir, type): def t(*args): mp.undo() raise TypeError() - mp.setattr(py.code, 'ExceptionInfo', t) + mp.setattr(_pytest._code, 'ExceptionInfo', t) try: excinfo = excinfo._excinfo result.add%(type)s(self, excinfo) From 7a6f902f6f9fad7dd679e9d53530080880807e4f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 22 Dec 2015 16:02:07 -0200 Subject: [PATCH 62/69] Drop assertionnew and assertionold from _pytest._code --- _pytest/_code/__init__.py | 5 - _pytest/_code/_assertionnew.py | 339 ----------------- _pytest/_code/_assertionold.py | 561 ---------------------------- _pytest/_code/assertion.py | 96 ----- _pytest/_code/code.py | 12 +- testing/code/test_code_assertion.py | 309 --------------- 6 files changed, 6 insertions(+), 1316 deletions(-) delete mode 100644 _pytest/_code/_assertionnew.py delete mode 100644 _pytest/_code/_assertionold.py delete mode 100644 _pytest/_code/assertion.py delete mode 100644 testing/code/test_code_assertion.py diff --git a/_pytest/_code/__init__.py b/_pytest/_code/__init__.py index 81b61e78a..c046b9716 100644 --- a/_pytest/_code/__init__.py +++ b/_pytest/_code/__init__.py @@ -1,9 +1,4 @@ """ python inspection/code generation API """ -from .assertion import AssertionError as _AssertionError # noqa -from .assertion import _format_explanation # noqa -from .assertion import _reprcompare # noqa -from .assertion import reinterpret as _reinterpret # noqa -from .assertion import reinterpret_old as _reinterpret_old # noqa from .code import Code # noqa from .code import ExceptionInfo # noqa from .code import Frame # noqa diff --git a/_pytest/_code/_assertionnew.py b/_pytest/_code/_assertionnew.py deleted file mode 100644 index a3d807847..000000000 --- a/_pytest/_code/_assertionnew.py +++ /dev/null @@ -1,339 +0,0 @@ -""" -Find intermediate evalutation results in assert statements through builtin AST. -This should replace _assertionold.py eventually. -""" - -import ast -import sys - -from .assertion import _format_explanation, BuiltinAssertionError - -if sys.platform.startswith("java") and sys.version_info < (2, 5, 2): - # 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) - - -class Failure(Exception): - """Error found while interpreting AST.""" - - def __init__(self, explanation=""): - self.cause = sys.exc_info() - self.explanation = explanation - - -def interpret(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 --no-assert)") - -def run(offending_line, frame=None): - from .code import Frame - if frame is None: - frame = Frame(sys._getframe(1)) - return interpret(offending_line, frame) - -def getfailure(failure): - explanation = _format_explanation(failure.explanation) - value = failure.cause[1] - if str(value): - lines = explanation.splitlines() - if not lines: - lines.append("") - lines[0] += " << %s" % (value,) - explanation = "\n".join(lines) - text = "%s: %s" % (failure.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 = False - if not 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 result: - break - except KeyboardInterrupt: - raise - except: - break - left_explanation, left_result = next_explanation, next_result - - import _pytest._code - rcomp = _pytest._code._reprcompare - if rcomp: - res = rcomp(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) - 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) - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - keyword_source = "%s=%%s" % (keyword.arg) - arguments.append(keyword_source % (arg_name,)) - arg_explanations.append(keyword_source % (arg_explanation,)) - if call.starargs: - 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 call.kwargs: - 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: - 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 = True - if 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) - if test_explanation.startswith("False\n{False =") and \ - test_explanation.endswith("\n"): - test_explanation = test_explanation[15:-2] - explanation = "assert %s" % (test_explanation,) - if not test_result: - try: - raise 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/_code/_assertionold.py b/_pytest/_code/_assertionold.py deleted file mode 100644 index 32492b25a..000000000 --- a/_pytest/_code/_assertionold.py +++ /dev/null @@ -1,561 +0,0 @@ -import inspect -import sys - -from compiler import parse, ast, pycodegen - -import py -from _pytest._code.assertion import BuiltinAssertionError, _format_explanation - -passthroughex = py.builtin._sysex - -class Failure: - def __init__(self, node): - self.exc, self.value, self.tb = sys.exc_info() - self.node = node - -class View(object): - """View base class. - - If C is a subclass of View, then C(x) creates a proxy object around - the object x. The actual class of the proxy is not C in general, - but a *subclass* of C determined by the rules below. To avoid confusion - we call view class the class of the proxy (a subclass of C, so of View) - and object class the class of x. - - Attributes and methods not found in the proxy are automatically read on x. - Other operations like setting attributes are performed on the proxy, as - determined by its view class. The object x is available from the proxy - as its __obj__ attribute. - - The view class selection is determined by the __view__ tuples and the - optional __viewkey__ method. By default, the selected view class is the - most specific subclass of C whose __view__ mentions the class of x. - If no such subclass is found, the search proceeds with the parent - object classes. For example, C(True) will first look for a subclass - of C with __view__ = (..., bool, ...) and only if it doesn't find any - look for one with __view__ = (..., int, ...), and then ..., object,... - If everything fails the class C itself is considered to be the default. - - Alternatively, the view class selection can be driven by another aspect - of the object x, instead of the class of x, by overriding __viewkey__. - See last example at the end of this module. - """ - - _viewcache = {} - __view__ = () - - def __new__(rootclass, obj, *args, **kwds): - self = object.__new__(rootclass) - self.__obj__ = obj - self.__rootclass__ = rootclass - key = self.__viewkey__() - try: - self.__class__ = self._viewcache[key] - except KeyError: - self.__class__ = self._selectsubclass(key) - return self - - def __getattr__(self, attr): - # attributes not found in the normal hierarchy rooted on View - # are looked up in the object's real class - return getattr(self.__obj__, attr) - - def __viewkey__(self): - return self.__obj__.__class__ - - def __matchkey__(self, key, subclasses): - if inspect.isclass(key): - keys = inspect.getmro(key) - else: - keys = [key] - for key in keys: - result = [C for C in subclasses if key in C.__view__] - if result: - return result - return [] - - def _selectsubclass(self, key): - subclasses = list(enumsubclasses(self.__rootclass__)) - for C in subclasses: - if not isinstance(C.__view__, tuple): - C.__view__ = (C.__view__,) - choices = self.__matchkey__(key, subclasses) - if not choices: - return self.__rootclass__ - elif len(choices) == 1: - return choices[0] - else: - # combine the multiple choices - return type('?', tuple(choices), {}) - - def __repr__(self): - return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) - - -def enumsubclasses(cls): - for subcls in cls.__subclasses__(): - for subsubclass in enumsubclasses(subcls): - yield subsubclass - yield cls - - -class Interpretable(View): - """A parse tree node with a few extra methods.""" - explanation = None - - def is_builtin(self, frame): - return False - - def eval(self, frame): - # fall-back for unknown expression nodes - try: - expr = ast.Expression(self.__obj__) - expr.filename = '' - self.__obj__.filename = '' - co = pycodegen.ExpressionCodeGenerator(expr).getCode() - result = frame.eval(co) - except passthroughex: - raise - except: - raise Failure(self) - self.result = result - self.explanation = self.explanation or frame.repr(self.result) - - def run(self, frame): - # fall-back for unknown statement nodes - try: - expr = ast.Module(None, ast.Stmt([self.__obj__])) - expr.filename = '' - co = pycodegen.ModuleCodeGenerator(expr).getCode() - frame.exec_(co) - except passthroughex: - raise - except: - raise Failure(self) - - def nice_explanation(self): - return _format_explanation(self.explanation) - - -class Name(Interpretable): - __view__ = ast.Name - - def is_local(self, frame): - source = '%r in locals() is not globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_global(self, frame): - source = '%r in globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_builtin(self, frame): - source = '%r not in locals() and %r not in globals()' % ( - self.name, self.name) - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - super(Name, self).eval(frame) - if not self.is_local(frame): - self.explanation = self.name - -class Compare(Interpretable): - __view__ = ast.Compare - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - for operation, expr2 in self.ops: - if hasattr(self, 'result'): - # shortcutting in chained expressions - if not frame.is_true(self.result): - break - expr2 = Interpretable(expr2) - expr2.eval(frame) - self.explanation = "%s %s %s" % ( - expr.explanation, operation, expr2.explanation) - source = "__exprinfo_left %s __exprinfo_right" % operation - try: - self.result = frame.eval(source, - __exprinfo_left=expr.result, - __exprinfo_right=expr2.result) - except passthroughex: - raise - except: - raise Failure(self) - expr = expr2 - -class And(Interpretable): - __view__ = ast.And - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if not frame.is_true(expr.result): - break - self.explanation = '(' + ' and '.join(explanations) + ')' - -class Or(Interpretable): - __view__ = ast.Or - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if frame.is_true(expr.result): - break - self.explanation = '(' + ' or '.join(explanations) + ')' - - -# == Unary operations == -keepalive = [] -for astclass, astpattern in { - ast.Not : 'not __exprinfo_expr', - ast.Invert : '(~__exprinfo_expr)', - }.items(): - - class UnaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - expr = Interpretable(self.expr) - expr.eval(frame) - self.explanation = astpattern.replace('__exprinfo_expr', - expr.explanation) - try: - self.result = frame.eval(astpattern, - __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(UnaryArith) - -# == Binary operations == -for astclass, astpattern in { - ast.Add : '(__exprinfo_left + __exprinfo_right)', - ast.Sub : '(__exprinfo_left - __exprinfo_right)', - ast.Mul : '(__exprinfo_left * __exprinfo_right)', - ast.Div : '(__exprinfo_left / __exprinfo_right)', - ast.Mod : '(__exprinfo_left % __exprinfo_right)', - ast.Power : '(__exprinfo_left ** __exprinfo_right)', - }.items(): - - class BinaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - left = Interpretable(self.left) - left.eval(frame) - right = Interpretable(self.right) - right.eval(frame) - self.explanation = (astpattern - .replace('__exprinfo_left', left .explanation) - .replace('__exprinfo_right', right.explanation)) - try: - self.result = frame.eval(astpattern, - __exprinfo_left=left.result, - __exprinfo_right=right.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(BinaryArith) - - -class CallFunc(Interpretable): - __view__ = ast.CallFunc - - def is_bool(self, frame): - source = 'isinstance(__exprinfo_value, bool)' - try: - return frame.is_true(frame.eval(source, - __exprinfo_value=self.result)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - node = Interpretable(self.node) - node.eval(frame) - explanations = [] - vars = {'__exprinfo_fn': node.result} - source = '__exprinfo_fn(' - for a in self.args: - if isinstance(a, ast.Keyword): - keyword = a.name - a = a.expr - else: - keyword = None - a = Interpretable(a) - a.eval(frame) - argname = '__exprinfo_%d' % len(vars) - vars[argname] = a.result - if keyword is None: - source += argname + ',' - explanations.append(a.explanation) - else: - source += '%s=%s,' % (keyword, argname) - explanations.append('%s=%s' % (keyword, a.explanation)) - if self.star_args: - star_args = Interpretable(self.star_args) - star_args.eval(frame) - argname = '__exprinfo_star' - vars[argname] = star_args.result - source += '*' + argname + ',' - explanations.append('*' + star_args.explanation) - if self.dstar_args: - dstar_args = Interpretable(self.dstar_args) - dstar_args.eval(frame) - argname = '__exprinfo_kwds' - vars[argname] = dstar_args.result - source += '**' + argname + ',' - explanations.append('**' + dstar_args.explanation) - self.explanation = "%s(%s)" % ( - node.explanation, ', '.join(explanations)) - if source.endswith(','): - source = source[:-1] - source += ')' - try: - self.result = frame.eval(source, **vars) - except passthroughex: - raise - except: - raise Failure(self) - if not node.is_builtin(frame) or not self.is_bool(frame): - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -class Getattr(Interpretable): - __view__ = ast.Getattr - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - source = '__exprinfo_expr.%s' % self.attrname - try: - self.result = frame.eval(source, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - self.explanation = '%s.%s' % (expr.explanation, self.attrname) - # if the attribute comes from the instance, its value is interesting - source = ('hasattr(__exprinfo_expr, "__dict__") and ' - '%r in __exprinfo_expr.__dict__' % self.attrname) - try: - from_instance = frame.is_true( - frame.eval(source, __exprinfo_expr=expr.result)) - except passthroughex: - raise - except: - from_instance = True - if from_instance: - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -# == Re-interpretation of full statements == - -class Assert(Interpretable): - __view__ = ast.Assert - - def run(self, frame): - test = Interpretable(self.test) - test.eval(frame) - # simplify 'assert False where False = ...' - if (test.explanation.startswith('False\n{False = ') and - test.explanation.endswith('\n}')): - test.explanation = test.explanation[15:-2] - # print the result as 'assert ' - self.result = test.result - self.explanation = 'assert ' + test.explanation - if not frame.is_true(test.result): - try: - raise BuiltinAssertionError - except passthroughex: - raise - except: - raise Failure(self) - -class Assign(Interpretable): - __view__ = ast.Assign - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = '... = ' + expr.explanation - # fall-back-run the rest of the assignment - ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr')) - mod = ast.Module(None, ast.Stmt([ass])) - mod.filename = '' - co = pycodegen.ModuleCodeGenerator(mod).getCode() - try: - frame.exec_(co, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - -class Discard(Interpretable): - __view__ = ast.Discard - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = expr.explanation - -class Stmt(Interpretable): - __view__ = ast.Stmt - - def run(self, frame): - for stmt in self.nodes: - stmt = Interpretable(stmt) - stmt.run(frame) - - -def report_failure(e): - explanation = e.node.nice_explanation() - if explanation: - explanation = ", in: " + explanation - else: - explanation = "" - sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation)) - -def check(s, frame=None): - from _pytest._code import Frame - if frame is None: - frame = sys._getframe(1) - frame = Frame(frame) - expr = parse(s, 'eval') - assert isinstance(expr, ast.Expression) - node = Interpretable(expr.node) - try: - node.eval(frame) - except passthroughex: - raise - except Failure: - e = sys.exc_info()[1] - report_failure(e) - else: - if not frame.is_true(node.result): - sys.stderr.write("assertion failed: %s\n" % node.nice_explanation()) - - -########################################################### -# API / Entry points -# ######################################################### - -def interpret(source, frame, should_fail=False): - module = Interpretable(parse(source, 'exec').node) - import _pytest._code - if isinstance(frame, py.std.types.FrameType): - frame = _pytest._code.Frame(frame) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - return getfailure(e) - except passthroughex: - raise - except: - import traceback - traceback.print_exc() - 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 --nomagic)") - else: - return None - -def getmsg(excinfo): - import _pytest._code - if isinstance(excinfo, tuple): - excinfo = _pytest._code.ExceptionInfo(excinfo) - #frame, line = gettbline(tb) - #frame = pytest.code.Frame(frame) - #return interpret(line, frame) - - tb = excinfo.traceback[-1] - source = str(tb.statement).strip() - x = interpret(source, tb.frame, should_fail=True) - if not isinstance(x, str): - raise TypeError("interpret returned non-string %r" % (x,)) - return x - -def getfailure(e): - explanation = e.node.nice_explanation() - if str(e.value): - lines = explanation.split('\n') - lines[0] += " << %s" % (e.value,) - explanation = '\n'.join(lines) - text = "%s: %s" % (e.exc.__name__, explanation) - if text.startswith('AssertionError: assert '): - text = text[16:] - return text - -def run(s, frame=None): - import _pytest._code - if frame is None: - frame = sys._getframe(1) - frame = _pytest._code.Frame(frame) - module = Interpretable(parse(s, 'exec').node) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - report_failure(e) - - -if __name__ == '__main__': - # example: - def f(): - return 5 - def g(): - return 3 - def h(x): - return 'never' - check("f() * g() == 5") - check("not f()") - check("not (f() and g() or 0)") - check("f() == g()") - i = 4 - check("i == f()") - check("len(f()) == 0") - check("isinstance(2+3+4, float)") - - run("x = i") - check("x == 5") - - run("assert not f(), 'oops'") - run("a, b, c = 1, 2") - run("a, b, c = f()") - - check("max([f(),g()]) == 4") - check("'hello'[g()] == 'h'") - run("'guk%d' % h(f())") diff --git a/_pytest/_code/assertion.py b/_pytest/_code/assertion.py deleted file mode 100644 index f5e0d65c4..000000000 --- a/_pytest/_code/assertion.py +++ /dev/null @@ -1,96 +0,0 @@ -import sys - -import py -from .code import Frame - -BuiltinAssertionError = py.builtin.builtins.AssertionError - -_reprcompare = None # if set, will be called by assert reinterp for comparison ops - -def _format_explanation(explanation): - """This formats an explanation - - Normally all embedded newlines are escaped, however there are - three exceptions: \n{, \n} and \n~. The first two are intended - cover nested explanations, see function and attribute explanations - for examples (.visit_Call(), visit_Attribute()). The last one is - for when one explanation needs to span multiple lines, e.g. when - displaying diffs. - """ - raw_lines = (explanation or '').split('\n') - # escape newlines not followed by {, } and ~ - lines = [raw_lines[0]] - for l in raw_lines[1:]: - if l.startswith('{') or l.startswith('}') or l.startswith('~'): - lines.append(l) - else: - lines[-1] += '\\n' + l - - result = lines[:1] - stack = [0] - stackcnt = [0] - for line in lines[1:]: - if line.startswith('{'): - if stackcnt[-1]: - s = 'and ' - else: - s = 'where ' - stack.append(len(result)) - stackcnt[-1] += 1 - stackcnt.append(0) - result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) - elif line.startswith('}'): - assert line.startswith('}') - stack.pop() - stackcnt.pop() - result[stack[-1]] += line[1:] - else: - assert line.startswith('~') - result.append(' '*len(stack) + line[1:]) - assert len(stack) == 1 - return '\n'.join(result) - - -class AssertionError(BuiltinAssertionError): - def __init__(self, *args): - BuiltinAssertionError.__init__(self, *args) - if args: - try: - self.msg = str(args[0]) - except py.builtin._sysex: - raise - except: - self.msg = "<[broken __repr__] %s at %0xd>" %( - args[0].__class__, id(args[0])) - else: - f = 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" - reinterpret_old = "old reinterpretation not available for py3" -else: - from _pytest._code._assertionold import interpret as reinterpret_old -if sys.version_info >= (2, 6) or (sys.platform.startswith("java")): - from _pytest._code._assertionnew import interpret as reinterpret -else: - reinterpret = reinterpret_old - diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index c705249ba..bc68aac55 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -177,11 +177,11 @@ class TracebackEntry(object): def reinterpret(self): """Reinterpret the failing statement and returns a detailed information about what operations are performed.""" - import _pytest._code + from _pytest.assertion.reinterpret import reinterpret if self.exprinfo is None: - source = str(self.statement).strip() - x = _pytest._code._reinterpret(source, self.frame, should_fail=True) - if not isinstance(x, str): + 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 @@ -760,10 +760,10 @@ oldbuiltins = {} def patch_builtins(assertion=True, compile=True): """ put compile and AssertionError builtins to Python's builtins. """ if assertion: - from _pytest._code import assertion + from _pytest.assertion import reinterpret l = oldbuiltins.setdefault('AssertionError', []) l.append(py.builtin.builtins.AssertionError) - py.builtin.builtins.AssertionError = assertion.AssertionError + py.builtin.builtins.AssertionError = reinterpret.AssertionError if compile: import _pytest._code l = oldbuiltins.setdefault('compile', []) diff --git a/testing/code/test_code_assertion.py b/testing/code/test_code_assertion.py deleted file mode 100644 index 13f230edb..000000000 --- a/testing/code/test_code_assertion.py +++ /dev/null @@ -1,309 +0,0 @@ -import pytest, py - -def exvalue(): - return py.std.sys.exc_info()[1] - -def f(): - return 2 - -def test_assert(): - try: - assert f() == 3 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 == 3\n') - - -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") - - -@pytest.mark.skipif("sys.version_info < (2,6)") -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") - -@pytest.mark.skipif("sys.version_info < (2,6)") -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_keyword_arg(): - def f(x=3): - return False - try: - assert f(x=5) - except AssertionError: - e = exvalue() - assert "x=5" 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 - - -class TestView: - - def setup_class(cls): - cls.View = pytest.importorskip("_pytest._code._assertionold").View - - def test_class_dispatch(self): - ### Use a custom class hierarchy with existing instances - - class Picklable(self.View): - pass - - class Simple(Picklable): - __view__ = object - def pickle(self): - return repr(self.__obj__) - - class Seq(Picklable): - __view__ = list, tuple, dict - def pickle(self): - return ';'.join( - [Picklable(item).pickle() for item in self.__obj__]) - - class Dict(Seq): - __view__ = dict - def pickle(self): - return Seq.pickle(self) + '!' + Seq(self.values()).pickle() - - assert Picklable(123).pickle() == '123' - assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4' - assert Picklable({1:2}).pickle() == '1!2' - - def test_viewtype_class_hierarchy(self): - # Use a custom class hierarchy based on attributes of existing instances - class Operation: - "Existing class that I don't want to change." - def __init__(self, opname, *args): - self.opname = opname - self.args = args - - existing = [Operation('+', 4, 5), - Operation('getitem', '', 'join'), - Operation('setattr', 'x', 'y', 3), - Operation('-', 12, 1)] - - class PyOp(self.View): - def __viewkey__(self): - return self.opname - def generate(self): - return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args))) - - class PyBinaryOp(PyOp): - __view__ = ('+', '-', '*', '/') - def generate(self): - return '%s %s %s' % (self.args[0], self.opname, self.args[1]) - - codelines = [PyOp(op).generate() for op in existing] - assert codelines == ["4 + 5", "getitem('', 'join')", - "setattr('x', 'y', 3)", "12 - 1"] - -def test_underscore_api(): - import _pytest._code - _pytest._code._AssertionError - _pytest._code._reinterpret_old # used by pypy - _pytest._code._reinterpret - -@pytest.mark.skipif("sys.version_info < (2,6)") -def test_assert_customizable_reprcompare(monkeypatch): - util = pytest.importorskip("_pytest.assertion.util") - 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(""" - 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*", - ]) - - -@pytest.mark.skipif("sys.version_info < (2,5)") -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 From 9f85d4c9527d83de8f30b7169aa64f4713e7f2f5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 23 Dec 2015 01:44:44 -0200 Subject: [PATCH 63/69] mark test_comments as xfail on pypy while migrating this code it was noticed that this test was failing even on the original py repository, so it was decided to xfail it and investigate later --- testing/code/test_source.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index c174e30bb..007ad1433 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -506,6 +506,8 @@ def test_oneline_and_comment(): source = getstatement(0, "raise ValueError\n#hello") assert str(source) == "raise ValueError" +@pytest.mark.xfail(hasattr(sys, "pypy_version_info"), + reason='does not work on pypy') def test_comments(): source = '''def test(): "comment 1" From e43eaffd9335824b4dd2c9c6fdb38606a0ac325b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Jan 2016 23:30:53 -0200 Subject: [PATCH 64/69] Remove unused import --- _pytest/doctest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/_pytest/doctest.py b/_pytest/doctest.py index c3fa94e7e..a57f7a494 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -3,7 +3,6 @@ from __future__ import absolute_import import traceback -import py import pytest from _pytest._code.code import TerminalRepr, ReprFileLocation, ExceptionInfo from _pytest.python import FixtureRequest From 4825678e1aec9da0b4ce06ed8223fa6a2774e26d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Jan 2016 23:34:07 -0200 Subject: [PATCH 65/69] Add CHANGELOG entry for py.code merge --- CHANGELOG.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ec058121..e1a8b1588 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,21 @@ **Changes** +* **Important**: `py.code `_ has been + merged into the ``pytest`` repository as ``pytest._code``. This decision + was made because ``py.code`` had very few uses outside ``pytest`` and the + fact that it was in a different repository made it difficult to fix bugs on + its code in a timely manner. The team hopes with this to be able to better + refactor out and improve that code. + This change shouldn't affect users, but it is useful to let users aware + if they encounter any strange behavior. + + Keep in mind that the code for ``pytest._code`` is **private** and + **experimental**, so you definitely should not import it explicitly! + + Please note that the original ``py.code`` is still available in + `pylib `_. + * ``pytest_enter_pdb`` now optionally receives the pytest config object. Thanks `@nicoddemus`_ for the PR. From e35ce98f898b8e9d3f5637aff02355c61b8e8b1b Mon Sep 17 00:00:00 2001 From: Michael Aquilina Date: Wed, 27 Jan 2016 21:28:38 +0000 Subject: [PATCH 66/69] Add explicit flag names for failed first and last failed --- _pytest/cacheprovider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/cacheprovider.py b/_pytest/cacheprovider.py index afd852de9..e5c11a878 100755 --- a/_pytest/cacheprovider.py +++ b/_pytest/cacheprovider.py @@ -155,11 +155,11 @@ class LFPlugin: def pytest_addoption(parser): group = parser.getgroup("general") group.addoption( - '--lf', action='store_true', dest="lf", + '--lf', '--last-failed', action='store_true', dest="lf", help="rerun only the tests that failed " "at the last run (or all if none failed)") group.addoption( - '--ff', action='store_true', dest="failedfirst", + '--ff', '--failed-first', action='store_true', dest="failedfirst", help="run all tests but run the last failures first. " "This may re-order tests and thus lead to " "repeated fixture setup/teardown") From 74a68b5ec676335221275587ea328a45da022727 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 27 Jan 2016 19:57:11 -0200 Subject: [PATCH 67/69] Add CHANGELOG and docs for #1345 --- CHANGELOG.rst | 4 ++++ doc/en/cache.rst | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e1a8b1588..4146f71a9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,10 @@ found in the environment, even when -vv isn't used. Thanks `@The-Compiler`_ for the PR. +* ``--lf`` and ``--ff`` now support long names: ``--last-failed`` and + ``--failed-first`` respectively. + Thanks `@MichaelAquilina`_ for the PR. + **Bug Fixes** diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 3c3b5eb22..c04b98fbb 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -17,8 +17,8 @@ Usage The plugin provides two command line options to rerun failures from the last ``py.test`` invocation: -* ``--lf`` (last failures) - to only re-run the failures. -* ``--ff`` (failures first) - to run the failures first and then the rest of +* ``--lf``, ``--last-failed`` - to only re-run the failures. +* ``--ff``, ``--failed-first`` - to run the failures first and then the rest of the tests. For cleanup (usually not needed), a ``--cache-clear`` option allows to remove From 79722ae89b7c26f9d03a08fe2b2de6873a40b1fe Mon Sep 17 00:00:00 2001 From: Georgy Dyuldin Date: Wed, 3 Feb 2016 12:01:03 +0300 Subject: [PATCH 68/69] Add expected exceptions to 'DID NOT RAISE' msg --- CHANGELOG.rst | 1 + _pytest/python.py | 3 +-- testing/python/raises.py | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4146f71a9..235c823e9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -49,6 +49,7 @@ ``--failed-first`` respectively. Thanks `@MichaelAquilina`_ for the PR. +* Added expected exceptions to pytest.raises fail message **Bug Fixes** diff --git a/_pytest/python.py b/_pytest/python.py index 065971be6..d5612a584 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1311,7 +1311,7 @@ def raises(expected_exception, *args, **kwargs): func(*args[1:], **kwargs) except expected_exception: return _pytest._code.ExceptionInfo() - pytest.fail("DID NOT RAISE") + pytest.fail("DID NOT RAISE {0}".format(expected_exception)) class RaisesContext(object): def __init__(self, expected_exception): @@ -1770,7 +1770,6 @@ class FixtureLookupError(LookupError): # the last fixture raise an error, let's present # it at the requesting side stack = stack[:-1] - for function in stack: fspath, lineno = getfslineno(function) try: diff --git a/testing/python/raises.py b/testing/python/raises.py index 0370f982b..0ea7f9bee 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -70,3 +70,9 @@ class TestRaises: def test_tuple(self): with pytest.raises((KeyError, ValueError)): raise KeyError('oops') + + def test_no_raise_message(self): + try: + pytest.raises(ValueError, int, '0') + except pytest.raises.Exception as e: + assert e.msg == "DID NOT RAISE {0}".format(repr(ValueError)) From 190a52badb74de84f4bb005a2d8fe1429d02f3cc Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 5 Feb 2016 01:18:11 +0100 Subject: [PATCH 69/69] correct merge misstake in appveyor.yml --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0b9280c86..4b73645f7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,8 +22,7 @@ install: build: false # Not a C# project, build stuff at the test step instead. test_script: - - C:\Python35\python -m tox - linting, + - C:\Python35\python -m tox # coveralls is not in tox's envlist, plus for PRs the secure variable # is not defined so we have to check for it - if defined COVERALLS_REPO_TOKEN C:\Python35\python -m tox -e coveralls