From commits-noreply at bitbucket.org Sun May 1 12:39:06 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sun, 01 May 2011 10:39:06 -0000 Subject: [py-svn] commit/pytest: hpk42: regen examples to use 2.0.3 version number Message-ID: <20110501103906.1533.55721@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/4305864420c8/ changeset: r2203:4305864420c8 user: hpk42 date: 2011-05-01 12:38:56 summary: regen examples to use 2.0.3 version number affected #: 16 files (107 bytes) --- a/doc/assert.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/assert.txt Sun May 01 12:38:56 2011 +0200 @@ -23,7 +23,7 @@ $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_assert1.py F @@ -37,7 +37,7 @@ E + where 3 = f() test_assert1.py:5: AssertionError - ========================= 1 failed in 0.07 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= Reporting details about the failing assertion is achieved by re-evaluating the assert expression and recording the intermediate values. @@ -108,7 +108,7 @@ $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_assert2.py F @@ -127,7 +127,7 @@ E '5' test_assert2.py:5: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.03 seconds ========================= Special comparisons are done for a number of cases: --- a/doc/capture.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/capture.txt Sun May 01 12:38:56 2011 +0200 @@ -64,7 +64,7 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items test_module.py .F @@ -78,7 +78,7 @@ test_module.py:9: AssertionError ----------------------------- Captured stdout ------------------------------ - setting up + setting up ==================== 1 failed, 1 passed in 0.02 seconds ==================== Accessing captured output from a test function --- a/doc/doctest.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/doctest.txt Sun May 01 12:38:56 2011 +0200 @@ -44,9 +44,9 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items mymodule.py . - ========================= 1 passed in 0.01 seconds ========================= + ========================= 1 passed in 0.40 seconds ========================= --- a/doc/example/mysetup.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/example/mysetup.txt Sun May 01 12:38:56 2011 +0200 @@ -49,7 +49,7 @@ $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_sample.py F @@ -57,7 +57,7 @@ ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,14 +122,14 @@ $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-36/conftest.py:22: specify ssh host with --ssh + SKIP [1] /tmp/doc-exec-37/conftest.py:22: specify ssh host with --ssh - ======================== 1 skipped in 0.02 seconds ========================= + ======================== 1 skipped in 0.01 seconds ========================= If you specify a command line option like ``py.test --ssh=python.org`` the test will execute as expected. --- a/doc/example/nonpython.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/example/nonpython.txt Sun May 01 12:38:56 2011 +0200 @@ -27,7 +27,7 @@ nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items test_simple.yml .F @@ -37,7 +37,7 @@ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.51 seconds ==================== + ==================== 1 failed, 1 passed in 0.24 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -56,7 +56,7 @@ nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 -- /home/hpk/venv/0/bin/python collecting ... collected 2 items test_simple.yml:1: usecase: ok PASSED @@ -67,17 +67,17 @@ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.07 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items - ============================= in 0.06 seconds ============================= + ============================= in 0.07 seconds ============================= --- a/doc/example/parametrize.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/example/parametrize.txt Sun May 01 12:38:56 2011 +0200 @@ -62,7 +62,7 @@ E assert 4 < 4 test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.02 seconds + 1 failed, 4 passed in 0.03 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. @@ -114,13 +114,13 @@ $ py.test test_backends.py --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items - ============================= in 0.00 seconds ============================= + ============================= in 0.01 seconds ============================= And then when we run the test:: @@ -130,7 +130,7 @@ ================================= FAILURES ================================= __________________________ test_db_initialized[1] __________________________ - db = + db = def test_db_initialized(db): # a dummy test @@ -139,7 +139,7 @@ E Failed: deliberately failing for demo purposes test_backends.py:6: Failed - 1 failed, 1 passed in 0.02 seconds + 1 failed, 1 passed in 0.03 seconds Now you see that one invocation of the test passes and another fails, as it to be expected. @@ -184,7 +184,7 @@ ================================= FAILURES ================================= __________________________ test_db_initialized[1] __________________________ - db = + db = def test_db_initialized(db): # a dummy test @@ -195,7 +195,7 @@ test_backends.py:6: Failed _________________________ TestClass.test_equals[0] _________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 def test_equals(self, a, b): > assert a == b @@ -204,14 +204,14 @@ test_parametrize.py:17: AssertionError ______________________ TestClass.test_zerodivision[1] ______________________ - self = , a = 3, b = 2 + self = , a = 3, b = 2 def test_zerodivision(self, a, b): > pytest.raises(ZeroDivisionError, "a/b") E Failed: DID NOT RAISE test_parametrize.py:20: Failed - 3 failed, 3 passed in 0.03 seconds + 3 failed, 3 passed in 0.05 seconds Parametrizing test methods through a decorator -------------------------------------------------------------- @@ -252,7 +252,7 @@ ================================= FAILURES ================================= _________________________ TestClass.test_equals[0] _________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 @params([dict(a=1, b=2), dict(a=3, b=3), ]) def test_equals(self, a, b): @@ -262,7 +262,7 @@ test_parametrize2.py:19: AssertionError ______________________ TestClass.test_zerodivision[1] ______________________ - self = , a = 3, b = 2 + self = , a = 3, b = 2 @params([dict(a=1, b=0), dict(a=3, b=2)]) def test_zerodivision(self, a, b): @@ -270,7 +270,7 @@ E Failed: DID NOT RAISE test_parametrize2.py:23: Failed - 2 failed, 2 passed in 0.02 seconds + 2 failed, 2 passed in 0.03 seconds checking serialization between Python interpreters -------------------------------------------------------------- @@ -291,4 +291,4 @@ . $ py.test -q multipython.py collecting ... collected 75 items ....s....s....s....ssssss....s....s....s....ssssss....s....s....s....ssssss - 48 passed, 27 skipped in 3.74 seconds + 48 passed, 27 skipped in 2.04 seconds --- a/doc/example/pythoncollection.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/example/pythoncollection.txt Sun May 01 12:38:56 2011 +0200 @@ -43,7 +43,7 @@ $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items @@ -82,7 +82,7 @@ . $ py.test --collectonly pythoncollection.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 3 items @@ -91,4 +91,4 @@ - ============================= in 0.06 seconds ============================= + ============================= in 0.01 seconds ============================= --- a/doc/example/reportingdemo.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/example/reportingdemo.txt Sun May 01 12:38:56 2011 +0200 @@ -13,7 +13,7 @@ assertion $ py.test failure_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 39 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF @@ -30,7 +30,7 @@ failure_demo.py:15: AssertionError _________________________ TestFailing.test_simple __________________________ - self = + self = def test_simple(self): def f(): @@ -40,13 +40,13 @@ > assert f() == g() E assert 42 == 43 - E + where 42 = () - E + and 43 = () + E + where 42 = () + E + and 43 = () failure_demo.py:28: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ - self = + self = def test_simple_multiline(self): otherfunc_multi( @@ -66,19 +66,19 @@ failure_demo.py:12: AssertionError ___________________________ TestFailing.test_not ___________________________ - self = + self = def test_not(self): def f(): return 42 > assert not f() E assert not 42 - E + where 42 = () + E + where 42 = () failure_demo.py:38: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ - self = + self = def test_eq_text(self): > assert 'spam' == 'eggs' @@ -89,7 +89,7 @@ failure_demo.py:42: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ - self = + self = def test_eq_similar_text(self): > assert 'foo 1 bar' == 'foo 2 bar' @@ -102,7 +102,7 @@ failure_demo.py:45: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ - self = + self = def test_eq_multiline_text(self): > assert 'foo\nspam\nbar' == 'foo\neggs\nbar' @@ -115,7 +115,7 @@ failure_demo.py:48: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ - self = + self = def test_eq_long_text(self): a = '1'*100 + 'a' + '2'*100 @@ -132,7 +132,7 @@ failure_demo.py:53: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ - self = + self = def test_eq_long_text_multiline(self): a = '1\n'*100 + 'a' + '2\n'*100 @@ -156,7 +156,7 @@ failure_demo.py:58: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ - self = + self = def test_eq_list(self): > assert [0, 1, 2] == [0, 1, 3] @@ -166,7 +166,7 @@ failure_demo.py:61: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ - self = + self = def test_eq_list_long(self): a = [0]*100 + [1] + [3]*100 @@ -178,7 +178,7 @@ failure_demo.py:66: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ - self = + self = def test_eq_dict(self): > assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} @@ -191,7 +191,7 @@ failure_demo.py:69: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ - self = + self = def test_eq_set(self): > assert set([0, 10, 11, 12]) == set([0, 20, 21]) @@ -207,7 +207,7 @@ failure_demo.py:72: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ - self = + self = def test_eq_longer_list(self): > assert [1,2] == [1,2,3] @@ -217,7 +217,7 @@ failure_demo.py:75: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ - self = + self = def test_in_list(self): > assert 1 in [0, 2, 3, 4, 5] @@ -226,7 +226,7 @@ failure_demo.py:78: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ - self = + self = def test_not_in_text_multiline(self): text = 'some multiline\ntext\nwhich\nincludes foo\nand a\ntail' @@ -244,7 +244,7 @@ failure_demo.py:82: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ - self = + self = def test_not_in_text_single(self): text = 'single foo line' @@ -257,7 +257,7 @@ failure_demo.py:86: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ - self = + self = def test_not_in_text_single_long(self): text = 'head ' * 50 + 'foo ' + 'tail ' * 20 @@ -270,7 +270,7 @@ failure_demo.py:90: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ - self = + self = def test_not_in_text_single_long_term(self): text = 'head ' * 50 + 'f'*70 + 'tail ' * 20 @@ -289,7 +289,7 @@ i = Foo() > assert i.b == 2 E assert 1 == 2 - E + where 1 = .b + E + where 1 = .b failure_demo.py:101: AssertionError _________________________ test_attribute_instance __________________________ @@ -299,8 +299,8 @@ b = 1 > assert Foo().b == 2 E assert 1 == 2 - E + where 1 = .b - E + where = () + E + where 1 = .b + E + where = () failure_demo.py:107: AssertionError __________________________ test_attribute_failure __________________________ @@ -316,7 +316,7 @@ failure_demo.py:116: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - self = + self = def _get_b(self): > raise Exception('Failed to get attrib') @@ -332,15 +332,15 @@ b = 2 > assert Foo().b == Bar().b E assert 1 == 2 - E + where 1 = .b - E + where = () - E + and 2 = .b - E + where = () + E + where 1 = .b + E + where = () + E + and 2 = .b + E + where = () failure_demo.py:124: AssertionError __________________________ TestRaises.test_raises __________________________ - self = + self = def test_raises(self): s = 'qwe' @@ -352,10 +352,10 @@ > int(s) E ValueError: invalid literal for int() with base 10: 'qwe' - <0-codegen /home/hpk/p/pytest/_pytest/python.py:837>:1: ValueError + <0-codegen /home/hpk/p/pytest/_pytest/python.py:831>:1: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ - self = + self = def test_raises_doesnt(self): > raises(IOError, "int('3')") @@ -364,7 +364,7 @@ failure_demo.py:136: Failed __________________________ TestRaises.test_raise ___________________________ - self = + self = def test_raise(self): > raise ValueError("demo error") @@ -373,7 +373,7 @@ failure_demo.py:139: ValueError ________________________ TestRaises.test_tupleerror ________________________ - self = + self = def test_tupleerror(self): > a,b = [1] @@ -382,7 +382,7 @@ failure_demo.py:142: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ - self = + self = def test_reinterpret_fails_with_print_for_the_fun_of_it(self): l = [1,2,3] @@ -395,7 +395,7 @@ l is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ - self = + self = def test_some_error(self): > if namenotexi: @@ -423,7 +423,7 @@ <2-codegen 'abc-123' /home/hpk/p/pytest/doc/example/assertion/failure_demo.py:162>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ - self = + self = def test_complex_error(self): def f(): @@ -452,7 +452,7 @@ failure_demo.py:5: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ - self = + self = def test_z1_unpack_error(self): l = [] @@ -462,7 +462,7 @@ failure_demo.py:179: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ - self = + self = def test_z2_type_error(self): l = 3 @@ -472,20 +472,20 @@ failure_demo.py:183: TypeError ______________________ TestMoreErrors.test_startswith ______________________ - self = + self = def test_startswith(self): s = "123" g = "456" > assert s.startswith(g) E assert False - E + where False = ('456') - E + where = '123'.startswith + E + where False = ('456') + E + where = '123'.startswith failure_demo.py:188: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ - self = + self = def test_startswith_nested(self): def f(): @@ -494,15 +494,15 @@ return "456" > assert f().startswith(g()) E assert False - E + where False = ('456') - E + where = '123'.startswith - E + where '123' = () - E + and '456' = () + E + where False = ('456') + E + where = '123'.startswith + E + where '123' = () + E + and '456' = () failure_demo.py:195: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ - self = + self = def test_global_func(self): > assert isinstance(globf(42), float) @@ -513,19 +513,19 @@ failure_demo.py:198: AssertionError _______________________ TestMoreErrors.test_instance _______________________ - self = + self = def test_instance(self): self.x = 6*7 > assert self.x != 42 E assert 42 != 42 E + where 42 = 42 - E + where 42 = .x + E + where 42 = .x failure_demo.py:202: AssertionError _______________________ TestMoreErrors.test_compare ________________________ - self = + self = def test_compare(self): > assert globf(10) < 5 @@ -535,7 +535,7 @@ failure_demo.py:205: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ - self = + self = def test_try_finally(self): x = 1 @@ -544,4 +544,4 @@ E assert 1 == 0 failure_demo.py:210: AssertionError - ======================== 39 failed in 0.19 seconds ========================= + ======================== 39 failed in 0.23 seconds ========================= --- a/doc/example/simple.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/example/simple.txt Sun May 01 12:38:56 2011 +0200 @@ -53,7 +53,7 @@ test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ first - 1 failed in 0.02 seconds + 1 failed in 0.03 seconds And now with supplying a command line option:: @@ -109,13 +109,13 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 gw0 I / gw1 I / gw2 I / gw3 I gw0 [0] / gw1 [0] / gw2 [0] / gw3 [0] scheduling tests via LoadScheduling - ============================= in 0.51 seconds ============================= + ============================= in 0.52 seconds ============================= .. _`excontrolskip`: @@ -156,20 +156,20 @@ $ py.test -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-41/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-42/conftest.py:9: need --runslow option to run - =================== 1 passed, 1 skipped in 0.02 seconds ==================== + =================== 1 passed, 1 skipped in 0.01 seconds ==================== Or run it including the ``slow`` marked test:: $ py.test --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 2 items test_module.py .. @@ -213,7 +213,7 @@ E Failed: not configured: 42 test_checkconfig.py:8: Failed - 1 failed in 0.01 seconds + 1 failed in 0.02 seconds Detect if running from within a py.test run -------------------------------------------------------------- @@ -261,7 +261,7 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 project deps: mylib-1.1 collecting ... collected 0 items @@ -284,7 +284,7 @@ $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 -- /home/hpk/venv/0/bin/python info1: did you know that ... did you? collecting ... collected 0 items @@ -295,7 +295,7 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 0 items ============================= in 0.00 seconds ============================= --- a/doc/funcargs.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/funcargs.txt Sun May 01 12:38:56 2011 +0200 @@ -61,7 +61,7 @@ $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_simplefactory.py F @@ -167,7 +167,7 @@ $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 10 items test_example.py .........F @@ -182,7 +182,7 @@ E assert 9 < 9 test_example.py:7: AssertionError - ==================== 1 failed, 9 passed in 0.02 seconds ==================== + ==================== 1 failed, 9 passed in 0.03 seconds ==================== Note that the ``pytest_generate_tests(metafunc)`` hook is called during the test collection phase which is separate from the actual test running. @@ -190,7 +190,7 @@ $ py.test --collectonly test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 10 items @@ -210,7 +210,7 @@ $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 -- /home/hpk/venv/0/bin/python collecting ... collected 10 items test_example.py:6: test_func[7] PASSED --- a/doc/getting-started.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/getting-started.txt Sun May 01 12:38:56 2011 +0200 @@ -16,9 +16,10 @@ To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.0.2, imported from /home/hpk/p/pytest/pytest.py + This is py.test version 2.0.3, imported from /home/hpk/p/pytest/pytest.pyc setuptools registered plugins: - pytest-xdist-1.6.dev2 at /home/hpk/p/pytest-xdist/xdist/plugin.pyc + pytest-xdist-1.6.dev3 at /home/hpk/p/pytest-xdist/xdist/plugin.pyc + pytest-incremental-0.1.0 at /home/hpk/venv/0/lib/python2.6/site-packages/pytest_incremental.pyc If you get an error checkout :ref:`installation issues`. @@ -40,7 +41,7 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_sample.py F @@ -99,7 +100,7 @@ $ py.test -q test_sysexit.py collecting ... collected 1 items . - 1 passed in 0.00 seconds + 1 passed in 0.01 seconds .. todo:: For further ways to assert exceptions see the `raises` @@ -130,7 +131,7 @@ ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -139,7 +140,7 @@ E + where False = hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.02 seconds + 1 failed, 1 passed in 0.03 seconds The first test passed, the second failed. Again we can easily see the intermediate values used in the assertion, helping us to @@ -168,7 +169,7 @@ ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-0/test_needsfiles0') + tmpdir = local('/tmp/pytest-10/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -177,8 +178,8 @@ test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-0/test_needsfiles0 - 1 failed in 0.02 seconds + /tmp/pytest-10/test_needsfiles0 + 1 failed in 0.13 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. --- a/doc/mark.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/mark.txt Sun May 01 12:38:56 2011 +0200 @@ -88,7 +88,7 @@ $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 4 items test_mark.py .. @@ -100,7 +100,7 @@ $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 4 items ===================== 4 tests deselected by '-webtest' ===================== @@ -110,7 +110,7 @@ $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 4 items test_mark_classlevel.py .. --- a/doc/monkeypatch.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/monkeypatch.txt Sun May 01 12:38:56 2011 +0200 @@ -39,7 +39,7 @@ .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 0 items ============================= in 0.00 seconds ============================= --- a/doc/skipping.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/skipping.txt Sun May 01 12:38:56 2011 +0200 @@ -130,7 +130,7 @@ example $ py.test -rx xfail_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 6 items xfail_demo.py xxxxxx @@ -147,7 +147,7 @@ XFAIL xfail_demo.py::test_hello6 reason: reason - ======================== 6 xfailed in 0.04 seconds ========================= + ======================== 6 xfailed in 0.05 seconds ========================= .. _`evaluation of skipif/xfail conditions`: --- a/doc/tmpdir.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/tmpdir.txt Sun May 01 12:38:56 2011 +0200 @@ -28,7 +28,7 @@ $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_tmpdir.py F @@ -36,7 +36,7 @@ ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-1/test_create_file0') + tmpdir = local('/tmp/pytest-11/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -47,7 +47,7 @@ E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.06 seconds ========================= .. _`base temporary directory`: --- a/doc/unittest.txt Sun Apr 17 23:16:16 2011 +0200 +++ b/doc/unittest.txt Sun May 01 12:38:56 2011 +0200 @@ -24,7 +24,7 @@ $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.6 -- pytest-2.0.2 + platform linux2 -- Python 2.6.6 -- pytest-2.0.3 collecting ... collected 1 items test_unittest.py F Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed May 11 11:23:56 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 11 May 2011 09:23:56 -0000 Subject: [py-svn] commit/pytest: hpk42: bump version Message-ID: <20110511092356.21794.51782@bitbucket02.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/cf87d8bd91c1/ changeset: r2204:cf87d8bd91c1 user: hpk42 date: 2011-05-11 11:23:46 summary: bump version affected #: 3 files (11 bytes) --- a/_pytest/__init__.py Sun May 01 12:38:56 2011 +0200 +++ b/_pytest/__init__.py Wed May 11 11:23:46 2011 +0200 @@ -1,2 +1,2 @@ # -__version__ = '2.0.3' +__version__ = '2.0.4.dev0' --- a/doc/conf.py Sun May 01 12:38:56 2011 +0200 +++ b/doc/conf.py Wed May 11 11:23:46 2011 +0200 @@ -52,7 +52,7 @@ version = '2.0' # The full version, including alpha/beta/rc tags. import py, pytest -assert py.path.local().relto(py.path.local(pytest.__file__).dirpath().dirpath()) +#assert py.path.local().relto(py.path.local(pytest.__file__).dirpath().dirpath()) release = pytest.__version__ # The language for content autogenerated by Sphinx. Refer to documentation --- a/setup.py Sun May 01 12:38:56 2011 +0200 +++ b/setup.py Wed May 11 11:23:46 2011 +0200 @@ -22,7 +22,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.3', + version='2.0.4.dev0', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed May 11 11:54:51 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 11 May 2011 09:54:51 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20110511095451.15204.4989@bitbucket01.managed.contegix.com> 2 new changesets in pytest: http://bitbucket.org/hpk42/pytest/changeset/363e5a5a59c8/ changeset: r2205:363e5a5a59c8 user: hpk42 date: 2011-05-11 11:54:30 summary: unbump version to retag affected #: 3 files (53 bytes) --- a/_pytest/__init__.py Wed May 11 11:23:46 2011 +0200 +++ b/_pytest/__init__.py Wed May 11 11:54:30 2011 +0200 @@ -1,2 +1,2 @@ # -__version__ = '2.0.4.dev0' +__version__ = '2.0.3' --- a/doc/conf.py Wed May 11 11:23:46 2011 +0200 +++ b/doc/conf.py Wed May 11 11:54:30 2011 +0200 @@ -52,8 +52,9 @@ version = '2.0' # The full version, including alpha/beta/rc tags. import py, pytest +release = pytest.__version__ +version = ".".join(release.split(".")[:2]) #assert py.path.local().relto(py.path.local(pytest.__file__).dirpath().dirpath()) -release = pytest.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. --- a/setup.py Wed May 11 11:23:46 2011 +0200 +++ b/setup.py Wed May 11 11:54:30 2011 +0200 @@ -22,7 +22,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.4.dev0', + version='2.0.3', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], http://bitbucket.org/hpk42/pytest/changeset/1a96be7e4291/ changeset: r2206:1a96be7e4291 user: hpk42 date: 2011-05-11 11:54:36 summary: Added tag 2.0.3 for changeset 363e5a5a59c8 affected #: 1 file (94 bytes) --- a/.hgtags Wed May 11 11:54:30 2011 +0200 +++ b/.hgtags Wed May 11 11:54:36 2011 +0200 @@ -39,3 +39,5 @@ c777dcad166548b7499564cb49ae5c8b4b07f935 2.0.3 c777dcad166548b7499564cb49ae5c8b4b07f935 2.0.3 49f11dbff725acdcc5fe3657cbcdf9ae04e25bbc 2.0.3 +49f11dbff725acdcc5fe3657cbcdf9ae04e25bbc 2.0.3 +363e5a5a59c803e6bc176a6f9cc4bf1a1ca2dab0 2.0.3 Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu May 12 13:52:25 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 12 May 2011 11:52:25 -0000 Subject: [py-svn] commit/pytest: hpk42: fix link to pypy tests Message-ID: <20110512115225.1370.30697@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/02e979ebf82b/ changeset: r2207:02e979ebf82b user: hpk42 date: 2011-05-12 13:52:14 summary: fix link to pypy tests affected #: 1 file (34 bytes) --- a/doc/projects.txt Wed May 11 11:54:36 2011 +0200 +++ b/doc/projects.txt Thu May 12 13:52:14 2011 +0200 @@ -5,7 +5,8 @@ Here are some examples of projects using py.test (please send notes via :ref:`contact`): -* `PyPy `_, Python with a JIT compiler, running over `16000 tests `_ +* `PyPy `_, Python with a JIT compiler, running over + `16000 tests `_ * the `MoinMoin `_ Wiki Engine * `tox `_, virtualenv/Hudson integration tool * `PIDA `_ framework for integrated development Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu May 12 23:52:21 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 12 May 2011 21:52:21 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: introduce the pytest_configure_funcargs hook for better control on funcarg instanciation/configuration Message-ID: <20110512215221.8380.90623@bitbucket03.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/6ffd88246edb/ changeset: r2208:6ffd88246edb user: RonnyPfannschmidt date: 2011-05-12 23:47:05 summary: introduce the pytest_configure_funcargs hook for better control on funcarg instanciation/configuration affected #: 4 files (1.1 KB) --- a/_pytest/hookspec.py Thu May 12 13:52:14 2011 +0200 +++ b/_pytest/hookspec.py Thu May 12 23:47:05 2011 +0200 @@ -115,6 +115,9 @@ def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" +def pytest_configure_funcargs(request): + """ configure funcargs """ + # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- --- a/_pytest/python.py Thu May 12 13:52:14 2011 +0200 +++ b/_pytest/python.py Thu May 12 23:47:05 2011 +0200 @@ -500,6 +500,16 @@ request = FuncargRequest(pyfuncitem=function) request._fillfuncargs() +def pytest_configure_funcargs(request): + argnames = getfuncargnames(request.function) + if argnames: + item = request._pyfuncitem + assert not getattr(item, '_args', None), ( + "yielded functions cannot have funcargs") + for argname in argnames: + if argname not in item.funcargs: + item.funcargs[argname] = request.getfuncargvalue(argname) + _notexists = object() class CallSpec: def __init__(self, funcargs, id, param): @@ -621,14 +631,6 @@ """ the file system path of the test module which collected this test. """ return self._pyfuncitem.fspath - def _fillfuncargs(self): - argnames = getfuncargnames(self.function) - if argnames: - assert not getattr(self._pyfuncitem, '_args', None), ( - "yielded functions cannot have funcargs") - for argname in argnames: - if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) def applymarker(self, marker): @@ -700,6 +702,9 @@ self._currentarg = oldarg return res + def _fillfuncargs(self): + self.config.hook.pytest_configure_funcargs.pcall(self._plugins, request=self) + def _getscopeitem(self, scope): if scope == "function": return self._pyfuncitem --- a/doc/funcargs.txt Thu May 12 13:52:14 2011 +0200 +++ b/doc/funcargs.txt Thu May 12 23:47:05 2011 +0200 @@ -115,6 +115,9 @@ .. _`funcarg factory`: .. _factory: + + + The funcarg **request** object ============================================= @@ -140,6 +143,16 @@ .. _`parametrizing-tests`: .. _`parametrized test functions`: + +Reconfiguring funcargs in a test's setup +======================================== + +Sometimes there is need to do additional funcarg setup steps +which are outside of the normal setup and involve more than just one funcarg. +For that reason the ``pytest_configure_funcargs(request)`` hook +is called to implement and extend the funcarg filling mechanism. + + Parametrizing multiple calls to a test function =========================================================== --- a/testing/test_python.py Thu May 12 13:52:14 2011 +0200 +++ b/testing/test_python.py Thu May 12 23:47:05 2011 +0200 @@ -606,6 +606,22 @@ fillfuncargs(item) assert len(item.funcargs) == 1 + def test_configure_hook(self, testdir): + item = testdir.getitem("def test_func(some, other=20): pass") + class Provider: + def pytest_funcarg__some(self, request): + return [] + def pytest_configure_funcargs(self, request): + request.getfuncargvalue('some').append(1) + item.config.pluginmanager.register(Provider()) + if hasattr(item, '_args'): + del item._args + from _pytest.python import fillfuncargs + fillfuncargs(item) + assert len(item.funcargs) == 1 + assert item.funcargs['some'] == [1] + + class TestRequest: def test_request_attributes(self, testdir): item = testdir.getitem(""" Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri May 13 09:57:44 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 13 May 2011 07:57:44 -0000 Subject: [py-svn] commit/pytest: hpk42: bumping version and adding changelog entry for configure funcargs Message-ID: <20110513075744.6363.47762@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/ee27f70561c9/ changeset: r2209:ee27f70561c9 user: hpk42 date: 2011-05-13 09:57:35 summary: bumping version and adding changelog entry for configure funcargs affected #: 3 files (149 bytes) --- a/CHANGELOG Thu May 12 23:47:05 2011 +0200 +++ b/CHANGELOG Fri May 13 09:57:35 2011 +0200 @@ -1,3 +1,8 @@ +Changes between 2.0.3 and DEV +---------------------------------------------- + +- introduce XXX pytest_configure_funcargs hack (thanks Ronny) + Changes between 2.0.2 and 2.0.3 ---------------------------------------------- --- a/_pytest/__init__.py Thu May 12 23:47:05 2011 +0200 +++ b/_pytest/__init__.py Fri May 13 09:57:35 2011 +0200 @@ -1,2 +1,2 @@ # -__version__ = '2.0.3' +__version__ = '2.0.4.dev' --- a/setup.py Thu May 12 23:47:05 2011 +0200 +++ b/setup.py Fri May 13 09:57:35 2011 +0200 @@ -22,7 +22,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.3', + version='2.0.4.dev', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed May 18 22:11:35 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 18 May 2011 20:11:35 -0000 Subject: [py-svn] commit/py: gutworth: add a way to get at _format_explanation Message-ID: <20110518201135.9270.86821@bitbucket03.managed.contegix.com> 1 new changeset in py: http://bitbucket.org/hpk42/py/changeset/4535871f7f76/ changeset: r2018:4535871f7f76 user: gutworth date: 2011-05-18 22:11:26 summary: add a way to get at _format_explanation affected #: 1 file (72 bytes) --- a/py/__init__.py Sun Apr 17 23:07:49 2011 +0200 +++ b/py/__init__.py Wed May 18 15:11:26 2011 -0500 @@ -74,6 +74,7 @@ '_reinterpret_old' : '._code.assertion:reinterpret_old', '_reinterpret' : '._code.assertion:reinterpret', '_reprcompare' : '._code.assertion:_reprcompare', + '_format_explanation' : '._code.assertion:_format_explanation', }, # backports and additions of builtins Repository URL: https://bitbucket.org/hpk42/py/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed May 25 21:55:07 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 25 May 2011 19:55:07 -0000 Subject: [py-svn] commit/py: hpk42: avoid unneccessary pytest reference for creating docs Message-ID: <20110525195507.3284.24827@bitbucket02.managed.contegix.com> 1 new changeset in py: http://bitbucket.org/hpk42/py/changeset/426d218a4eaa/ changeset: r2019:426d218a4eaa user: hpk42 date: 2011-05-25 21:54:59 summary: avoid unneccessary pytest reference for creating docs affected #: 1 file (8 bytes) --- a/doc/conf.py Wed May 18 15:11:26 2011 -0500 +++ b/doc/conf.py Wed May 25 21:54:59 2011 +0200 @@ -49,7 +49,7 @@ # # The short X.Y version. # The full version, including alpha/beta/rc tags. -import py, pytest +import py assert py.path.local().relto(py.path.local(py.__file__).dirpath().dirpath()) release = py.__version__ version = ".".join(release.split(".")[:2]) Repository URL: https://bitbucket.org/hpk42/py/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed May 25 21:55:59 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 25 May 2011 19:55:59 -0000 Subject: [py-svn] commit/py: hpk42: avoid safety assertion for rtfd Message-ID: <20110525195559.6346.54872@bitbucket03.managed.contegix.com> 1 new changeset in py: http://bitbucket.org/hpk42/py/changeset/076524bd60aa/ changeset: r2020:076524bd60aa user: hpk42 date: 2011-05-25 21:55:52 summary: avoid safety assertion for rtfd affected #: 1 file (77 bytes) --- a/doc/conf.py Wed May 25 21:54:59 2011 +0200 +++ b/doc/conf.py Wed May 25 21:55:52 2011 +0200 @@ -50,7 +50,6 @@ # The short X.Y version. # The full version, including alpha/beta/rc tags. import py -assert py.path.local().relto(py.path.local(py.__file__).dirpath().dirpath()) release = py.__version__ version = ".".join(release.split(".")[:2]) Repository URL: https://bitbucket.org/hpk42/py/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu May 26 00:44:33 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 25 May 2011 22:44:33 -0000 Subject: [py-svn] commit/py: gutworth: loosen this check a bit Message-ID: <20110525224433.6345.72742@bitbucket03.managed.contegix.com> 1 new changeset in py: http://bitbucket.org/hpk42/py/changeset/dac8c182956b/ changeset: r2021:dac8c182956b user: gutworth date: 2011-05-26 00:44:46 summary: loosen this check a bit affected #: 1 file (9 bytes) --- a/py/_code/code.py Wed May 25 21:55:52 2011 +0200 +++ b/py/_code/code.py Wed May 25 17:44:46 2011 -0500 @@ -310,7 +310,7 @@ # ExceptionInfo-like classes may have different attributes. if tup is None: tup = sys.exc_info() - if exprinfo is None and isinstance(tup[1], py.code._AssertionError): + if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], 'msg', None) if exprinfo is None: exprinfo = str(tup[1]) Repository URL: https://bitbucket.org/hpk42/py/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri May 27 08:00:29 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 27 May 2011 06:00:29 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: use os.path.expanduser/expandvars on the junitxml path for convience, fixes #44 Message-ID: <20110527060029.11173.65659@bitbucket03.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/8436bf14aae1/ changeset: 8436bf14aae1 branches: user: RonnyPfannschmidt date: 2011-05-27 07:54:03 summary: use os.path.expanduser/expandvars on the junitxml path for convience, fixes #44 affected #: 2 files (477 bytes) --- a/_pytest/junitxml.py Fri May 13 09:57:35 2011 +0200 +++ b/_pytest/junitxml.py Fri May 27 07:54:03 2011 +0200 @@ -65,7 +65,7 @@ class LogXML(object): def __init__(self, logfile, prefix): - self.logfile = logfile + self.logfile = os.path.expanduser(os.path.expandvars(logfile)) self.prefix = prefix self.test_logs = [] self.passed = self.skipped = 0 --- a/testing/test_junitxml.py Fri May 13 09:57:35 2011 +0200 +++ b/testing/test_junitxml.py Fri May 27 07:54:03 2011 +0200 @@ -1,6 +1,6 @@ from xml.dom import minidom -import py, sys +import py, sys, os def runandparse(testdir, *args): resultpath = testdir.tmpdir.join("junit.xml") @@ -351,3 +351,16 @@ assert '#x%04X' % i in text for i in valid: assert chr(i) in text + +def test_logxml_path_expansion(): + from _pytest.junitxml import LogXML + + home_tilde = py.path.local(os.path.expanduser('~/test.xml')) + # this is here for when $HOME is not set correct + home_var = py.path.local(os.path.expandvars('$HOME/test.xml')) + + xml_tilde = LogXML('~/test.xml', None) + assert xml_tilde.logfile == home_tilde + + xml_var = LogXML('$HOME/test.xml', None) + assert xml_var.logfile == home_var Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri May 27 12:59:53 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 27 May 2011 10:59:53 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: update changelog Message-ID: <20110527105953.19866.85749@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/6c46ab72ed11/ changeset: 6c46ab72ed11 branches: user: RonnyPfannschmidt date: 2011-05-27 12:58:22 summary: update changelog affected #: 1 file (64 bytes) --- a/CHANGELOG Fri May 27 07:54:03 2011 +0200 +++ b/CHANGELOG Fri May 27 12:58:22 2011 +0200 @@ -2,6 +2,7 @@ ---------------------------------------------- - introduce XXX pytest_configure_funcargs hack (thanks Ronny) +- env/username expansion for junitxml file path (fixes issue44) Changes between 2.0.2 and 2.0.3 ---------------------------------------------- Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat May 28 14:03:29 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 12:03:29 -0000 Subject: [py-svn] commit/pytest: hpk42: fix issue47 - fix time-per-test timing output for junitxml Message-ID: <20110528120329.13727.29507@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/743998323adc/ changeset: 743998323adc branches: user: hpk42 date: 2011-05-28 14:03:10 summary: fix issue47 - fix time-per-test timing output for junitxml affected #: 5 files (525 bytes) --- a/CHANGELOG Fri May 27 12:58:22 2011 +0200 +++ b/CHANGELOG Sat May 28 14:03:10 2011 +0200 @@ -1,6 +1,7 @@ Changes between 2.0.3 and DEV ---------------------------------------------- +- fix issue47: timing output in junitxml for test cases is now correct - introduce XXX pytest_configure_funcargs hack (thanks Ronny) - env/username expansion for junitxml file path (fixes issue44) --- a/_pytest/__init__.py Fri May 27 12:58:22 2011 +0200 +++ b/_pytest/__init__.py Sat May 28 14:03:10 2011 +0200 @@ -1,2 +1,2 @@ # -__version__ = '2.0.4.dev' +__version__ = '2.1.0.dev1' --- a/_pytest/junitxml.py Fri May 27 12:58:22 2011 +0200 +++ b/_pytest/junitxml.py Sat May 28 14:03:10 2011 +0200 @@ -76,7 +76,7 @@ names = report.nodeid.split("::") names[0] = names[0].replace("/", '.') names = tuple(names) - d = {'time': self._durations.pop(names, "0")} + d = {'time': self._durations.pop(report.nodeid, "0")} names = [x.replace(".py", "") for x in names if x != "()"] classnames = names[:-1] if self.prefix: @@ -170,12 +170,11 @@ self.append_skipped(report) def pytest_runtest_call(self, item, __multicall__): - names = tuple(item.listnames()) start = time.time() try: return __multicall__.execute() finally: - self._durations[names] = time.time() - start + self._durations[item.nodeid] = time.time() - start def pytest_collectreport(self, report): if not report.passed: --- a/setup.py Fri May 27 12:58:22 2011 +0200 +++ b/setup.py Sat May 28 14:03:10 2011 +0200 @@ -22,7 +22,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.4.dev', + version='2.1.0.dev1', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_junitxml.py Fri May 27 12:58:22 2011 +0200 +++ b/testing/test_junitxml.py Sat May 28 14:03:10 2011 +0200 @@ -39,6 +39,18 @@ node = dom.getElementsByTagName("testsuite")[0] assert_attr(node, errors=0, failures=1, skips=3, tests=2) + def test_timing_function(self, testdir): + testdir.makepyfile(""" + import time, pytest + def test_sleep(): + time.sleep(0.01) + """) + result, dom = runandparse(testdir) + node = dom.getElementsByTagName("testsuite")[0] + tnode = node.getElementsByTagName("testcase")[0] + val = tnode.getAttributeNode("time").value + assert float(val) >= 0.01 + def test_setup_error(self, testdir): testdir.makepyfile(""" def pytest_funcarg__arg(request): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat May 28 14:38:29 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 12:38:29 -0000 Subject: [py-svn] commit/pytest: hpk42: fix issue43 - better tracebacks for unexpected exceptions in doctests Message-ID: <20110528123829.8675.22357@bitbucket02.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/fd6fd56eeb97/ changeset: fd6fd56eeb97 branches: user: hpk42 date: 2011-05-28 14:38:15 summary: fix issue43 - better tracebacks for unexpected exceptions in doctests affected #: 3 files (649 bytes) --- a/CHANGELOG Sat May 28 14:03:10 2011 +0200 +++ b/CHANGELOG Sat May 28 14:38:15 2011 +0200 @@ -1,6 +1,8 @@ Changes between 2.0.3 and DEV ---------------------------------------------- +- fix issue43: improve doctests with better traceback reporting on + unexpected exceptions - fix issue47: timing output in junitxml for test cases is now correct - introduce XXX pytest_configure_funcargs hack (thanks Ronny) - env/username expansion for junitxml file path (fixes issue44) --- a/_pytest/doctest.py Sat May 28 14:03:10 2011 +0200 +++ b/_pytest/doctest.py Sat May 28 14:38:15 2011 +0200 @@ -59,7 +59,7 @@ inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] - + lines += py.std.traceback.format_exception(*excinfo.value.exc_info) return ReprFailDoctest(reprlocation, lines) else: return super(DoctestItem, self).repr_failure(excinfo) --- a/testing/test_doctest.py Sat May 28 14:03:10 2011 +0200 +++ b/testing/test_doctest.py Sat May 28 14:38:15 2011 +0200 @@ -59,6 +59,21 @@ "*UNEXPECTED*ZeroDivision*", ]) + def test_doctest_unex_importerror(self, testdir): + testdir.tmpdir.join("hello.py").write(py.code.Source(""" + import asdalsdkjaslkdjasd + """)) + p = testdir.maketxtfile(""" + >>> import hello + >>> + """) + result = testdir.runpytest("--doctest-modules") + result.stdout.fnmatch_lines([ + "*>>> import hello", + "*UNEXPECTED*ImportError*", + "*import asdals*", + ]) + def test_doctestmodule(self, testdir): p = testdir.makepyfile(""" ''' Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat May 28 16:23:26 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 14:23:26 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: also apply normpath to junitxml file path Message-ID: <20110528142326.14659.35277@bitbucket03.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/b199f0432dc0/ changeset: b199f0432dc0 branches: user: RonnyPfannschmidt date: 2011-05-28 16:21:57 summary: also apply normpath to junitxml file path affected #: 1 file (44 bytes) --- a/_pytest/junitxml.py Sat May 28 14:38:15 2011 +0200 +++ b/_pytest/junitxml.py Sat May 28 16:21:57 2011 +0200 @@ -65,7 +65,8 @@ class LogXML(object): def __init__(self, logfile, prefix): - self.logfile = os.path.expanduser(os.path.expandvars(logfile)) + logfile = os.path.expanduser(os.path.expandvars(logfile)) + self.logfile = os.path.normpath(logfile) self.prefix = prefix self.test_logs = [] self.passed = self.skipped = 0 Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat May 28 16:53:32 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 14:53:32 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: dont wrap comparisation paths in py.path.local for the junitxml tests, since missing $HOME causes issues else Message-ID: <20110528145332.8674.13471@bitbucket02.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/94fdfb180acc/ changeset: 94fdfb180acc branches: user: RonnyPfannschmidt date: 2011-05-28 16:52:05 summary: dont wrap comparisation paths in py.path.local for the junitxml tests, since missing $HOME causes issues else affected #: 1 file (30 bytes) --- a/testing/test_junitxml.py Sat May 28 16:21:57 2011 +0200 +++ b/testing/test_junitxml.py Sat May 28 16:52:05 2011 +0200 @@ -367,9 +367,9 @@ def test_logxml_path_expansion(): from _pytest.junitxml import LogXML - home_tilde = py.path.local(os.path.expanduser('~/test.xml')) + home_tilde = os.path.expanduser('~/test.xml') # this is here for when $HOME is not set correct - home_var = py.path.local(os.path.expandvars('$HOME/test.xml')) + home_var = os.path.expandvars('$HOME/test.xml') xml_tilde = LogXML('~/test.xml', None) assert xml_tilde.logfile == home_tilde Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat May 28 17:52:41 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 15:52:41 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: hopefully final win32 fix for the junitxml path expansion Message-ID: <20110528155241.14659.97230@bitbucket03.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/e859678c2700/ changeset: e859678c2700 branches: user: RonnyPfannschmidt date: 2011-05-28 17:36:38 summary: hopefully final win32 fix for the junitxml path expansion affected #: 1 file (18 bytes) --- a/testing/test_junitxml.py Sat May 28 16:52:05 2011 +0200 +++ b/testing/test_junitxml.py Sat May 28 17:36:38 2011 +0200 @@ -369,7 +369,7 @@ home_tilde = os.path.expanduser('~/test.xml') # this is here for when $HOME is not set correct - home_var = os.path.expandvars('$HOME/test.xml') + home_var = os.path.normpath(os.path.expandvars('$HOME/test.xml')) xml_tilde = LogXML('~/test.xml', None) assert xml_tilde.logfile == home_tilde Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat May 28 19:02:06 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 17:02:06 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: add another normpath in the junitxml tests Message-ID: <20110528170206.13726.87231@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/a7cbffe7357e/ changeset: a7cbffe7357e branches: user: RonnyPfannschmidt date: 2011-05-28 19:00:23 summary: add another normpath in the junitxml tests affected #: 1 file (18 bytes) --- a/testing/test_junitxml.py Sat May 28 17:36:38 2011 +0200 +++ b/testing/test_junitxml.py Sat May 28 19:00:23 2011 +0200 @@ -367,7 +367,7 @@ def test_logxml_path_expansion(): from _pytest.junitxml import LogXML - home_tilde = os.path.expanduser('~/test.xml') + home_tilde = os.path.normpath(os.path.expanduser('~/test.xml')) # this is here for when $HOME is not set correct home_var = os.path.normpath(os.path.expandvars('$HOME/test.xml')) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun May 29 00:48:13 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 28 May 2011 22:48:13 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20110528224813.13728.17643@bitbucket01.managed.contegix.com> 2 new changesets in pytest: http://bitbucket.org/hpk42/pytest/changeset/97b702dfb711/ changeset: 97b702dfb711 branches: user: hpk42 date: 2011-05-29 00:47:32 summary: add Mozilla QA people to pytest users affected #: 1 file (151 bytes) --- a/doc/projects.txt Sat May 28 19:00:23 2011 +0200 +++ b/doc/projects.txt Sun May 29 00:47:32 2011 +0200 @@ -43,6 +43,7 @@ ----------------------------------- * `Square Kilometre Array, Cape Town `_ +* `Some Mozilla QA people `_ use pytest to distribute their Selenium tests * `Tandberg `_ * `Shootq `_ * `Stups department of Heinrich Heine University D?sseldorf `_ http://bitbucket.org/hpk42/pytest/changeset/801fea1aa198/ changeset: 801fea1aa198 branches: user: hpk42 date: 2011-05-29 00:45:31 summary: fix timing float comparison affected #: 1 file (1 byte) --- a/testing/test_junitxml.py Sun May 29 00:47:32 2011 +0200 +++ b/testing/test_junitxml.py Sun May 29 00:45:31 2011 +0200 @@ -49,7 +49,7 @@ node = dom.getElementsByTagName("testsuite")[0] tnode = node.getElementsByTagName("testcase")[0] val = tnode.getAttributeNode("time").value - assert float(val) >= 0.01 + assert float(val) >= 0.001 def test_setup_error(self, testdir): testdir.makepyfile(""" Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun May 29 08:01:51 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sun, 29 May 2011 06:01:51 -0000 Subject: [py-svn] commit/py: hpk42: bump version Message-ID: <20110529060151.8675.28489@bitbucket02.managed.contegix.com> 1 new changeset in py: http://bitbucket.org/hpk42/py/changeset/8980257b1719/ changeset: 8980257b1719 branches: user: hpk42 date: 2011-05-29 08:01:15 summary: bump version affected #: 2 files (10 bytes) --- a/py/__init__.py Wed May 25 17:44:46 2011 -0500 +++ b/py/__init__.py Sun May 29 08:01:15 2011 +0200 @@ -8,7 +8,7 @@ (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.3' +__version__ = '1.4.4.dev1' from py import _apipkg --- a/setup.py Wed May 25 17:44:46 2011 -0500 +++ b/setup.py Sun May 29 08:01:15 2011 +0200 @@ -9,7 +9,7 @@ name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.3', + version='1.4.4.dev1', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/hpk42/py/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun May 29 09:21:55 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sun, 29 May 2011 07:21:55 -0000 Subject: [py-svn] commit/pytest: hpk42: fix a buffering issue that i think/hope only occurs during internal tests Message-ID: <20110529072155.13726.54141@bitbucket01.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/0fb99b23d31c/ changeset: 0fb99b23d31c branches: user: hpk42 date: 2011-05-29 09:21:48 summary: fix a buffering issue that i think/hope only occurs during internal tests affected #: 1 file (6 bytes) --- a/testing/test_doctest.py Sun May 29 00:45:31 2011 +0200 +++ b/testing/test_doctest.py Sun May 29 09:21:48 2011 +0200 @@ -114,7 +114,7 @@ >>> i + 1 2 """) - result = testdir.runpytest(p) + result = testdir.runpytest(p, "-s") result.stdout.fnmatch_lines([ '001 >>> i = 0', '002 >>> i + 1', Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From MAILER-DAEMON at codespeak.net Mon May 30 09:42:25 2011 From: MAILER-DAEMON at codespeak.net (Returned mail) Date: Mon, 30 May 2011 14:42:25 +0700 Subject: [py-svn] (no subject) Message-ID: <20110530081937.A7E4E282B8B@codespeak.net> ? l^?`F?B?? 3?,m??l?%?????k???? B?`?;?????????^i?h?`E!?-?x?l?0?*??wj??????J???e`??[??? ??!??i*8P.o-????$Sa????S??%L???2???Q$!???(?zENH?????G??]?????????G????E????.?5b?y??dr?Y??(?|?i]??[t$???r???/??{??ywJ????b?\c??? ?G????\t?e??[??s????$????Koyc?F??V'?GO?CPft?6?M????H?p??LW?c?O????(lt?t0?[?S?????????rzmw????????t?m?If?:G?d??q??eq???Y?? !?_O??h?N? ???E???v??-0???V??8 ????H?r?M?????t???T? j?o???n?!???0?Ay???4~???#???????????k?q[??$t?????Dk|?j??{C5????e??? ?sn??f????e??>??Z[?s???q????q "`ogc\?[J??7??`u?i??:_?~???H??_??m???Z?T(?/??A???F-"???-?????-????U????t u?2?^??Wbl#pe???Q ???????) s???N????S5?%????d??/zQ"~6??A??2-??h?"??T?-??? ?a??fz>s?^!??T?`?"??s??S~???Te>???????Y?????i? n????mQ[Z??{yQ?K?p8E?V??XK???? ????u???3{nM??d{v????s?????????o????Z???7?F 8????? ?????Ib???&?r??/3?L5 wxp[?B???:R?????D??LhS?TB1????P b??Y????2 ?;k?k?1???2???H???[??s????(?1????C?3?u!E?-???p2?????E????????2???s????xVQ\?\???X????F?j??B???8???L?W??? WU??;e??R,jAm?J????Z^?^??!%??0? ?N?-??XZ?U&???$? ??Vl?&??R????M??,p;z???F(?2??)CQh?[%?L?t???p?.L????Hh/*?????*?,??U;??y??? ???????????s?nMH??S????a&???????R??5*s??????????????K???`??hjK??????!??o?yX??sx????p9?sWB ????N^?[??b???6??hv?P????\??{????/v?l?j?:??c'l??:I??c?w??s|?08??i?I?o8??????%??B]?]??|?*??G? ????_???4?~?yE??B???N??SOW?ok????3|??:w?,;????E?Tc??y??/hf??R????-??^{2d?F??{7F y?g$????2?SH7??8?[?c???%?????Re????Y????4hw?????-??-??l? ??u ???d?Q?F??8?-Rql?Y/?? 5???(?[X??c??A???????A??/P From commits-noreply at bitbucket.org Tue May 31 14:43:33 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 31 May 2011 12:43:33 -0000 Subject: [py-svn] commit/pytest: 60 new changesets Message-ID: <20110531124333.8673.83794@bitbucket02.managed.contegix.com> 60 new changesets in pytest: http://bitbucket.org/hpk42/pytest/changeset/940ab5305b0c/ changeset: 940ab5305b0c branches: user: gutworth date: 2011-05-18 22:31:10 summary: new assertion debugger which rewrites asserts before they are run affected #: 2 files (16.4 KB) --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertrewrite.py Wed May 18 15:31:10 2011 -0500 @@ -0,0 +1,295 @@ +"""Rewrite assertion AST to produce nice error messages""" + +import ast +import collections +import itertools + +import py + + +def rewrite_asserts(mod): + """Rewrite the assert statements in mod.""" + AssertionRewriter().run(mod) + + +_saferepr = py.io.saferepr +_format_explanation = py.code._format_explanation + +def _format_boolop(operands, explanations, is_or): + show_explanations = [] + for operand, expl in zip(operands, explanations): + show_explanations.append(expl) + if operand == is_or: + break + return "(" + (is_or and " or " or " and ").join(show_explanations) + ")" + +def _call_reprcompare(ops, results, expls, each_obj): + for i, res, expl in zip(range(len(ops)), results, expls): + if not res: + break + if py.code._reprcompare is not None: + custom = py.code._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if custom is not None: + return custom + return expl + + +unary_map = { + ast.Not : "not %s", + ast.Invert : "~%s", + ast.USub : "-%s", + ast.UAdd : "+%s" +} + +binop_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" +} + + +class AssertionRewriter(ast.NodeVisitor): + + def run(self, mod): + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return + # Insert some special imports at top but after any docstrings. + aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), + ast.alias("py", "@pylib"), + ast.alias("_pytest.assertrewrite", "@pytest_ar")] + imports = [ast.Import([alias], lineno=0, col_offset=0) + for alias in aliases] + pos = 0 + if isinstance(mod.body[0], ast.Str): + pos = 1 + mod.body[pos:pos] = imports + # Collect asserts. + asserts = [] + nodes = collections.deque([mod]) + while nodes: + node = nodes.popleft() + for name, field in ast.iter_fields(node): + if isinstance(field, list): + for i, child in enumerate(field): + if isinstance(child, ast.Assert): + asserts.append((field, i, child)) + elif isinstance(child, ast.AST): + nodes.append(child) + elif (isinstance(field, ast.AST) and + # Don't recurse into expressions as they can't contain + # asserts. + not isinstance(field, ast.expr)): + nodes.append(field) + # Transform asserts. + for parent, pos, assert_ in asserts: + parent[pos:pos + 1] = self.visit(assert_) + + def assign(self, expr): + """Give *expr* a name.""" + # Use a character invalid in python identifiers to avoid clashing. + name = "@py_assert" + str(next(self.variable_counter)) + self.variables.add(name) + self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) + return ast.Name(name, ast.Load()) + + def display(self, expr): + """Call py.io.saferepr on the expression.""" + return self.helper("saferepr", expr) + + def helper(self, name, *args): + """Call a helper in this module.""" + py_name = ast.Name("@pytest_ar", ast.Load()) + attr = ast.Attribute(py_name, "_" + name, ast.Load()) + return ast.Call(attr, list(args), [], None, None) + + def builtin(self, name): + """Return the builtin called *name*.""" + builtin_name = ast.Name("@py_builtins", ast.Load()) + return ast.Attribute(builtin_name, name, ast.Load()) + + def explanation_param(self, expr): + specifier = "py" + str(next(self.variable_counter)) + self.explanation_specifiers[specifier] = expr + return "%(" + specifier + ")s" + + def push_format_context(self): + self.explanation_specifiers = {} + self.stack.append(self.explanation_specifiers) + + def pop_format_context(self, expl_expr): + current = self.stack.pop() + if self.stack: + self.explanation_specifiers = self.stack[-1] + keys = [ast.Str(key) for key in current.keys()] + format_dict = ast.Dict(keys, current.values()) + form = ast.BinOp(expl_expr, ast.Mod(), format_dict) + name = "@py_format" + str(next(self.variable_counter)) + self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) + return ast.Name(name, ast.Load()) + + def generic_visit(self, node): + """Handle expressions we don't have custom code for.""" + assert isinstance(node, ast.expr) + res = self.assign(node) + return res, self.explanation_param(self.display(res)) + + def visit_Assert(self, assert_): + if assert_.msg: + # There's already a message. Don't mess with it. + return [assert_] + self.statements = [] + self.variables = set() + self.variable_counter = itertools.count() + self.stack = [] + self.on_failure = [] + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) + # Create failure message. + body = self.on_failure + negation = ast.UnaryOp(ast.Not(), top_condition) + self.statements.append(ast.If(negation, body, [])) + explanation = "assert " + explanation + template = ast.Str(explanation) + msg = self.pop_format_context(template) + fmt = self.helper("format_explanation", msg) + body.append(ast.Assert(top_condition, fmt)) + # Delete temporary variables. + names = [ast.Name(name, ast.Del()) for name in self.variables] + if names: + delete = ast.Delete(names) + self.statements.append(delete) + # Fix line numbers. + for stmt in self.statements: + stmt.lineno = assert_.lineno + stmt.col_offset = assert_.col_offset + ast.fix_missing_locations(stmt) + return self.statements + + def visit_Name(self, name): + # Check if the name is local or not. + locs = ast.Call(self.builtin("locals"), [], [], None, None) + globs = ast.Call(self.builtin("globals"), [], [], None, None) + ops = [ast.In(), ast.IsNot()] + test = ast.Compare(ast.Str(name.id), ops, [locs, globs]) + expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) + return name, self.explanation_param(expr) + + def visit_BoolOp(self, boolop): + operands = [] + explanations = [] + self.push_format_context() + for operand in boolop.values: + res, explanation = self.visit(operand) + operands.append(res) + explanations.append(explanation) + expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) + is_or = ast.Num(isinstance(boolop.op, ast.Or)) + expl_template = self.helper("format_boolop", + ast.Tuple(operands, ast.Load()), expls, + is_or) + expl = self.pop_format_context(expl_template) + res = self.assign(ast.BoolOp(boolop.op, operands)) + return res, self.explanation_param(expl) + + def visit_UnaryOp(self, unary): + pattern = unary_map[unary.op.__class__] + operand_res, operand_expl = self.visit(unary.operand) + res = self.assign(ast.UnaryOp(unary.op, operand_res)) + return res, pattern % (operand_expl,) + + def visit_BinOp(self, binop): + symbol = binop_map[binop.op.__class__] + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) + explanation = "(%s %s %s)" % (left_expl, symbol, right_expl) + res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) + return res, explanation + + def visit_Call(self, call): + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + new_star = new_kwarg = None + for arg in call.args: + res, expl = self.visit(arg) + new_args.append(res) + arg_expls.append(expl) + for keyword in call.keywords: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + arg_expls.append(keyword.arg + "=" + expl) + if call.starargs: + new_star, expl = self.visit(call.starargs) + arg_expls.append("*" + expl) + if call.kwargs: + new_kwarg, expl = self.visit(call.kwarg) + arg_expls.append("**" + expl) + expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) + new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) + return res, outer_expl + + def visit_Attribute(self, attr): + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + value, value_expl = self.visit(attr.value) + res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) + res_expl = self.explanation_param(self.display(res)) + pat = "%s\n{%s = %s.%s\n}" + expl = pat % (res_expl, res_expl, value_expl, attr.attr) + return res, expl + + def visit_Compare(self, comp): + self.push_format_context() + left_res, left_expl = self.visit(comp.left) + res_variables = ["@py_assert" + str(next(self.variable_counter)) + for i in range(len(comp.ops))] + load_names = [ast.Name(v, ast.Load()) for v in res_variables] + store_names = [ast.Name(v, ast.Store()) for v in res_variables] + it = zip(range(len(comp.ops)), comp.ops, comp.comparators) + expls = [] + syms = [] + results = [left_res] + for i, op, next_operand in it: + next_res, next_expl = self.visit(next_operand) + results.append(next_res) + sym = binop_map[op.__class__] + syms.append(ast.Str(sym)) + expl = "%s %s %s" % (left_expl, sym, next_expl) + expls.append(ast.Str(expl)) + res_expr = ast.Compare(left_res, [op], [next_res]) + self.statements.append(ast.Assign([store_names[i]], res_expr)) + left_res, left_expl = next_res, next_expl + # Use py.code._reprcompare if that's available. + expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), + ast.Tuple(load_names, ast.Load()), + ast.Tuple(expls, ast.Load()), + ast.Tuple(results, ast.Load())) + args = [ast.List(load_names, ast.Load())] + res = ast.Call(self.builtin("all"), args, [], None, None) + return res, self.explanation_param(self.pop_format_context(expl_call)) --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/testing/test_assertrewrite.py Wed May 18 15:31:10 2011 -0500 @@ -0,0 +1,195 @@ +import sys +import py +import pytest + +ast = pytest.importorskip("ast") + +from _pytest.assertrewrite import rewrite_asserts + + +def setup_module(mod): + mod._old_reprcompare = py.code._reprcompare + py.code._reprcompare = None + +def teardown_module(mod): + py.code._reprcompare = mod._old_reprcompare + del mod._old_reprcompare + + +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) + mod = ast.parse(src) + rewrite_asserts(mod) + code = compile(mod, "", "exec") + ns = {} + if extra_ns is not None: + ns.update(extra_ns) + exec code in ns + func = ns[f.__name__] + try: + func() + except AssertionError: + if must_pass: + pytest.fail("shouldn't have raised") + s = str(sys.exc_info()[1]) + if not s.startswith("assert"): + return "AssertionError: " + s + return s + else: + if not must_pass: + pytest.fail("function didn't raise at all") + + +class TestAssertionRewrite: + + def test_name(self): + def f(): + assert False + assert getmsg(f) == "assert False" + def f(): + f = False + assert f + assert getmsg(f) == "assert False" + def f(): + assert a_global + assert getmsg(f, {"a_global" : False}) == "assert a_global" + + def test_assert_already_has_message(self): + def f(): + assert False, "something bad!" + assert getmsg(f) == "AssertionError: something bad!" + + def test_boolop(self): + def f(): + f = g = False + assert f and g + assert getmsg(f) == "assert (False)" + def f(): + f = True + g = False + assert f and g + assert getmsg(f) == "assert (True and False)" + def f(): + f = False + g = True + assert f and g + assert getmsg(f) == "assert (False)" + def f(): + f = g = False + assert f or g + assert getmsg(f) == "assert (False or False)" + def f(): + f = True + g = False + assert f or g + getmsg(f, must_pass=True) + + def test_short_circut_evaluation(self): + pytest.xfail("complicated fix; I'm not sure if it's important") + def f(): + assert True or explode + getmsg(f, must_pass=True) + + def test_unary_op(self): + def f(): + x = True + assert not x + assert getmsg(f) == "assert not True" + def f(): + x = 0 + assert ~x + 1 + assert getmsg(f) == "assert (~0 + 1)" + def f(): + x = 3 + assert -x + x + assert getmsg(f) == "assert (-3 + 3)" + def f(): + x = 0 + assert +x + x + assert getmsg(f) == "assert (+0 + 0)" + + def test_binary_op(self): + def f(): + x = 1 + y = -1 + assert x + y + assert getmsg(f) == "assert (1 + -1)" + + def test_call(self): + def g(a=42, *args, **kwargs): + return False + ns = {"g" : g} + def f(): + assert g() + assert getmsg(f, ns) == """assert False + + where False = g()""" + def f(): + assert g(1) + assert getmsg(f, ns) == """assert False + + where False = g(1)""" + def f(): + assert g(1, 2) + assert getmsg(f, ns) == """assert False + + where False = g(1, 2)""" + def f(): + assert g(1, g=42) + assert getmsg(f, ns) == """assert False + + where False = g(1, g=42)""" + def f(): + assert g(1, 3, g=23) + assert getmsg(f, ns) == """assert False + + where False = g(1, 3, g=23)""" + + def test_attribute(self): + class X(object): + g = 3 + ns = {"X" : X, "x" : X()} + def f(): + assert not x.g + assert getmsg(f, ns) == """assert not 3 + + where 3 = x.g""" + def f(): + x.a = False + assert x.a + assert getmsg(f, ns) == """assert False + + where False = x.a""" + + def test_comparisons(self): + def f(): + a, b = range(2) + assert b < a + assert getmsg(f) == """assert 1 < 0""" + def f(): + a, b, c = range(3) + assert a > b > c + assert getmsg(f) == """assert 0 > 1""" + def f(): + a, b, c = range(3) + assert a < b > c + assert getmsg(f) == """assert 1 > 2""" + def f(): + a, b, c = range(3) + assert a < b <= c + getmsg(f, must_pass=True) + + def test_len(self): + def f(): + l = range(10) + assert len(l) == 11 + assert getmsg(f).startswith("""assert 10 == 11 + + where 10 = len([""") + + def test_custom_reprcompare(self, monkeypatch): + def my_reprcompare(op, left, right): + return "42" + monkeypatch.setattr(py.code, "_reprcompare", my_reprcompare) + def f(): + assert 42 < 3 + assert getmsg(f) == "assert 42" + def my_reprcompare(op, left, right): + return "%s %s %s" % (left, op, right) + monkeypatch.setattr(py.code, "_reprcompare", my_reprcompare) + def f(): + assert 1 < 3 < 5 <= 4 < 7 + assert getmsg(f) == "assert 5 <= 4" http://bitbucket.org/hpk42/pytest/changeset/deea1778861a/ changeset: deea1778861a branches: user: gutworth date: 2011-05-19 23:53:13 summary: place assertion imports after __future__ statements and docstrings affected #: 2 files (1.2 KB) --- a/_pytest/assertrewrite.py Wed May 18 15:31:10 2011 -0500 +++ b/_pytest/assertrewrite.py Thu May 19 16:53:13 2011 -0500 @@ -74,15 +74,23 @@ if not mod.body: # Nothing to do. return - # Insert some special imports at top but after any docstrings. + # Insert some special imports at top but after any docstrings and + # __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), ast.alias("py", "@pylib"), ast.alias("_pytest.assertrewrite", "@pytest_ar")] imports = [ast.Import([alias], lineno=0, col_offset=0) for alias in aliases] + expect_docstring = True pos = 0 - if isinstance(mod.body[0], ast.Str): - pos = 1 + for item in mod.body: + if (expect_docstring and isinstance(item, ast.Expr) and + isinstance(item.value, ast.Str)): + expect_docstring = False + elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and + item.identifier != "__future__"): + break + pos += 1 mod.body[pos:pos] = imports # Collect asserts. asserts = [] --- a/testing/test_assertrewrite.py Wed May 18 15:31:10 2011 -0500 +++ b/testing/test_assertrewrite.py Thu May 19 16:53:13 2011 -0500 @@ -16,11 +16,15 @@ del mod._old_reprcompare +def rewrite(src): + tree = ast.parse(src) + rewrite_asserts(tree) + return tree + 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) - mod = ast.parse(src) - rewrite_asserts(mod) + mod = rewrite(src) code = compile(mod, "", "exec") ns = {} if extra_ns is not None: @@ -43,6 +47,26 @@ class TestAssertionRewrite: + def test_place_initial_imports(self): + s = """'Doc string'""" + m = rewrite(s) + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + for imp in m.body[1:]: + assert isinstance(imp, ast.Import) + s = """from __future__ import with_statement""" + m = rewrite(s) + assert isinstance(m.body[0], ast.ImportFrom) + for imp in m.body[1:]: + assert isinstance(imp, ast.Import) + s = """'doc string'\nfrom __future__ import with_statement""" + m = rewrite(s) + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + assert isinstance(m.body[1], ast.ImportFrom) + for imp in m.body[2:]: + assert isinstance(imp, ast.Import) + def test_name(self): def f(): assert False http://bitbucket.org/hpk42/pytest/changeset/248936311096/ changeset: 248936311096 branches: user: gutworth date: 2011-05-20 01:32:48 summary: small refactoring affected #: 1 file (53 bytes) --- a/_pytest/assertrewrite.py Thu May 19 16:53:13 2011 -0500 +++ b/_pytest/assertrewrite.py Thu May 19 18:32:48 2011 -0500 @@ -113,11 +113,16 @@ for parent, pos, assert_ in asserts: parent[pos:pos + 1] = self.visit(assert_) - def assign(self, expr): - """Give *expr* a name.""" + def variable(self): + """Get a new variable.""" # Use a character invalid in python identifiers to avoid clashing. name = "@py_assert" + str(next(self.variable_counter)) self.variables.add(name) + return name + + def assign(self, expr): + """Give *expr* a name.""" + name = self.variable() self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) return ast.Name(name, ast.Load()) @@ -275,8 +280,7 @@ def visit_Compare(self, comp): self.push_format_context() left_res, left_expl = self.visit(comp.left) - res_variables = ["@py_assert" + str(next(self.variable_counter)) - for i in range(len(comp.ops))] + res_variables = [self.variable() for i in range(len(comp.ops))] load_names = [ast.Name(v, ast.Load()) for v in res_variables] store_names = [ast.Name(v, ast.Store()) for v in res_variables] it = zip(range(len(comp.ops)), comp.ops, comp.comparators) http://bitbucket.org/hpk42/pytest/changeset/b2c9efd36e4f/ changeset: b2c9efd36e4f branches: user: gutworth date: 2011-05-20 01:56:48 summary: correctly handle multiple asserts affected #: 2 files (266 bytes) --- a/_pytest/assertrewrite.py Thu May 19 18:32:48 2011 -0500 +++ b/_pytest/assertrewrite.py Thu May 19 18:56:48 2011 -0500 @@ -99,19 +99,22 @@ node = nodes.popleft() for name, field in ast.iter_fields(node): if isinstance(field, list): + new = [] for i, child in enumerate(field): if isinstance(child, ast.Assert): + # Transform assert. + new.extend(self.visit(child)) asserts.append((field, i, child)) - elif isinstance(child, ast.AST): - nodes.append(child) + else: + new.append(child) + if isinstance(child, ast.AST): + nodes.append(child) + setattr(node, name, new) elif (isinstance(field, ast.AST) and # Don't recurse into expressions as they can't contain # asserts. not isinstance(field, ast.expr)): nodes.append(field) - # Transform asserts. - for parent, pos, assert_ in asserts: - parent[pos:pos + 1] = self.visit(assert_) def variable(self): """Get a new variable.""" --- a/testing/test_assertrewrite.py Thu May 19 18:32:48 2011 -0500 +++ b/testing/test_assertrewrite.py Thu May 19 18:56:48 2011 -0500 @@ -196,6 +196,11 @@ a, b, c = range(3) assert a < b <= c getmsg(f, must_pass=True) + def f(): + a, b, c = range(3) + assert a < b + assert b < c + getmsg(f, must_pass=True) def test_len(self): def f(): http://bitbucket.org/hpk42/pytest/changeset/1fa4b61b0eb0/ changeset: 1fa4b61b0eb0 branches: user: gutworth date: 2011-05-20 02:15:20 summary: remove unneeded list affected #: 1 file (83 bytes) --- a/_pytest/assertrewrite.py Thu May 19 18:56:48 2011 -0500 +++ b/_pytest/assertrewrite.py Thu May 19 19:15:20 2011 -0500 @@ -93,7 +93,6 @@ pos += 1 mod.body[pos:pos] = imports # Collect asserts. - asserts = [] nodes = collections.deque([mod]) while nodes: node = nodes.popleft() @@ -104,7 +103,6 @@ if isinstance(child, ast.Assert): # Transform assert. new.extend(self.visit(child)) - asserts.append((field, i, child)) else: new.append(child) if isinstance(child, ast.AST): http://bitbucket.org/hpk42/pytest/changeset/1e45172fbfc6/ changeset: 1e45172fbfc6 branches: user: gutworth date: 2011-05-20 04:45:33 summary: add hooks before and after a module is imported affected #: 2 files (342 bytes) --- a/_pytest/hookspec.py Thu May 19 19:15:20 2011 -0500 +++ b/_pytest/hookspec.py Thu May 19 21:45:33 2011 -0500 @@ -104,6 +104,12 @@ """ pytest_pycollect_makemodule.firstresult = True +def pytest_pycollect_before_module_import(mod): + """Called before a module is imported.""" + +def pytest_pycollect_after_module_import(mod): + """Called after a module is imported.""" + def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ pytest_pycollect_makeitem.firstresult = True --- a/_pytest/python.py Thu May 19 19:15:20 2011 -0500 +++ b/_pytest/python.py Thu May 19 21:45:33 2011 -0500 @@ -225,6 +225,7 @@ return self._memoizedcall('_obj', self._importtestmodule) def _importtestmodule(self): + self.ihook.pytest_pycollect_before_module_import(mod=self) # we assume we are only called once per module try: mod = self.fspath.pyimport(ensuresyspath=True) @@ -242,6 +243,8 @@ "HINT: use a unique basename for your test file modules" % e.args ) + finally: + self.ihook.pytest_pycollect_after_module_import(mod=self) #print "imported test module", mod self.config.pluginmanager.consider_module(mod) return mod http://bitbucket.org/hpk42/pytest/changeset/1a4241dede66/ changeset: 1a4241dede66 branches: user: gutworth date: 2011-05-20 04:49:37 summary: unconditionally override lineno and col_offset on generated ast affected #: 1 file (376 bytes) --- a/_pytest/assertrewrite.py Thu May 19 21:45:33 2011 -0500 +++ b/_pytest/assertrewrite.py Thu May 19 21:49:37 2011 -0500 @@ -67,6 +67,19 @@ } +def set_location(node, lineno, col_offset): + """Set node location information recursively.""" + def _fix(node, lineno, col_offset): + if "lineno" in node._attributes: + node.lineno = lineno + if "col_offset" in node._attributes: + node.col_offset = col_offset + for child in ast.iter_child_nodes(node): + _fix(child, lineno, col_offset) + _fix(node, lineno, col_offset) + return node + + class AssertionRewriter(ast.NodeVisitor): def run(self, mod): @@ -196,9 +209,7 @@ self.statements.append(delete) # Fix line numbers. for stmt in self.statements: - stmt.lineno = assert_.lineno - stmt.col_offset = assert_.col_offset - ast.fix_missing_locations(stmt) + set_location(stmt, assert_.lineno, assert_.col_offset) return self.statements def visit_Name(self, name): http://bitbucket.org/hpk42/pytest/changeset/4e906489caac/ changeset: 4e906489caac branches: user: gutworth date: 2011-05-20 04:52:10 summary: use assertion rewriting on test files This works by writing a fake pyc with the asserts rewritten. affected #: 1 file (1.5 KB) --- a/_pytest/assertion.py Thu May 19 21:49:37 2011 -0500 +++ b/_pytest/assertion.py Thu May 19 21:52:10 2011 -0500 @@ -2,9 +2,19 @@ support for presented detailed information in failing assertions. """ import py +import imp +import marshal +import struct import sys from _pytest.monkeypatch import monkeypatch +try: + from _pytest.assertrewrite import rewrite_asserts +except ImportError: + rewrite_asserts = None +else: + import ast + def pytest_addoption(parser): group = parser.getgroup("debugconfig") group._addoption('--no-assert', action="store_true", default=False, @@ -12,6 +22,7 @@ help="disable python assert expression reinterpretation."), def pytest_configure(config): + global rewrite_asserts # The _reprcompare attribute on the py.code module is used by # py._code._assertionnew to detect this plugin was loaded and in # turn call the hooks defined here as part of the @@ -29,6 +40,51 @@ m.setattr(py.builtin.builtins, 'AssertionError', py.code._AssertionError) m.setattr(py.code, '_reprcompare', callbinrepr) + else: + rewrite_asserts = None + +def pytest_pycollect_before_module_import(mod): + if rewrite_asserts is None: + return + # Some deep magic: load the source, rewrite the asserts, and write a + # fake pyc, so that it'll be loaded further down this function. + source = mod.fspath.read() + try: + tree = ast.parse(source) + except SyntaxError: + # Let this pop up again in the real import. + return + rewrite_asserts(tree) + try: + co = compile(tree, str(mod.fspath), "exec") + except SyntaxError: + # It's possible that this error is from some bug in the assertion + # rewriting, but I don't know of a fast way to tell. + return + if hasattr(imp, "cache_from_source"): + # Handle PEP 3147 pycs. + pyc = py.path(imp.cache_from_source(mod.fspath)) + pyc.dirname.ensure(dir=True) + else: + pyc = mod.fspath + "c" + mod._pyc = pyc + mtime = int(mod.fspath.mtime()) + fp = pyc.open("wb") + try: + fp.write(imp.get_magic()) + fp.write(struct.pack(" 1: + res = ast.BoolOp(ast.And(), load_names) + else: + res = load_names[0] return res, self.explanation_param(self.pop_format_context(expl_call)) http://bitbucket.org/hpk42/pytest/changeset/f51063a9012e/ changeset: f51063a9012e branches: user: gutworth date: 2011-05-20 16:44:36 summary: improve this test affected #: 1 file (181 bytes) --- a/testing/test_assertrewrite.py Thu May 19 22:11:18 2011 -0500 +++ b/testing/test_assertrewrite.py Fri May 20 09:44:36 2011 -0500 @@ -48,24 +48,27 @@ class TestAssertionRewrite: def test_place_initial_imports(self): - s = """'Doc string'""" + s = """'Doc string'\nother = stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.Expr) assert isinstance(m.body[0].value, ast.Str) - for imp in m.body[1:]: + for imp in m.body[1:4]: assert isinstance(imp, ast.Import) - s = """from __future__ import with_statement""" + assert isinstance(m.body[4], ast.Assign) + s = """from __future__ import with_statement\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:]: + for imp in m.body[1:4]: assert isinstance(imp, ast.Import) - s = """'doc string'\nfrom __future__ import with_statement""" + assert isinstance(m.body[4], ast.Expr) + s = """'doc string'\nfrom __future__ import with_statement\nother""" m = rewrite(s) assert isinstance(m.body[0], ast.Expr) assert isinstance(m.body[0].value, ast.Str) assert isinstance(m.body[1], ast.ImportFrom) - for imp in m.body[2:]: + for imp in m.body[2:5]: assert isinstance(imp, ast.Import) + assert isinstance(m.body[5], ast.Expr) def test_name(self): def f(): http://bitbucket.org/hpk42/pytest/changeset/904deddfc353/ changeset: 904deddfc353 branches: user: gutworth date: 2011-05-25 00:21:58 summary: give initial imports a reasonable lineno affected #: 2 files (344 bytes) --- a/_pytest/assertrewrite.py Fri May 20 09:44:36 2011 -0500 +++ b/_pytest/assertrewrite.py Tue May 24 17:21:58 2011 -0500 @@ -92,18 +92,21 @@ aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), ast.alias("py", "@pylib"), ast.alias("_pytest.assertrewrite", "@pytest_ar")] - imports = [ast.Import([alias], lineno=0, col_offset=0) - for alias in aliases] expect_docstring = True pos = 0 + lineno = 0 for item in mod.body: if (expect_docstring and isinstance(item, ast.Expr) and isinstance(item.value, ast.Str)): + lineno += len(item.value.s.splitlines()) - 1 expect_docstring = False elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and item.identifier != "__future__"): + lineno = item.lineno break pos += 1 + imports = [ast.Import([alias], lineno=lineno, col_offset=0) + for alias in aliases] mod.body[pos:pos] = imports # Collect asserts. nodes = collections.deque([mod]) --- a/testing/test_assertrewrite.py Fri May 20 09:44:36 2011 -0500 +++ b/testing/test_assertrewrite.py Tue May 24 17:21:58 2011 -0500 @@ -54,12 +54,16 @@ assert isinstance(m.body[0].value, ast.Str) for imp in m.body[1:4]: assert isinstance(imp, ast.Import) + assert imp.lineno == 2 + assert imp.col_offset == 0 assert isinstance(m.body[4], ast.Assign) s = """from __future__ import with_statement\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) for imp in m.body[1:4]: assert isinstance(imp, ast.Import) + assert imp.lineno == 2 + assert imp.col_offset == 0 assert isinstance(m.body[4], ast.Expr) s = """'doc string'\nfrom __future__ import with_statement\nother""" m = rewrite(s) @@ -68,6 +72,8 @@ assert isinstance(m.body[1], ast.ImportFrom) for imp in m.body[2:5]: assert isinstance(imp, ast.Import) + assert imp.lineno == 3 + assert imp.col_offset == 0 assert isinstance(m.body[5], ast.Expr) def test_name(self): http://bitbucket.org/hpk42/pytest/changeset/7e2b622aa4f6/ changeset: 7e2b622aa4f6 branches: user: gutworth date: 2011-05-25 00:28:20 summary: fix sentence affected #: 1 file (18 bytes) --- a/_pytest/assertrewrite.py Tue May 24 17:21:58 2011 -0500 +++ b/_pytest/assertrewrite.py Tue May 24 17:28:20 2011 -0500 @@ -87,8 +87,8 @@ if not mod.body: # Nothing to do. return - # Insert some special imports at top but after any docstrings and - # __future__ imports. + # Insert some special imports at the top of the module but after any + # docstrings and __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), ast.alias("py", "@pylib"), ast.alias("_pytest.assertrewrite", "@pytest_ar")] http://bitbucket.org/hpk42/pytest/changeset/2abe5df9dffb/ changeset: 2abe5df9dffb branches: user: gutworth date: 2011-05-25 00:30:35 summary: add a way to disable assertion rewriting for a module affected #: 2 files (434 bytes) --- a/_pytest/assertrewrite.py Tue May 24 17:28:20 2011 -0500 +++ b/_pytest/assertrewrite.py Tue May 24 17:30:35 2011 -0500 @@ -98,7 +98,11 @@ for item in mod.body: if (expect_docstring and isinstance(item, ast.Expr) and isinstance(item.value, ast.Str)): - lineno += len(item.value.s.splitlines()) - 1 + doc = item.value.s + if "PYTEST_DONT_REWRITE" in doc: + # The module has disabled assertion rewriting. + return + lineno += len(doc) - 1 expect_docstring = False elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and item.identifier != "__future__"): --- a/testing/test_assertrewrite.py Tue May 24 17:28:20 2011 -0500 +++ b/testing/test_assertrewrite.py Tue May 24 17:30:35 2011 -0500 @@ -76,6 +76,14 @@ assert imp.col_offset == 0 assert isinstance(m.body[5], ast.Expr) + def test_dont_rewrite(self): + s = """'PYTEST_DONT_REWRITE'\nassert 14""" + m = rewrite(s) + assert len(m.body) == 2 + assert isinstance(m.body[0].value, ast.Str) + assert isinstance(m.body[1], ast.Assert) + assert m.body[1].msg is None + def test_name(self): def f(): assert False http://bitbucket.org/hpk42/pytest/changeset/df6ec5b68a65/ changeset: df6ec5b68a65 branches: user: gutworth date: 2011-05-25 00:48:56 summary: refactor writing the fake pyc into its own function affected #: 1 file (75 bytes) --- a/_pytest/assertion.py Tue May 24 17:30:35 2011 -0500 +++ b/_pytest/assertion.py Tue May 24 17:48:56 2011 -0500 @@ -43,6 +43,23 @@ else: rewrite_asserts = None +def _write_pyc(co, source_path): + if hasattr(imp, "cache_from_source"): + # Handle PEP 3147 pycs. + pyc = py.path(imp.cache_from_source(source_math)) + pyc.dirname.ensure(dir=True) + else: + pyc = source_path + "c" + mtime = int(source_path.mtime()) + fp = pyc.open("wb") + try: + fp.write(imp.get_magic()) + fp.write(struct.pack("" + def myany(x): + return False + assert myany(A() < 0) + assert "< 0" in getmsg(f) http://bitbucket.org/hpk42/pytest/changeset/95c14cbc8a5e/ changeset: 95c14cbc8a5e branches: user: gutworth date: 2011-05-25 01:28:05 summary: use py.builtin.exec_ affected #: 1 file (11 bytes) --- a/testing/test_assertrewrite.py Tue May 24 18:15:08 2011 -0500 +++ b/testing/test_assertrewrite.py Tue May 24 18:28:05 2011 -0500 @@ -29,7 +29,7 @@ ns = {} if extra_ns is not None: ns.update(extra_ns) - exec code in ns + py.builtin.exec_(code, ns) func = ns[f.__name__] try: func() http://bitbucket.org/hpk42/pytest/changeset/74c7bc904259/ changeset: 74c7bc904259 branches: user: gutworth date: 2011-05-25 01:28:20 summary: account for py3 dict.values affected #: 1 file (6 bytes) --- a/_pytest/assertrewrite.py Tue May 24 18:28:05 2011 -0500 +++ b/_pytest/assertrewrite.py Tue May 24 18:28:20 2011 -0500 @@ -180,7 +180,7 @@ if self.stack: self.explanation_specifiers = self.stack[-1] keys = [ast.Str(key) for key in current.keys()] - format_dict = ast.Dict(keys, current.values()) + format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) http://bitbucket.org/hpk42/pytest/changeset/f94092f7a020/ changeset: f94092f7a020 branches: user: gutworth date: 2011-05-25 01:30:18 summary: account py3 range objects affected #: 1 file (6 bytes) --- a/testing/test_assertrewrite.py Tue May 24 18:28:20 2011 -0500 +++ b/testing/test_assertrewrite.py Tue May 24 18:30:18 2011 -0500 @@ -221,7 +221,7 @@ def test_len(self): def f(): - l = range(10) + l = list(range(10)) assert len(l) == 11 assert getmsg(f).startswith("""assert 10 == 11 + where 10 = len([""") http://bitbucket.org/hpk42/pytest/changeset/2ec7aaa48386/ changeset: 2ec7aaa48386 branches: user: gutworth date: 2011-05-25 22:55:57 summary: expand try/except/finally which py2.4 does't like affected #: 1 file (29 bytes) --- a/_pytest/python.py Tue May 24 18:30:18 2011 -0500 +++ b/_pytest/python.py Wed May 25 15:55:57 2011 -0500 @@ -228,7 +228,10 @@ self.ihook.pytest_pycollect_before_module_import(mod=self) # we assume we are only called once per module try: - mod = self.fspath.pyimport(ensuresyspath=True) + try: + mod = self.fspath.pyimport(ensuresyspath=True) + finally: + self.ihook.pytest_pycollect_after_module_import(mod=self) except SyntaxError: excinfo = py.code.ExceptionInfo() raise self.CollectError(excinfo.getrepr(style="short")) @@ -243,8 +246,6 @@ "HINT: use a unique basename for your test file modules" % e.args ) - finally: - self.ihook.pytest_pycollect_after_module_import(mod=self) #print "imported test module", mod self.config.pluginmanager.consider_module(mod) return mod http://bitbucket.org/hpk42/pytest/changeset/eefa041b09af/ changeset: eefa041b09af branches: user: gutworth date: 2011-05-25 23:18:45 summary: create the _pytest/assertion package affected #: 5 files (20.1 KB) --- a/_pytest/assertion.py Wed May 25 15:55:57 2011 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,236 +0,0 @@ -""" -support for presented detailed information in failing assertions. -""" -import py -import imp -import marshal -import struct -import sys -from _pytest.monkeypatch import monkeypatch - -try: - from _pytest.assertrewrite import rewrite_asserts -except ImportError: - rewrite_asserts = None -else: - import ast - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group._addoption('--no-assert', action="store_true", default=False, - dest="noassert", - help="disable python assert expression reinterpretation."), - -def pytest_configure(config): - global rewrite_asserts - # The _reprcompare attribute on the py.code module is used by - # py._code._assertionnew to detect this plugin was loaded and in - # turn call the hooks defined here as part of the - # DebugInterpreter. - m = monkeypatch() - config._cleanup.append(m.undo) - warn_about_missing_assertion() - if not config.getvalue("noassert") and not config.getvalue("nomagic"): - def callbinrepr(op, left, right): - hook_result = config.hook.pytest_assertrepr_compare( - config=config, op=op, left=left, right=right) - for new_expl in hook_result: - if new_expl: - return '\n~'.join(new_expl) - m.setattr(py.builtin.builtins, - 'AssertionError', py.code._AssertionError) - m.setattr(py.code, '_reprcompare', callbinrepr) - else: - rewrite_asserts = None - -def _write_pyc(co, source_path): - if hasattr(imp, "cache_from_source"): - # Handle PEP 3147 pycs. - pyc = py.path(imp.cache_from_source(source_math)) - pyc.dirname.ensure(dir=True) - else: - pyc = source_path + "c" - mtime = int(source_path.mtime()) - fp = pyc.open("wb") - try: - fp.write(imp.get_magic()) - fp.write(struct.pack(" 80*8: - explanation = ['Detailed information too verbose, truncated'] - - return [summary] + explanation - - -def _diff_text(left, right): - """Return the explanation for the diff between text - - This will skip leading and trailing characters which are - identical to keep the diff minimal. - """ - explanation = [] - i = 0 # just in case left or right has zero length - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - break - if i > 42: - i -= 10 # Provide some context - explanation = ['Skipping %s identical ' - 'leading characters in diff' % i] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += ['Skipping %s identical ' - 'trailing characters in diff' % i] - left = left[:-i] - right = right[:-i] - explanation += [line.strip('\n') - for line in py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] - return explanation - - -def _compare_eq_sequence(left, right): - explanation = [] - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (i, left[i], right[i])] - break - if len(left) > len(right): - explanation += ['Left contains more items, ' - 'first extra item: %s' % py.io.saferepr(left[len(right)],)] - elif len(left) < len(right): - explanation += ['Right contains more items, ' - 'first extra item: %s' % py.io.saferepr(right[len(left)],)] - return explanation # + _diff_text(py.std.pprint.pformat(left), - # py.std.pprint.pformat(right)) - - -def _compare_eq_set(left, right): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append('Extra items in the left set:') - for item in diff_left: - explanation.append(py.io.saferepr(item)) - if diff_right: - explanation.append('Extra items in the right set:') - for item in diff_right: - explanation.append(py.io.saferepr(item)) - return explanation - - -def _notin_text(term, text): - index = text.find(term) - head = text[:index] - tail = text[index+len(term):] - correct_text = head + tail - diff = _diff_text(correct_text, text) - newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] - for line in diff: - if line.startswith('Skipping'): - continue - if line.startswith('- '): - continue - if line.startswith('+ '): - newdiff.append(' ' + line[2:]) - else: - newdiff.append(line) - return newdiff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/__init__.py Wed May 25 16:18:45 2011 -0500 @@ -0,0 +1,236 @@ +""" +support for presented detailed information in failing assertions. +""" +import py +import imp +import marshal +import struct +import sys +from _pytest.monkeypatch import monkeypatch + +try: + from _pytest.assertion.rewrite import rewrite_asserts +except ImportError: + rewrite_asserts = None +else: + import ast + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group._addoption('--no-assert', action="store_true", default=False, + dest="noassert", + help="disable python assert expression reinterpretation."), + +def pytest_configure(config): + global rewrite_asserts + # The _reprcompare attribute on the py.code module is used by + # py._code._assertionnew to detect this plugin was loaded and in + # turn call the hooks defined here as part of the + # DebugInterpreter. + m = monkeypatch() + config._cleanup.append(m.undo) + warn_about_missing_assertion() + if not config.getvalue("noassert") and not config.getvalue("nomagic"): + def callbinrepr(op, left, right): + hook_result = config.hook.pytest_assertrepr_compare( + config=config, op=op, left=left, right=right) + for new_expl in hook_result: + if new_expl: + return '\n~'.join(new_expl) + m.setattr(py.builtin.builtins, + 'AssertionError', py.code._AssertionError) + m.setattr(py.code, '_reprcompare', callbinrepr) + else: + rewrite_asserts = None + +def _write_pyc(co, source_path): + if hasattr(imp, "cache_from_source"): + # Handle PEP 3147 pycs. + pyc = py.path(imp.cache_from_source(source_math)) + pyc.dirname.ensure(dir=True) + else: + pyc = source_path + "c" + mtime = int(source_path.mtime()) + fp = pyc.open("wb") + try: + fp.write(imp.get_magic()) + fp.write(struct.pack(" 80*8: + explanation = ['Detailed information too verbose, truncated'] + + return [summary] + explanation + + +def _diff_text(left, right): + """Return the explanation for the diff between text + + This will skip leading and trailing characters which are + identical to keep the diff minimal. + """ + explanation = [] + i = 0 # just in case left or right has zero length + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = ['Skipping %s identical ' + 'leading characters in diff' % i] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += ['Skipping %s identical ' + 'trailing characters in diff' % i] + left = left[:-i] + right = right[:-i] + explanation += [line.strip('\n') + for line in py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + return explanation + + +def _compare_eq_sequence(left, right): + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + explanation += ['At index %s diff: %r != %r' % + (i, left[i], right[i])] + break + if len(left) > len(right): + explanation += ['Left contains more items, ' + 'first extra item: %s' % py.io.saferepr(left[len(right)],)] + elif len(left) < len(right): + explanation += ['Right contains more items, ' + 'first extra item: %s' % py.io.saferepr(right[len(left)],)] + return explanation # + _diff_text(py.std.pprint.pformat(left), + # py.std.pprint.pformat(right)) + + +def _compare_eq_set(left, right): + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append('Extra items in the left set:') + for item in diff_left: + explanation.append(py.io.saferepr(item)) + if diff_right: + explanation.append('Extra items in the right set:') + for item in diff_right: + explanation.append(py.io.saferepr(item)) + return explanation + + +def _notin_text(term, text): + index = text.find(term) + head = text[:index] + tail = text[index+len(term):] + correct_text = head + tail + diff = _diff_text(correct_text, text) + newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] + for line in diff: + if line.startswith('Skipping'): + continue + if line.startswith('- '): + continue + if line.startswith('+ '): + newdiff.append(' ' + line[2:]) + else: + newdiff.append(line) + return newdiff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/rewrite.py Wed May 25 16:18:45 2011 -0500 @@ -0,0 +1,332 @@ +"""Rewrite assertion AST to produce nice error messages""" + +import ast +import collections +import itertools + +import py + + +def rewrite_asserts(mod): + """Rewrite the assert statements in mod.""" + AssertionRewriter().run(mod) + + +_saferepr = py.io.saferepr +_format_explanation = py.code._format_explanation + +def _format_boolop(operands, explanations, is_or): + show_explanations = [] + for operand, expl in zip(operands, explanations): + show_explanations.append(expl) + if operand == is_or: + break + return "(" + (is_or and " or " or " and ").join(show_explanations) + ")" + +def _call_reprcompare(ops, results, expls, each_obj): + for i, res, expl in zip(range(len(ops)), results, expls): + try: + done = not res + except Exception: + done = True + if done: + break + if py.code._reprcompare is not None: + custom = py.code._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if custom is not None: + return custom + return expl + + +unary_map = { + ast.Not : "not %s", + ast.Invert : "~%s", + ast.USub : "-%s", + ast.UAdd : "+%s" +} + +binop_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" +} + + +def set_location(node, lineno, col_offset): + """Set node location information recursively.""" + def _fix(node, lineno, col_offset): + if "lineno" in node._attributes: + node.lineno = lineno + if "col_offset" in node._attributes: + node.col_offset = col_offset + for child in ast.iter_child_nodes(node): + _fix(child, lineno, col_offset) + _fix(node, lineno, col_offset) + return node + + +class AssertionRewriter(ast.NodeVisitor): + + def run(self, mod): + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return + # Insert some special imports at the top of the module but after any + # docstrings and __future__ imports. + aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), + ast.alias("py", "@pylib"), + ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] + expect_docstring = True + pos = 0 + lineno = 0 + for item in mod.body: + if (expect_docstring and isinstance(item, ast.Expr) and + isinstance(item.value, ast.Str)): + doc = item.value.s + if "PYTEST_DONT_REWRITE" in doc: + # The module has disabled assertion rewriting. + return + lineno += len(doc) - 1 + expect_docstring = False + elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and + item.identifier != "__future__"): + lineno = item.lineno + break + pos += 1 + imports = [ast.Import([alias], lineno=lineno, col_offset=0) + for alias in aliases] + mod.body[pos:pos] = imports + # Collect asserts. + nodes = collections.deque([mod]) + while nodes: + node = nodes.popleft() + for name, field in ast.iter_fields(node): + if isinstance(field, list): + new = [] + for i, child in enumerate(field): + if isinstance(child, ast.Assert): + # Transform assert. + new.extend(self.visit(child)) + else: + new.append(child) + if isinstance(child, ast.AST): + nodes.append(child) + setattr(node, name, new) + elif (isinstance(field, ast.AST) and + # Don't recurse into expressions as they can't contain + # asserts. + not isinstance(field, ast.expr)): + nodes.append(field) + + def variable(self): + """Get a new variable.""" + # Use a character invalid in python identifiers to avoid clashing. + name = "@py_assert" + str(next(self.variable_counter)) + self.variables.add(name) + return name + + def assign(self, expr): + """Give *expr* a name.""" + name = self.variable() + self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) + return ast.Name(name, ast.Load()) + + def display(self, expr): + """Call py.io.saferepr on the expression.""" + return self.helper("saferepr", expr) + + def helper(self, name, *args): + """Call a helper in this module.""" + py_name = ast.Name("@pytest_ar", ast.Load()) + attr = ast.Attribute(py_name, "_" + name, ast.Load()) + return ast.Call(attr, list(args), [], None, None) + + def builtin(self, name): + """Return the builtin called *name*.""" + builtin_name = ast.Name("@py_builtins", ast.Load()) + return ast.Attribute(builtin_name, name, ast.Load()) + + def explanation_param(self, expr): + specifier = "py" + str(next(self.variable_counter)) + self.explanation_specifiers[specifier] = expr + return "%(" + specifier + ")s" + + def push_format_context(self): + self.explanation_specifiers = {} + self.stack.append(self.explanation_specifiers) + + def pop_format_context(self, expl_expr): + current = self.stack.pop() + if self.stack: + self.explanation_specifiers = self.stack[-1] + keys = [ast.Str(key) for key in current.keys()] + format_dict = ast.Dict(keys, list(current.values())) + form = ast.BinOp(expl_expr, ast.Mod(), format_dict) + name = "@py_format" + str(next(self.variable_counter)) + self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) + return ast.Name(name, ast.Load()) + + def generic_visit(self, node): + """Handle expressions we don't have custom code for.""" + assert isinstance(node, ast.expr) + res = self.assign(node) + return res, self.explanation_param(self.display(res)) + + def visit_Assert(self, assert_): + if assert_.msg: + # There's already a message. Don't mess with it. + return [assert_] + self.statements = [] + self.variables = set() + self.variable_counter = itertools.count() + self.stack = [] + self.on_failure = [] + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) + # Create failure message. + body = self.on_failure + negation = ast.UnaryOp(ast.Not(), top_condition) + self.statements.append(ast.If(negation, body, [])) + explanation = "assert " + explanation + template = ast.Str(explanation) + msg = self.pop_format_context(template) + fmt = self.helper("format_explanation", msg) + body.append(ast.Assert(top_condition, fmt)) + # Delete temporary variables. + names = [ast.Name(name, ast.Del()) for name in self.variables] + if names: + delete = ast.Delete(names) + self.statements.append(delete) + # Fix line numbers. + for stmt in self.statements: + set_location(stmt, assert_.lineno, assert_.col_offset) + return self.statements + + def visit_Name(self, name): + # Check if the name is local or not. + locs = ast.Call(self.builtin("locals"), [], [], None, None) + globs = ast.Call(self.builtin("globals"), [], [], None, None) + ops = [ast.In(), ast.IsNot()] + test = ast.Compare(ast.Str(name.id), ops, [locs, globs]) + expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) + return name, self.explanation_param(expr) + + def visit_BoolOp(self, boolop): + operands = [] + explanations = [] + self.push_format_context() + for operand in boolop.values: + res, explanation = self.visit(operand) + operands.append(res) + explanations.append(explanation) + expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) + is_or = ast.Num(isinstance(boolop.op, ast.Or)) + expl_template = self.helper("format_boolop", + ast.Tuple(operands, ast.Load()), expls, + is_or) + expl = self.pop_format_context(expl_template) + res = self.assign(ast.BoolOp(boolop.op, operands)) + return res, self.explanation_param(expl) + + def visit_UnaryOp(self, unary): + pattern = unary_map[unary.op.__class__] + operand_res, operand_expl = self.visit(unary.operand) + res = self.assign(ast.UnaryOp(unary.op, operand_res)) + return res, pattern % (operand_expl,) + + def visit_BinOp(self, binop): + symbol = binop_map[binop.op.__class__] + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) + explanation = "(%s %s %s)" % (left_expl, symbol, right_expl) + res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) + return res, explanation + + def visit_Call(self, call): + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + new_star = new_kwarg = None + for arg in call.args: + res, expl = self.visit(arg) + new_args.append(res) + arg_expls.append(expl) + for keyword in call.keywords: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + arg_expls.append(keyword.arg + "=" + expl) + if call.starargs: + new_star, expl = self.visit(call.starargs) + arg_expls.append("*" + expl) + if call.kwargs: + new_kwarg, expl = self.visit(call.kwarg) + arg_expls.append("**" + expl) + expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) + new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) + return res, outer_expl + + def visit_Attribute(self, attr): + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + value, value_expl = self.visit(attr.value) + res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) + res_expl = self.explanation_param(self.display(res)) + pat = "%s\n{%s = %s.%s\n}" + expl = pat % (res_expl, res_expl, value_expl, attr.attr) + return res, expl + + def visit_Compare(self, comp): + self.push_format_context() + left_res, left_expl = self.visit(comp.left) + res_variables = [self.variable() for i in range(len(comp.ops))] + load_names = [ast.Name(v, ast.Load()) for v in res_variables] + store_names = [ast.Name(v, ast.Store()) for v in res_variables] + it = zip(range(len(comp.ops)), comp.ops, comp.comparators) + expls = [] + syms = [] + results = [left_res] + for i, op, next_operand in it: + next_res, next_expl = self.visit(next_operand) + results.append(next_res) + sym = binop_map[op.__class__] + syms.append(ast.Str(sym)) + expl = "%s %s %s" % (left_expl, sym, next_expl) + expls.append(ast.Str(expl)) + res_expr = ast.Compare(left_res, [op], [next_res]) + self.statements.append(ast.Assign([store_names[i]], res_expr)) + left_res, left_expl = next_res, next_expl + # Use py.code._reprcompare if that's available. + expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), + ast.Tuple(load_names, ast.Load()), + ast.Tuple(expls, ast.Load()), + ast.Tuple(results, ast.Load())) + if len(comp.ops) > 1: + res = ast.BoolOp(ast.And(), load_names) + else: + res = load_names[0] + return res, self.explanation_param(self.pop_format_context(expl_call)) --- a/_pytest/assertrewrite.py Wed May 25 15:55:57 2011 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,332 +0,0 @@ -"""Rewrite assertion AST to produce nice error messages""" - -import ast -import collections -import itertools - -import py - - -def rewrite_asserts(mod): - """Rewrite the assert statements in mod.""" - AssertionRewriter().run(mod) - - -_saferepr = py.io.saferepr -_format_explanation = py.code._format_explanation - -def _format_boolop(operands, explanations, is_or): - show_explanations = [] - for operand, expl in zip(operands, explanations): - show_explanations.append(expl) - if operand == is_or: - break - return "(" + (is_or and " or " or " and ").join(show_explanations) + ")" - -def _call_reprcompare(ops, results, expls, each_obj): - for i, res, expl in zip(range(len(ops)), results, expls): - try: - done = not res - except Exception: - done = True - if done: - break - if py.code._reprcompare is not None: - custom = py.code._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) - if custom is not None: - return custom - return expl - - -unary_map = { - ast.Not : "not %s", - ast.Invert : "~%s", - ast.USub : "-%s", - ast.UAdd : "+%s" -} - -binop_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" -} - - -def set_location(node, lineno, col_offset): - """Set node location information recursively.""" - def _fix(node, lineno, col_offset): - if "lineno" in node._attributes: - node.lineno = lineno - if "col_offset" in node._attributes: - node.col_offset = col_offset - for child in ast.iter_child_nodes(node): - _fix(child, lineno, col_offset) - _fix(node, lineno, col_offset) - return node - - -class AssertionRewriter(ast.NodeVisitor): - - def run(self, mod): - """Find all assert statements in *mod* and rewrite them.""" - if not mod.body: - # Nothing to do. - return - # Insert some special imports at the top of the module but after any - # docstrings and __future__ imports. - aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), - ast.alias("py", "@pylib"), - ast.alias("_pytest.assertrewrite", "@pytest_ar")] - expect_docstring = True - pos = 0 - lineno = 0 - for item in mod.body: - if (expect_docstring and isinstance(item, ast.Expr) and - isinstance(item.value, ast.Str)): - doc = item.value.s - if "PYTEST_DONT_REWRITE" in doc: - # The module has disabled assertion rewriting. - return - lineno += len(doc) - 1 - expect_docstring = False - elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and - item.identifier != "__future__"): - lineno = item.lineno - break - pos += 1 - imports = [ast.Import([alias], lineno=lineno, col_offset=0) - for alias in aliases] - mod.body[pos:pos] = imports - # Collect asserts. - nodes = collections.deque([mod]) - while nodes: - node = nodes.popleft() - for name, field in ast.iter_fields(node): - if isinstance(field, list): - new = [] - for i, child in enumerate(field): - if isinstance(child, ast.Assert): - # Transform assert. - new.extend(self.visit(child)) - else: - new.append(child) - if isinstance(child, ast.AST): - nodes.append(child) - setattr(node, name, new) - elif (isinstance(field, ast.AST) and - # Don't recurse into expressions as they can't contain - # asserts. - not isinstance(field, ast.expr)): - nodes.append(field) - - def variable(self): - """Get a new variable.""" - # Use a character invalid in python identifiers to avoid clashing. - name = "@py_assert" + str(next(self.variable_counter)) - self.variables.add(name) - return name - - def assign(self, expr): - """Give *expr* a name.""" - name = self.variable() - self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) - return ast.Name(name, ast.Load()) - - def display(self, expr): - """Call py.io.saferepr on the expression.""" - return self.helper("saferepr", expr) - - def helper(self, name, *args): - """Call a helper in this module.""" - py_name = ast.Name("@pytest_ar", ast.Load()) - attr = ast.Attribute(py_name, "_" + name, ast.Load()) - return ast.Call(attr, list(args), [], None, None) - - def builtin(self, name): - """Return the builtin called *name*.""" - builtin_name = ast.Name("@py_builtins", ast.Load()) - return ast.Attribute(builtin_name, name, ast.Load()) - - def explanation_param(self, expr): - specifier = "py" + str(next(self.variable_counter)) - self.explanation_specifiers[specifier] = expr - return "%(" + specifier + ")s" - - def push_format_context(self): - self.explanation_specifiers = {} - self.stack.append(self.explanation_specifiers) - - def pop_format_context(self, expl_expr): - current = self.stack.pop() - if self.stack: - self.explanation_specifiers = self.stack[-1] - keys = [ast.Str(key) for key in current.keys()] - format_dict = ast.Dict(keys, list(current.values())) - form = ast.BinOp(expl_expr, ast.Mod(), format_dict) - name = "@py_format" + str(next(self.variable_counter)) - self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) - return ast.Name(name, ast.Load()) - - def generic_visit(self, node): - """Handle expressions we don't have custom code for.""" - assert isinstance(node, ast.expr) - res = self.assign(node) - return res, self.explanation_param(self.display(res)) - - def visit_Assert(self, assert_): - if assert_.msg: - # There's already a message. Don't mess with it. - return [assert_] - self.statements = [] - self.variables = set() - self.variable_counter = itertools.count() - self.stack = [] - self.on_failure = [] - self.push_format_context() - # Rewrite assert into a bunch of statements. - top_condition, explanation = self.visit(assert_.test) - # Create failure message. - body = self.on_failure - negation = ast.UnaryOp(ast.Not(), top_condition) - self.statements.append(ast.If(negation, body, [])) - explanation = "assert " + explanation - template = ast.Str(explanation) - msg = self.pop_format_context(template) - fmt = self.helper("format_explanation", msg) - body.append(ast.Assert(top_condition, fmt)) - # Delete temporary variables. - names = [ast.Name(name, ast.Del()) for name in self.variables] - if names: - delete = ast.Delete(names) - self.statements.append(delete) - # Fix line numbers. - for stmt in self.statements: - set_location(stmt, assert_.lineno, assert_.col_offset) - return self.statements - - def visit_Name(self, name): - # Check if the name is local or not. - locs = ast.Call(self.builtin("locals"), [], [], None, None) - globs = ast.Call(self.builtin("globals"), [], [], None, None) - ops = [ast.In(), ast.IsNot()] - test = ast.Compare(ast.Str(name.id), ops, [locs, globs]) - expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) - return name, self.explanation_param(expr) - - def visit_BoolOp(self, boolop): - operands = [] - explanations = [] - self.push_format_context() - for operand in boolop.values: - res, explanation = self.visit(operand) - operands.append(res) - explanations.append(explanation) - expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) - is_or = ast.Num(isinstance(boolop.op, ast.Or)) - expl_template = self.helper("format_boolop", - ast.Tuple(operands, ast.Load()), expls, - is_or) - expl = self.pop_format_context(expl_template) - res = self.assign(ast.BoolOp(boolop.op, operands)) - return res, self.explanation_param(expl) - - def visit_UnaryOp(self, unary): - pattern = unary_map[unary.op.__class__] - operand_res, operand_expl = self.visit(unary.operand) - res = self.assign(ast.UnaryOp(unary.op, operand_res)) - return res, pattern % (operand_expl,) - - def visit_BinOp(self, binop): - symbol = binop_map[binop.op.__class__] - left_expr, left_expl = self.visit(binop.left) - right_expr, right_expl = self.visit(binop.right) - explanation = "(%s %s %s)" % (left_expl, symbol, right_expl) - res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) - return res, explanation - - def visit_Call(self, call): - new_func, func_expl = self.visit(call.func) - arg_expls = [] - new_args = [] - new_kwargs = [] - new_star = new_kwarg = None - for arg in call.args: - res, expl = self.visit(arg) - new_args.append(res) - arg_expls.append(expl) - for keyword in call.keywords: - res, expl = self.visit(keyword.value) - new_kwargs.append(ast.keyword(keyword.arg, res)) - arg_expls.append(keyword.arg + "=" + expl) - if call.starargs: - new_star, expl = self.visit(call.starargs) - arg_expls.append("*" + expl) - if call.kwargs: - new_kwarg, expl = self.visit(call.kwarg) - arg_expls.append("**" + expl) - expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) - new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg) - res = self.assign(new_call) - res_expl = self.explanation_param(self.display(res)) - outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) - return res, outer_expl - - def visit_Attribute(self, attr): - if not isinstance(attr.ctx, ast.Load): - return self.generic_visit(attr) - value, value_expl = self.visit(attr.value) - res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) - res_expl = self.explanation_param(self.display(res)) - pat = "%s\n{%s = %s.%s\n}" - expl = pat % (res_expl, res_expl, value_expl, attr.attr) - return res, expl - - def visit_Compare(self, comp): - self.push_format_context() - left_res, left_expl = self.visit(comp.left) - res_variables = [self.variable() for i in range(len(comp.ops))] - load_names = [ast.Name(v, ast.Load()) for v in res_variables] - store_names = [ast.Name(v, ast.Store()) for v in res_variables] - it = zip(range(len(comp.ops)), comp.ops, comp.comparators) - expls = [] - syms = [] - results = [left_res] - for i, op, next_operand in it: - next_res, next_expl = self.visit(next_operand) - results.append(next_res) - sym = binop_map[op.__class__] - syms.append(ast.Str(sym)) - expl = "%s %s %s" % (left_expl, sym, next_expl) - expls.append(ast.Str(expl)) - res_expr = ast.Compare(left_res, [op], [next_res]) - self.statements.append(ast.Assign([store_names[i]], res_expr)) - left_res, left_expl = next_res, next_expl - # Use py.code._reprcompare if that's available. - expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), - ast.Tuple(load_names, ast.Load()), - ast.Tuple(expls, ast.Load()), - ast.Tuple(results, ast.Load())) - if len(comp.ops) > 1: - res = ast.BoolOp(ast.And(), load_names) - else: - res = load_names[0] - return res, self.explanation_param(self.pop_format_context(expl_call)) --- a/testing/test_assertrewrite.py Wed May 25 15:55:57 2011 -0500 +++ b/testing/test_assertrewrite.py Wed May 25 16:18:45 2011 -0500 @@ -4,7 +4,7 @@ ast = pytest.importorskip("ast") -from _pytest.assertrewrite import rewrite_asserts +from _pytest.assertion.rewrite import rewrite_asserts def setup_module(mod): http://bitbucket.org/hpk42/pytest/changeset/6c90e65621ad/ changeset: 6c90e65621ad branches: user: gutworth date: 2011-05-26 00:54:02 summary: import assertion code from pylib affected #: 8 files (41.1 KB) --- a/_pytest/assertion/__init__.py Wed May 25 16:18:45 2011 -0500 +++ b/_pytest/assertion/__init__.py Wed May 25 17:54:02 2011 -0500 @@ -31,15 +31,16 @@ config._cleanup.append(m.undo) warn_about_missing_assertion() if not config.getvalue("noassert") and not config.getvalue("nomagic"): + from _pytest.assertion import reinterpret def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) for new_expl in hook_result: if new_expl: return '\n~'.join(new_expl) - m.setattr(py.builtin.builtins, - 'AssertionError', py.code._AssertionError) - m.setattr(py.code, '_reprcompare', callbinrepr) + m.setattr(py.builtin.builtins, 'AssertionError', + reinterpret.AssertionError) + m.setattr(sys.modules[__name__], '_reprcompare', callbinrepr) else: rewrite_asserts = None @@ -98,6 +99,53 @@ sys.stderr.write("WARNING: failing tests may report as passing because " "assertions are turned off! (are you using python -O?)\n") +# if set, will be called by assert reinterp for comparison ops +_reprcompare = None + +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) + + # Provide basestring in python3 try: basestring = basestring --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/newinterpret.py Wed May 25 17:54:02 2011 -0500 @@ -0,0 +1,340 @@ +""" +Find intermediate evalutation results in assert statements through builtin AST. +This should replace oldinterpret.py eventually. +""" + +import sys +import ast + +import py +from _pytest import assertion +from _pytest.assertion import _format_explanation +from _pytest.assertion.reinterpret import 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): + if frame is None: + frame = py.code.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 + + if assertion._reprcompare is not None: + res = assertion._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) + 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 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/oldinterpret.py Wed May 25 17:54:02 2011 -0500 @@ -0,0 +1,556 @@ +import py +import sys, inspect +from compiler import parse, ast, pycodegen +from _pytest.assertion import _format_explanation +from _pytest.assertion.reinterpret import 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(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): + 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, py.std.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: + 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): + 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())") --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/reinterpret.py Wed May 25 17:54:02 2011 -0500 @@ -0,0 +1,48 @@ +import sys +import py + +BuiltinAssertionError = py.builtin.builtins.AssertionError + +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 = py.code.Frame(sys._getframe(1)) + try: + source = f.code.fullsource + if source is not None: + try: + source = source.getstatement(f.lineno, assertion=True) + except IndexError: + source = None + else: + source = str(source.deindent()).strip() + except py.error.ENOENT: + source = None + # this can also occur during reinterpretation, when the + # co_filename is set to "". + if source: + self.msg = reinterpret(source, f, should_fail=True) + else: + self.msg = "" + if not self.args: + self.args = (self.msg,) + +if sys.version_info > (3, 0): + AssertionError.__module__ = "builtins" + reinterpret_old = "old reinterpretation not available for py3" +else: + from _pytest.assertion.oldinterpret import interpret as reinterpret_old +if sys.version_info >= (2, 6) or (sys.platform.startswith("java")): + from _pytest.assertion.newinterpret import interpret as reinterpret +else: + reinterpret = reinterpret_old + --- a/_pytest/assertion/rewrite.py Wed May 25 16:18:45 2011 -0500 +++ b/_pytest/assertion/rewrite.py Wed May 25 17:54:02 2011 -0500 @@ -13,7 +13,6 @@ _saferepr = py.io.saferepr -_format_explanation = py.code._format_explanation def _format_boolop(operands, explanations, is_or): show_explanations = [] @@ -31,8 +30,9 @@ done = True if done: break - if py.code._reprcompare is not None: - custom = py.code._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + from _pytest.assertion import _reprcompare + if _reprcompare is not None: + custom = _reprcompare(ops[i], each_obj[i], each_obj[i + 1]) if custom is not None: return custom return expl @@ -94,7 +94,7 @@ # Insert some special imports at the top of the module but after any # docstrings and __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), - ast.alias("py", "@pylib"), + ast.alias("_pytest.assertion", "@pytest_a"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] expect_docstring = True pos = 0 @@ -153,11 +153,11 @@ def display(self, expr): """Call py.io.saferepr on the expression.""" - return self.helper("saferepr", expr) + return self.helper("ar", "saferepr", expr) - def helper(self, name, *args): + def helper(self, mod, name, *args): """Call a helper in this module.""" - py_name = ast.Name("@pytest_ar", ast.Load()) + py_name = ast.Name("@pytest_" + mod, ast.Load()) attr = ast.Attribute(py_name, "_" + name, ast.Load()) return ast.Call(attr, list(args), [], None, None) @@ -211,7 +211,7 @@ explanation = "assert " + explanation template = ast.Str(explanation) msg = self.pop_format_context(template) - fmt = self.helper("format_explanation", msg) + fmt = self.helper("a", "format_explanation", msg) body.append(ast.Assert(top_condition, fmt)) # Delete temporary variables. names = [ast.Name(name, ast.Del()) for name in self.variables] @@ -242,7 +242,7 @@ explanations.append(explanation) expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) is_or = ast.Num(isinstance(boolop.op, ast.Or)) - expl_template = self.helper("format_boolop", + expl_template = self.helper("ar", "format_boolop", ast.Tuple(operands, ast.Load()), expls, is_or) expl = self.pop_format_context(expl_template) @@ -321,7 +321,8 @@ self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl # Use py.code._reprcompare if that's available. - expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), + expl_call = self.helper("ar", "call_reprcompare", + ast.Tuple(syms, ast.Load()), ast.Tuple(load_names, ast.Load()), ast.Tuple(expls, ast.Load()), ast.Tuple(results, ast.Load())) --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/testing/test_assertinterpret.py Wed May 25 17:54:02 2011 -0500 @@ -0,0 +1,327 @@ +"PYTEST_DONT_REWRITE" +import pytest, py + +from _pytest import assertion + +def exvalue(): + return py.std.sys.exc_info()[1] + +def f(): + return 2 + +def test_not_being_rewritten(): + assert "@py_builtins" not in globals() + +def test_assert(): + try: + assert f() == 3 + except AssertionError: + e = exvalue() + s = str(e) + assert s.startswith('assert 2 == 3\n') + +def test_assert_with_explicit_message(): + try: + assert f() == 3, "hello" + except AssertionError: + e = exvalue() + assert e.msg == 'hello' + +def test_assert_within_finally(): + class A: + def f(): + pass + excinfo = py.test.raises(TypeError, """ + try: + A().f() + finally: + i = 42 + """) + s = excinfo.exconly() + assert s.find("takes no argument") != -1 + + #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") + + + at py.test.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") + + at py.test.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_non_string_message(): + class A: + def __str__(self): + return "hello" + try: + assert 0 == 1, A() + except AssertionError: + e = exvalue() + assert e.msg == "hello" + +def test_assert_keyword_arg(): + def f(x=3): + return False + try: + assert f(x=5) + except AssertionError: + e = exvalue() + assert "x=5" in e.msg + +# 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: + py.test.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.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"] + + at py.test.mark.skipif("sys.version_info < (2,6)") +def test_assert_customizable_reprcompare(monkeypatch): + monkeypatch.setattr(assertion, '_reprcompare', lambda *args: 'hello') + try: + assert 3 == 4 + except AssertionError: + e = exvalue() + s = str(e) + assert "hello" in s + +def test_assert_long_source_1(): + try: + assert len == [ + (None, ['somet text', 'more text']), + ] + except AssertionError: + e = exvalue() + s = str(e) + assert 're-run' not in s + assert 'somet text' in s + +def test_assert_long_source_2(): + try: + assert(len == [ + (None, ['somet text', 'more text']), + ]) + except AssertionError: + e = exvalue() + s = str(e) + assert 're-run' not in s + assert 'somet text' in s + +def test_assert_raise_alias(testdir): + testdir.makepyfile(""" + "PYTEST_DONT_REWRITE" + import sys + EX = AssertionError + def test_hello(): + raise EX("hello" + "multi" + "line") + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello*", + "*raise EX*", + "*1 failed*", + ]) + + + at 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 --- a/testing/test_assertion.py Wed May 25 16:18:45 2011 -0500 +++ b/testing/test_assertion.py Wed May 25 17:54:02 2011 -0500 @@ -2,11 +2,12 @@ import py, pytest import _pytest.assertion as plugin +from _pytest.assertion import reinterpret needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") def interpret(expr): - return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) + return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1))) class TestBinReprIntegration: pytestmark = needsnewassert @@ -25,7 +26,7 @@ self.right = right mockhook = MockHook() monkeypatch = request.getfuncargvalue("monkeypatch") - monkeypatch.setattr(py.code, '_reprcompare', mockhook) + monkeypatch.setattr(plugin, '_reprcompare', mockhook) return mockhook def test_pytest_assertrepr_compare_called(self, hook): @@ -40,13 +41,13 @@ assert hook.right == [0, 2] def test_configure_unconfigure(self, testdir, hook): - assert hook == py.code._reprcompare + assert hook == plugin._reprcompare config = testdir.parseconfig() plugin.pytest_configure(config) - assert hook != py.code._reprcompare + assert hook != plugin._reprcompare from _pytest.config import pytest_unconfigure pytest_unconfigure(config) - assert hook == py.code._reprcompare + assert hook == plugin._reprcompare def callequal(left, right): return plugin.pytest_assertrepr_compare('==', left, right) --- a/testing/test_assertrewrite.py Wed May 25 16:18:45 2011 -0500 +++ b/testing/test_assertrewrite.py Wed May 25 17:54:02 2011 -0500 @@ -4,15 +4,16 @@ ast = pytest.importorskip("ast") +from _pytest import assertion from _pytest.assertion.rewrite import rewrite_asserts def setup_module(mod): - mod._old_reprcompare = py.code._reprcompare + mod._old_reprcompare = assertion._reprcompare py.code._reprcompare = None def teardown_module(mod): - py.code._reprcompare = mod._old_reprcompare + assertion._reprcompare = mod._old_reprcompare del mod._old_reprcompare @@ -229,13 +230,13 @@ def test_custom_reprcompare(self, monkeypatch): def my_reprcompare(op, left, right): return "42" - monkeypatch.setattr(py.code, "_reprcompare", my_reprcompare) + monkeypatch.setattr(assertion, "_reprcompare", my_reprcompare) def f(): assert 42 < 3 assert getmsg(f) == "assert 42" def my_reprcompare(op, left, right): return "%s %s %s" % (left, op, right) - monkeypatch.setattr(py.code, "_reprcompare", my_reprcompare) + monkeypatch.setattr(assertion, "_reprcompare", my_reprcompare) def f(): assert 1 < 3 < 5 <= 4 < 7 assert getmsg(f) == "assert 5 <= 4" http://bitbucket.org/hpk42/pytest/changeset/729c4984b76e/ changeset: 729c4984b76e branches: user: gutworth date: 2011-05-26 19:01:34 summary: refactor explanation formatting things into their own module affected #: 7 files (6.3 KB) --- a/_pytest/assertion/__init__.py Wed May 25 17:54:02 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 12:01:34 2011 -0500 @@ -7,6 +7,7 @@ import struct import sys from _pytest.monkeypatch import monkeypatch +from _pytest.assertion import reinterpret, util try: from _pytest.assertion.rewrite import rewrite_asserts @@ -31,7 +32,6 @@ config._cleanup.append(m.undo) warn_about_missing_assertion() if not config.getvalue("noassert") and not config.getvalue("nomagic"): - from _pytest.assertion import reinterpret def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) @@ -40,7 +40,7 @@ return '\n~'.join(new_expl) m.setattr(py.builtin.builtins, 'AssertionError', reinterpret.AssertionError) - m.setattr(sys.modules[__name__], '_reprcompare', callbinrepr) + m.setattr(util, '_reprcompare', callbinrepr) else: rewrite_asserts = None @@ -99,186 +99,4 @@ sys.stderr.write("WARNING: failing tests may report as passing because " "assertions are turned off! (are you using python -O?)\n") -# if set, will be called by assert reinterp for comparison ops -_reprcompare = None - -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) - - -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - -def pytest_assertrepr_compare(op, left, right): - """return specialised explanations for some operators/operands""" - width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = py.io.saferepr(left, maxsize=int(width/2)) - right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) - summary = '%s %s %s' % (left_repr, op, right_repr) - - issequence = lambda x: isinstance(x, (list, tuple)) - istext = lambda x: isinstance(x, basestring) - isdict = lambda x: isinstance(x, dict) - isset = lambda x: isinstance(x, set) - - explanation = None - try: - if op == '==': - if istext(left) and istext(right): - explanation = _diff_text(left, right) - elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right) - elif isdict(left) and isdict(right): - explanation = _diff_text(py.std.pprint.pformat(left), - py.std.pprint.pformat(right)) - elif op == 'not in': - if istext(left) and istext(right): - explanation = _notin_text(left, right) - except py.builtin._sysex: - raise - except: - excinfo = py.code.ExceptionInfo() - explanation = ['(pytest_assertion plugin: representation of ' - 'details failed. Probably an object has a faulty __repr__.)', - str(excinfo) - ] - - - if not explanation: - return None - - # Don't include pageloads of data, should be configurable - if len(''.join(explanation)) > 80*8: - explanation = ['Detailed information too verbose, truncated'] - - return [summary] + explanation - - -def _diff_text(left, right): - """Return the explanation for the diff between text - - This will skip leading and trailing characters which are - identical to keep the diff minimal. - """ - explanation = [] - i = 0 # just in case left or right has zero length - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - break - if i > 42: - i -= 10 # Provide some context - explanation = ['Skipping %s identical ' - 'leading characters in diff' % i] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += ['Skipping %s identical ' - 'trailing characters in diff' % i] - left = left[:-i] - right = right[:-i] - explanation += [line.strip('\n') - for line in py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] - return explanation - - -def _compare_eq_sequence(left, right): - explanation = [] - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (i, left[i], right[i])] - break - if len(left) > len(right): - explanation += ['Left contains more items, ' - 'first extra item: %s' % py.io.saferepr(left[len(right)],)] - elif len(left) < len(right): - explanation += ['Right contains more items, ' - 'first extra item: %s' % py.io.saferepr(right[len(left)],)] - return explanation # + _diff_text(py.std.pprint.pformat(left), - # py.std.pprint.pformat(right)) - - -def _compare_eq_set(left, right): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append('Extra items in the left set:') - for item in diff_left: - explanation.append(py.io.saferepr(item)) - if diff_right: - explanation.append('Extra items in the right set:') - for item in diff_right: - explanation.append(py.io.saferepr(item)) - return explanation - - -def _notin_text(term, text): - index = text.find(term) - head = text[:index] - tail = text[index+len(term):] - correct_text = head + tail - diff = _diff_text(correct_text, text) - newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] - for line in diff: - if line.startswith('Skipping'): - continue - if line.startswith('- '): - continue - if line.startswith('+ '): - newdiff.append(' ' + line[2:]) - else: - newdiff.append(line) - return newdiff +pytest_assertrepr_compare = util.assertrepr_compare --- a/_pytest/assertion/newinterpret.py Wed May 25 17:54:02 2011 -0500 +++ b/_pytest/assertion/newinterpret.py Thu May 26 12:01:34 2011 -0500 @@ -7,8 +7,7 @@ import ast import py -from _pytest import assertion -from _pytest.assertion import _format_explanation +from _pytest.assertion import util from _pytest.assertion.reinterpret import BuiltinAssertionError @@ -62,7 +61,7 @@ return interpret(offending_line, frame) def getfailure(failure): - explanation = _format_explanation(failure.explanation) + explanation = util.format_explanation(failure.explanation) value = failure.cause[1] if str(value): lines = explanation.splitlines() @@ -185,8 +184,8 @@ break left_explanation, left_result = next_explanation, next_result - if assertion._reprcompare is not None: - res = assertion._reprcompare(op_symbol, left_result, next_result) + if util._reprcompare is not None: + res = util._reprcompare(op_symbol, left_result, next_result) if res: explanation = res return explanation, result --- a/_pytest/assertion/oldinterpret.py Wed May 25 17:54:02 2011 -0500 +++ b/_pytest/assertion/oldinterpret.py Thu May 26 12:01:34 2011 -0500 @@ -1,7 +1,7 @@ import py import sys, inspect from compiler import parse, ast, pycodegen -from _pytest.assertion import _format_explanation +from _pytest.assertion.util import format_explanation from _pytest.assertion.reinterpret import BuiltinAssertionError passthroughex = py.builtin._sysex @@ -132,7 +132,7 @@ raise Failure(self) def nice_explanation(self): - return _format_explanation(self.explanation) + return format_explanation(self.explanation) class Name(Interpretable): --- a/_pytest/assertion/rewrite.py Wed May 25 17:54:02 2011 -0500 +++ b/_pytest/assertion/rewrite.py Thu May 26 12:01:34 2011 -0500 @@ -5,6 +5,7 @@ import itertools import py +from _pytest.assertion import util def rewrite_asserts(mod): @@ -13,6 +14,7 @@ _saferepr = py.io.saferepr +from _pytest.assertion.util import format_explanation as _format_explanation def _format_boolop(operands, explanations, is_or): show_explanations = [] @@ -30,9 +32,8 @@ done = True if done: break - from _pytest.assertion import _reprcompare - if _reprcompare is not None: - custom = _reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if util._reprcompare is not None: + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) if custom is not None: return custom return expl @@ -94,7 +95,6 @@ # Insert some special imports at the top of the module but after any # docstrings and __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), - ast.alias("_pytest.assertion", "@pytest_a"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] expect_docstring = True pos = 0 @@ -153,11 +153,11 @@ def display(self, expr): """Call py.io.saferepr on the expression.""" - return self.helper("ar", "saferepr", expr) + return self.helper("saferepr", expr) - def helper(self, mod, name, *args): + def helper(self, name, *args): """Call a helper in this module.""" - py_name = ast.Name("@pytest_" + mod, ast.Load()) + py_name = ast.Name("@pytest_ar", ast.Load()) attr = ast.Attribute(py_name, "_" + name, ast.Load()) return ast.Call(attr, list(args), [], None, None) @@ -211,7 +211,7 @@ explanation = "assert " + explanation template = ast.Str(explanation) msg = self.pop_format_context(template) - fmt = self.helper("a", "format_explanation", msg) + fmt = self.helper("format_explanation", msg) body.append(ast.Assert(top_condition, fmt)) # Delete temporary variables. names = [ast.Name(name, ast.Del()) for name in self.variables] @@ -242,7 +242,7 @@ explanations.append(explanation) expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) is_or = ast.Num(isinstance(boolop.op, ast.Or)) - expl_template = self.helper("ar", "format_boolop", + expl_template = self.helper("format_boolop", ast.Tuple(operands, ast.Load()), expls, is_or) expl = self.pop_format_context(expl_template) @@ -321,7 +321,7 @@ self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl # Use py.code._reprcompare if that's available. - expl_call = self.helper("ar", "call_reprcompare", + expl_call = self.helper("call_reprcompare", ast.Tuple(syms, ast.Load()), ast.Tuple(load_names, ast.Load()), ast.Tuple(expls, ast.Load()), --- a/testing/test_assertinterpret.py Wed May 25 17:54:02 2011 -0500 +++ b/testing/test_assertinterpret.py Thu May 26 12:01:34 2011 -0500 @@ -1,7 +1,7 @@ "PYTEST_DONT_REWRITE" import pytest, py -from _pytest import assertion +from _pytest.assertion import util def exvalue(): return py.std.sys.exc_info()[1] @@ -249,7 +249,7 @@ @py.test.mark.skipif("sys.version_info < (2,6)") def test_assert_customizable_reprcompare(monkeypatch): - monkeypatch.setattr(assertion, '_reprcompare', lambda *args: 'hello') + monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') try: assert 3 == 4 except AssertionError: --- a/testing/test_assertion.py Wed May 25 17:54:02 2011 -0500 +++ b/testing/test_assertion.py Thu May 26 12:01:34 2011 -0500 @@ -2,7 +2,7 @@ import py, pytest import _pytest.assertion as plugin -from _pytest.assertion import reinterpret +from _pytest.assertion import reinterpret, util needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") @@ -26,7 +26,7 @@ self.right = right mockhook = MockHook() monkeypatch = request.getfuncargvalue("monkeypatch") - monkeypatch.setattr(plugin, '_reprcompare', mockhook) + monkeypatch.setattr(util, '_reprcompare', mockhook) return mockhook def test_pytest_assertrepr_compare_called(self, hook): @@ -41,13 +41,13 @@ assert hook.right == [0, 2] def test_configure_unconfigure(self, testdir, hook): - assert hook == plugin._reprcompare + assert hook == util._reprcompare config = testdir.parseconfig() plugin.pytest_configure(config) - assert hook != plugin._reprcompare + assert hook != util._reprcompare from _pytest.config import pytest_unconfigure pytest_unconfigure(config) - assert hook == plugin._reprcompare + assert hook == util._reprcompare def callequal(left, right): return plugin.pytest_assertrepr_compare('==', left, right) --- a/testing/test_assertrewrite.py Wed May 25 17:54:02 2011 -0500 +++ b/testing/test_assertrewrite.py Thu May 26 12:01:34 2011 -0500 @@ -4,16 +4,16 @@ ast = pytest.importorskip("ast") -from _pytest import assertion +from _pytest.assertion import util from _pytest.assertion.rewrite import rewrite_asserts def setup_module(mod): - mod._old_reprcompare = assertion._reprcompare + mod._old_reprcompare = util._reprcompare py.code._reprcompare = None def teardown_module(mod): - assertion._reprcompare = mod._old_reprcompare + util._reprcompare = mod._old_reprcompare del mod._old_reprcompare @@ -53,29 +53,29 @@ m = rewrite(s) assert isinstance(m.body[0], ast.Expr) assert isinstance(m.body[0].value, ast.Str) - for imp in m.body[1:4]: + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[4], ast.Assign) + assert isinstance(m.body[3], ast.Assign) s = """from __future__ import with_statement\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:4]: + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[4], ast.Expr) + assert isinstance(m.body[3], ast.Expr) s = """'doc string'\nfrom __future__ import with_statement\nother""" m = rewrite(s) assert isinstance(m.body[0], ast.Expr) assert isinstance(m.body[0].value, ast.Str) assert isinstance(m.body[1], ast.ImportFrom) - for imp in m.body[2:5]: + for imp in m.body[2:4]: assert isinstance(imp, ast.Import) assert imp.lineno == 3 assert imp.col_offset == 0 - assert isinstance(m.body[5], ast.Expr) + assert isinstance(m.body[4], ast.Expr) def test_dont_rewrite(self): s = """'PYTEST_DONT_REWRITE'\nassert 14""" @@ -230,13 +230,13 @@ def test_custom_reprcompare(self, monkeypatch): def my_reprcompare(op, left, right): return "42" - monkeypatch.setattr(assertion, "_reprcompare", my_reprcompare) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare) def f(): assert 42 < 3 assert getmsg(f) == "assert 42" def my_reprcompare(op, left, right): return "%s %s %s" % (left, op, right) - monkeypatch.setattr(assertion, "_reprcompare", my_reprcompare) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare) def f(): assert 1 < 3 < 5 <= 4 < 7 assert getmsg(f) == "assert 5 <= 4" http://bitbucket.org/hpk42/pytest/changeset/9ac784842275/ changeset: 9ac784842275 branches: user: gutworth date: 2011-05-26 20:15:03 summary: fix comment affected #: 1 file (1 byte) --- a/_pytest/assertion/__init__.py Thu May 26 12:01:34 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 13:15:03 2011 -0500 @@ -65,7 +65,7 @@ if rewrite_asserts is None: return # Some deep magic: load the source, rewrite the asserts, and write a - # fake pyc, so that it'll be loaded further down this function. + # fake pyc, so that it'll be loaded when the module is imported. source = mod.fspath.read() try: tree = ast.parse(source) http://bitbucket.org/hpk42/pytest/changeset/637ccd9d613d/ changeset: 637ccd9d613d branches: user: gutworth date: 2011-05-26 20:15:21 summary: fix grammar affected #: 1 file (1 byte) --- a/_pytest/assertion/__init__.py Thu May 26 13:15:03 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 13:15:21 2011 -0500 @@ -1,5 +1,5 @@ """ -support for presented detailed information in failing assertions. +support for presenting detailed information in failing assertions. """ import py import imp http://bitbucket.org/hpk42/pytest/changeset/08e73722661a/ changeset: 08e73722661a branches: user: gutworth date: 2011-05-26 20:17:26 summary: forgot to util module affected #: 1 file (6.4 KB) --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/util.py Thu May 26 13:17:26 2011 -0500 @@ -0,0 +1,191 @@ +"""Utilities for assertion debugging""" + +import py + + +# The _reprcompare attribute on the util module is used by the new assertion +# interpretation code and assertion rewriter to detect this plugin was +# loaded and in turn call the hooks defined here as part of the +# DebugInterpreter. +_reprcompare = None + +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) + + +# Provide basestring in python3 +try: + basestring = basestring +except NameError: + basestring = str + + +def assertrepr_compare(op, left, right): + """return specialised explanations for some operators/operands""" + width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op + left_repr = py.io.saferepr(left, maxsize=int(width/2)) + right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) + summary = '%s %s %s' % (left_repr, op, right_repr) + + issequence = lambda x: isinstance(x, (list, tuple)) + istext = lambda x: isinstance(x, basestring) + isdict = lambda x: isinstance(x, dict) + isset = lambda x: isinstance(x, set) + + explanation = None + try: + if op == '==': + if istext(left) and istext(right): + explanation = _diff_text(left, right) + elif issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right) + elif isdict(left) and isdict(right): + explanation = _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) + elif op == 'not in': + if istext(left) and istext(right): + explanation = _notin_text(left, right) + except py.builtin._sysex: + raise + except: + excinfo = py.code.ExceptionInfo() + explanation = ['(pytest_assertion plugin: representation of ' + 'details failed. Probably an object has a faulty __repr__.)', + str(excinfo) + ] + + + if not explanation: + return None + + # Don't include pageloads of data, should be configurable + if len(''.join(explanation)) > 80*8: + explanation = ['Detailed information too verbose, truncated'] + + return [summary] + explanation + + +def _diff_text(left, right): + """Return the explanation for the diff between text + + This will skip leading and trailing characters which are + identical to keep the diff minimal. + """ + explanation = [] + i = 0 # just in case left or right has zero length + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = ['Skipping %s identical ' + 'leading characters in diff' % i] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += ['Skipping %s identical ' + 'trailing characters in diff' % i] + left = left[:-i] + right = right[:-i] + explanation += [line.strip('\n') + for line in py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + return explanation + + +def _compare_eq_sequence(left, right): + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + explanation += ['At index %s diff: %r != %r' % + (i, left[i], right[i])] + break + if len(left) > len(right): + explanation += ['Left contains more items, ' + 'first extra item: %s' % py.io.saferepr(left[len(right)],)] + elif len(left) < len(right): + explanation += ['Right contains more items, ' + 'first extra item: %s' % py.io.saferepr(right[len(left)],)] + return explanation # + _diff_text(py.std.pprint.pformat(left), + # py.std.pprint.pformat(right)) + + +def _compare_eq_set(left, right): + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append('Extra items in the left set:') + for item in diff_left: + explanation.append(py.io.saferepr(item)) + if diff_right: + explanation.append('Extra items in the right set:') + for item in diff_right: + explanation.append(py.io.saferepr(item)) + return explanation + + +def _notin_text(term, text): + index = text.find(term) + head = text[:index] + tail = text[index+len(term):] + correct_text = head + tail + diff = _diff_text(correct_text, text) + newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] + for line in diff: + if line.startswith('Skipping'): + continue + if line.startswith('- '): + continue + if line.startswith('+ '): + newdiff.append(' ' + line[2:]) + else: + newdiff.append(line) + return newdiff http://bitbucket.org/hpk42/pytest/changeset/5538d16228e8/ changeset: 5538d16228e8 branches: user: gutworth date: 2011-05-26 20:17:39 summary: this comment was moved away affected #: 1 file (213 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 13:17:26 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 13:17:39 2011 -0500 @@ -24,10 +24,6 @@ def pytest_configure(config): global rewrite_asserts - # The _reprcompare attribute on the py.code module is used by - # py._code._assertionnew to detect this plugin was loaded and in - # turn call the hooks defined here as part of the - # DebugInterpreter. m = monkeypatch() config._cleanup.append(m.undo) warn_about_missing_assertion() http://bitbucket.org/hpk42/pytest/changeset/7239a1d519f4/ changeset: 7239a1d519f4 branches: user: gutworth date: 2011-05-26 21:34:27 summary: introduce --assertmode option affected #: 3 files (1.7 KB) --- a/_pytest/assertion/__init__.py Thu May 26 13:17:39 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 14:34:27 2011 -0500 @@ -6,6 +6,7 @@ import marshal import struct import sys +import pytest from _pytest.monkeypatch import monkeypatch from _pytest.assertion import reinterpret, util @@ -18,26 +19,40 @@ def pytest_addoption(parser): group = parser.getgroup("debugconfig") + group._addoption('--assertmode', action="store", dest="assertmode", + choices=("on", "old", "off", "default"), default="default", + metavar="on|old|off", + help="Control assertion debugging tools") group._addoption('--no-assert', action="store_true", default=False, - dest="noassert", - help="disable python assert expression reinterpretation."), + dest="noassert", help="DEPRECATED equivalent to --assertmode=off") + group._addoption('--nomagic', action="store_true", default=False, + dest="nomagic", + help="DEPRECATED equivalent to --assertmode=off") + def pytest_configure(config): global rewrite_asserts - m = monkeypatch() - config._cleanup.append(m.undo) warn_about_missing_assertion() - if not config.getvalue("noassert") and not config.getvalue("nomagic"): + mode = config.getvalue("assertmode") + if config.getvalue("noassert") or config.getvalue("nomagic"): + if mode not in ("off", "default"): + raise pytest.UsageError("assertion options conflict") + mode = "off" + elif mode == "default": + mode = "on" + if mode != "off": def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) for new_expl in hook_result: if new_expl: return '\n~'.join(new_expl) + m = monkeypatch() + config._cleanup.append(m.undo) m.setattr(py.builtin.builtins, 'AssertionError', reinterpret.AssertionError) m.setattr(util, '_reprcompare', callbinrepr) - else: + if mode != "on": rewrite_asserts = None def _write_pyc(co, source_path): --- a/_pytest/helpconfig.py Thu May 26 13:17:39 2011 -0500 +++ b/_pytest/helpconfig.py Thu May 26 14:34:27 2011 -0500 @@ -16,9 +16,6 @@ group.addoption('--traceconfig', action="store_true", dest="traceconfig", default=False, help="trace considerations of conftest.py files."), - group._addoption('--nomagic', - action="store_true", dest="nomagic", default=False, - help="don't reinterpret asserts, no traceback cutting. ") group.addoption('--debug', action="store_true", dest="debug", default=False, help="generate and show internal debugging information.") --- a/testing/test_assertion.py Thu May 26 13:17:39 2011 -0500 +++ b/testing/test_assertion.py Thu May 26 14:34:27 2011 -0500 @@ -160,7 +160,7 @@ ]) -def test_functional(testdir): +def test_assertion_options(testdir): testdir.makepyfile(""" def test_hello(): x = 3 @@ -168,8 +168,30 @@ """) result = testdir.runpytest() assert "3 == 4" in result.stdout.str() - result = testdir.runpytest("--no-assert") - assert "3 == 4" not in result.stdout.str() + off_options = (("--no-assert",), + ("--nomagic",), + ("--no-assert", "--nomagic"), + ("--assertmode=off",), + ("--assertmode=off", "--no-assert"), + ("--assertmode=off", "--nomagic"), + ("--assertmode=off," "--no-assert", "--nomagic")) + for opt in off_options: + result = testdir.runpytest(*opt) + assert "3 == 4" not in result.stdout.str() + for mode in "on", "old": + for other_opt in off_options[:3]: + opt = ("--assertmode=" + mode,) + other_opt + result = testdir.runpytest(*opt) + assert result.ret == 3 + assert "assertion options conflict" in result.stderr.str() + +def test_old_assert_mode(testdir): + testdir.makepyfile(""" + def test_in_old_mode(): + assert "@py_builtins" not in globals() + """) + result = testdir.runpytest("--assertmode=old") + assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" http://bitbucket.org/hpk42/pytest/changeset/e533e6c42e00/ changeset: e533e6c42e00 branches: user: gutworth date: 2011-05-26 23:08:25 summary: add some tracing in the assert plugin affected #: 1 file (675 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 14:34:27 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 16:08:25 2011 -0500 @@ -29,6 +29,12 @@ dest="nomagic", help="DEPRECATED equivalent to --assertmode=off") +class AssertionState: + """State for the assertion plugin.""" + + def __init__(self, config, mode): + self.mode = mode + self.trace = config.trace.root.get("assertion") def pytest_configure(config): global rewrite_asserts @@ -54,6 +60,8 @@ m.setattr(util, '_reprcompare', callbinrepr) if mode != "on": rewrite_asserts = None + config._assertion = AssertionState(config, mode) + config._assertion.trace("configured with mode set to %r" % (mode,)) def _write_pyc(co, source_path): if hasattr(imp, "cache_from_source"): @@ -82,6 +90,7 @@ tree = ast.parse(source) except SyntaxError: # Let this pop up again in the real import. + mod.config._assertstate.trace("failed to parse: %r" % (mod.fspath,)) return rewrite_asserts(tree) try: @@ -89,8 +98,10 @@ except SyntaxError: # It's possible that this error is from some bug in the assertion # rewriting, but I don't know of a fast way to tell. + mod.config._assertstate.trace("failed to compile: %r" % (mod.fspath,)) return mod._pyc = _write_pyc(co, mod.fspath) + mod.config._assertstate.trace("wrote pyc: %r" % (mod._pyc,)) def pytest_pycollect_after_module_import(mod): if rewrite_asserts is None or not hasattr(mod, "_pyc"): @@ -99,7 +110,9 @@ try: mod._pyc.remove() except py.error.ENOENT: - pass + mod.config._assertstate.trace("couldn't find pyc: %r" % (mod._pyc,)) + else: + mod.config._assertstate.trace("removed pyc: %r" % (mod._pyc,)) def warn_about_missing_assertion(): try: http://bitbucket.org/hpk42/pytest/changeset/f404da94abab/ changeset: f404da94abab branches: user: gutworth date: 2011-05-26 23:18:18 summary: correct attribute name affected #: 1 file (4 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 16:08:25 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 16:18:18 2011 -0500 @@ -60,8 +60,8 @@ m.setattr(util, '_reprcompare', callbinrepr) if mode != "on": rewrite_asserts = None - config._assertion = AssertionState(config, mode) - config._assertion.trace("configured with mode set to %r" % (mode,)) + config._assertstate = AssertionState(config, mode) + config._assertstate.trace("configured with mode set to %r" % (mode,)) def _write_pyc(co, source_path): if hasattr(imp, "cache_from_source"): http://bitbucket.org/hpk42/pytest/changeset/5c8335e71705/ changeset: 5c8335e71705 branches: user: gutworth date: 2011-05-26 23:50:04 summary: these tests should cause pytest_configure to be called affected #: 1 file (21 bytes) --- a/testing/test_collection.py Thu May 26 16:18:18 2011 -0500 +++ b/testing/test_collection.py Thu May 26 16:50:04 2011 -0500 @@ -328,7 +328,7 @@ def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) topdir = testdir.tmpdir rcol = Session(config) assert topdir == rcol.fspath @@ -363,7 +363,7 @@ p.basename + "::TestClass::()", normid, ]: - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) rcol = Session(config=config) rcol.perform_collect() items = rcol.items @@ -388,7 +388,7 @@ """ % p.basename) id = p.basename - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -413,7 +413,7 @@ aaa = testdir.mkpydir("aaa") test_aaa = aaa.join("test_aaa.py") p.move(test_aaa) - config = testdir.parseconfig() + config = testdir.parseconfigure() rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -437,7 +437,7 @@ p.move(test_bbb) id = "." - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -455,7 +455,7 @@ def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") - config = testdir.parseconfig() + config = testdir.parseconfigure() rcol = Session(config) rcol.perform_collect() items = rcol.items @@ -476,7 +476,7 @@ pass """) arg = p.basename + ("::TestClass::test_method") - config = testdir.parseconfig(arg) + config = testdir.parseconfigure(arg) rcol = Session(config) rcol.perform_collect() items = rcol.items http://bitbucket.org/hpk42/pytest/changeset/f874c002e245/ changeset: f874c002e245 branches: user: gutworth date: 2011-05-27 00:08:44 summary: correctly initialize and shutdown sessions affected #: 1 file (450 bytes) --- a/_pytest/pytester.py Thu May 26 16:50:04 2011 -0500 +++ b/_pytest/pytester.py Thu May 26 17:08:44 2011 -0500 @@ -6,7 +6,7 @@ import inspect import time from fnmatch import fnmatch -from _pytest.main import Session +from _pytest.main import Session, EXIT_OK from py.builtin import print_ from _pytest.core import HookRelay @@ -292,13 +292,19 @@ assert '::' not in str(arg) p = py.path.local(arg) x = session.fspath.bestrelpath(p) - return session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + return res def getpathnode(self, path): config = self.parseconfig(path) session = Session(config) x = session.fspath.bestrelpath(path) - return session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + return res def genitems(self, colitems): session = colitems[0].session @@ -312,7 +318,9 @@ config = self.parseconfigure(*args) rec = self.getreportrecorder(config) session = Session(config) + config.hook.pytest_sessionstart(session=session) session.perform_collect() + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) return session.items, rec def runitem(self, source): http://bitbucket.org/hpk42/pytest/changeset/83b65dee3ccb/ changeset: 83b65dee3ccb branches: user: gutworth date: 2011-05-27 00:08:56 summary: move _setupstate into session affected #: 3 files (182 bytes) --- a/_pytest/python.py Thu May 26 17:08:44 2011 -0500 +++ b/_pytest/python.py Thu May 26 17:08:56 2011 -0500 @@ -378,7 +378,7 @@ # test generators are seen as collectors but they also # invoke setup/teardown on popular request # (induced by the common "test_*" naming shared with normal tests) - self.config._setupstate.prepare(self) + self.session._setupstate.prepare(self) # see FunctionMixin.setup and test_setupstate_is_preserved_134 self._preservedparent = self.parent.obj l = [] @@ -730,7 +730,7 @@ def _addfinalizer(self, finalizer, scope): colitem = self._getscopeitem(scope) - self.config._setupstate.addfinalizer( + self._pyfuncitem.session._setupstate.addfinalizer( finalizer=finalizer, colitem=colitem) def __repr__(self): --- a/_pytest/runner.py Thu May 26 17:08:44 2011 -0500 +++ b/_pytest/runner.py Thu May 26 17:08:56 2011 -0500 @@ -14,17 +14,15 @@ # # pytest plugin hooks -# XXX move to pytest_sessionstart and fix py.test owns tests -def pytest_configure(config): - config._setupstate = SetupState() +def pytest_sessionstart(session): + session._setupstate = SetupState() def pytest_sessionfinish(session, exitstatus): - if hasattr(session.config, '_setupstate'): - hook = session.config.hook - rep = hook.pytest__teardown_final(session=session) - if rep: - hook.pytest__teardown_final_logerror(session=session, report=rep) - session.exitstatus = 1 + hook = session.config.hook + rep = hook.pytest__teardown_final(session=session) + if rep: + hook.pytest__teardown_final_logerror(session=session, report=rep) + session.exitstatus = 1 class NodeInfo: def __init__(self, location): @@ -46,16 +44,16 @@ return reports def pytest_runtest_setup(item): - item.config._setupstate.prepare(item) + item.session._setupstate.prepare(item) def pytest_runtest_call(item): item.runtest() def pytest_runtest_teardown(item): - item.config._setupstate.teardown_exact(item) + item.session._setupstate.teardown_exact(item) def pytest__teardown_final(session): - call = CallInfo(session.config._setupstate.teardown_all, when="teardown") + call = CallInfo(session._setupstate.teardown_all, when="teardown") if call.excinfo: ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) call.excinfo.traceback = ntraceback.filter() --- a/testing/test_python.py Thu May 26 17:08:44 2011 -0500 +++ b/testing/test_python.py Thu May 26 17:08:56 2011 -0500 @@ -705,11 +705,11 @@ def test_func(something): pass """) req = funcargs.FuncargRequest(item) - req.config._setupstate.prepare(item) # XXX + req._pyfuncitem.session._setupstate.prepare(item) # XXX req._fillfuncargs() # successively check finalization calls teardownlist = item.getparent(pytest.Module).obj.teardownlist - ss = item.config._setupstate + ss = item.session._setupstate assert not teardownlist ss.teardown_exact(item) print(ss.stack) @@ -834,11 +834,11 @@ ret1 = req1.cached_setup(setup, teardown, scope="function") assert l == ['setup'] # artificial call of finalizer - req1.config._setupstate._callfinalizers(item1) + req1._pyfuncitem.session._setupstate._callfinalizers(item1) assert l == ["setup", "teardown"] ret2 = req1.cached_setup(setup, teardown, scope="function") assert l == ["setup", "teardown", "setup"] - req1.config._setupstate._callfinalizers(item1) + req1._pyfuncitem.session._setupstate._callfinalizers(item1) assert l == ["setup", "teardown", "setup", "teardown"] def test_request_cached_setup_two_args(self, testdir): http://bitbucket.org/hpk42/pytest/changeset/4049aa5f3d91/ changeset: 4049aa5f3d91 branches: user: gutworth date: 2011-05-27 00:17:48 summary: a less ugly way to detect if assert rewriting is enabled affected #: 1 file (18 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 17:08:56 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 17:17:48 2011 -0500 @@ -37,7 +37,6 @@ self.trace = config.trace.root.get("assertion") def pytest_configure(config): - global rewrite_asserts warn_about_missing_assertion() mode = config.getvalue("assertmode") if config.getvalue("noassert") or config.getvalue("nomagic"): @@ -58,8 +57,8 @@ m.setattr(py.builtin.builtins, 'AssertionError', reinterpret.AssertionError) m.setattr(util, '_reprcompare', callbinrepr) - if mode != "on": - rewrite_asserts = None + if mode == "on" and rewrite_asserts is None: + mode = "old" config._assertstate = AssertionState(config, mode) config._assertstate.trace("configured with mode set to %r" % (mode,)) @@ -81,7 +80,7 @@ return pyc def pytest_pycollect_before_module_import(mod): - if rewrite_asserts is None: + if mod.config._assertstate.mode != "on": return # Some deep magic: load the source, rewrite the asserts, and write a # fake pyc, so that it'll be loaded when the module is imported. @@ -104,7 +103,7 @@ mod.config._assertstate.trace("wrote pyc: %r" % (mod._pyc,)) def pytest_pycollect_after_module_import(mod): - if rewrite_asserts is None or not hasattr(mod, "_pyc"): + if mod.config._assertstate.mode != "on" or not hasattr(mod, "_pyc"): return # Remove our tweaked pyc to avoid subtle bugs. try: http://bitbucket.org/hpk42/pytest/changeset/7c9a411e05ca/ changeset: 7c9a411e05ca branches: user: gutworth date: 2011-05-27 01:10:49 summary: cause configure hooks to be called affected #: 1 file (3 bytes) --- a/testing/test_collection.py Thu May 26 17:17:48 2011 -0500 +++ b/testing/test_collection.py Thu May 26 18:10:49 2011 -0500 @@ -313,7 +313,7 @@ def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) topdir = testdir.tmpdir rcol = Session(config) assert topdir == rcol.fspath http://bitbucket.org/hpk42/pytest/changeset/f9ef04e6aa19/ changeset: f9ef04e6aa19 branches: user: gutworth date: 2011-05-27 01:11:12 summary: call configure hooks in reparseconfig affected #: 1 file (125 bytes) --- a/_pytest/pytester.py Thu May 26 18:10:49 2011 -0500 +++ b/_pytest/pytester.py Thu May 26 18:11:12 2011 -0500 @@ -390,6 +390,8 @@ c.basetemp = py.path.local.make_numbered_dir(prefix="reparse", keep=0, rootdir=self.tmpdir, lock_timeout=None) c.parse(args) + c.pluginmanager.do_configure(c) + self.request.addfinalizer(lambda: c.pluginmanager.do_unconfigure(c)) return c finally: py.test.config = oldconfig http://bitbucket.org/hpk42/pytest/changeset/435a4915e185/ changeset: 435a4915e185 branches: user: gutworth date: 2011-05-27 01:37:04 summary: do configure hooks here, too affected #: 1 file (3 bytes) --- a/_pytest/pytester.py Thu May 26 18:11:12 2011 -0500 +++ b/_pytest/pytester.py Thu May 26 18:37:04 2011 -0500 @@ -298,7 +298,7 @@ return res def getpathnode(self, path): - config = self.parseconfig(path) + config = self.parseconfigure(path) session = Session(config) x = session.fspath.bestrelpath(path) config.hook.pytest_sessionstart(session=session) http://bitbucket.org/hpk42/pytest/changeset/eeb15722a91c/ changeset: eeb15722a91c branches: user: gutworth date: 2011-05-27 01:56:45 summary: add a hook called when a Module is successfully created affected #: 2 files (189 bytes) --- a/_pytest/hookspec.py Thu May 26 18:37:04 2011 -0500 +++ b/_pytest/hookspec.py Thu May 26 18:56:45 2011 -0500 @@ -104,6 +104,9 @@ """ pytest_pycollect_makemodule.firstresult = True +def pytest_pycollect_onmodule(mod): + """ Called when a module is collected.""" + def pytest_pycollect_before_module_import(mod): """Called before a module is imported.""" --- a/_pytest/python.py Thu May 26 18:37:04 2011 -0500 +++ b/_pytest/python.py Thu May 26 18:56:45 2011 -0500 @@ -60,8 +60,11 @@ break else: return - return parent.ihook.pytest_pycollect_makemodule( + mod = parent.ihook.pytest_pycollect_makemodule( path=path, parent=parent) + if mod is not None: + parent.ihook.pytest_pycollect_onmodule(mod=mod) + return mod def pytest_pycollect_makemodule(path, parent): return Module(path, parent) http://bitbucket.org/hpk42/pytest/changeset/c5181bca2ae7/ changeset: c5181bca2ae7 branches: user: gutworth date: 2011-05-27 01:57:37 summary: add a hook called after the inital fs collection affected #: 2 files (210 bytes) --- a/_pytest/hookspec.py Thu May 26 18:56:45 2011 -0500 +++ b/_pytest/hookspec.py Thu May 26 18:57:37 2011 -0500 @@ -79,6 +79,9 @@ def pytest_collectstart(collector): """ collector starts collecting. """ +def pytest_after_initial_collect(collector): + """ after the initial file system walk before genitems""" + def pytest_itemcollected(item): """ we just collected a test item. """ --- a/_pytest/main.py Thu May 26 18:56:45 2011 -0500 +++ b/_pytest/main.py Thu May 26 18:57:37 2011 -0500 @@ -386,7 +386,10 @@ self._initialparts.append(parts) self._initialpaths.add(parts[0]) self.ihook.pytest_collectstart(collector=self) - rep = self.ihook.pytest_make_collect_report(collector=self) + try: + rep = self.ihook.pytest_make_collect_report(collector=self) + finally: + self.ihook.pytest_after_initial_collect(collector=self) self.ihook.pytest_collectreport(report=rep) self.trace.root.indent -= 1 if self._notfound: http://bitbucket.org/hpk42/pytest/changeset/d0f80a75e27b/ changeset: d0f80a75e27b branches: user: gutworth date: 2011-05-27 01:58:31 summary: expose Session on pytest namespace affected #: 1 file (39 bytes) --- a/_pytest/main.py Thu May 26 18:57:37 2011 -0500 +++ b/_pytest/main.py Thu May 26 18:58:31 2011 -0500 @@ -46,7 +46,8 @@ def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, File=File)) + collect = dict(Item=Item, Collector=Collector, File=File, Session=Session) + return dict(collect=collect) def pytest_configure(config): py.test.config = config # compatibiltiy http://bitbucket.org/hpk42/pytest/changeset/9beec68f4fe3/ changeset: 9beec68f4fe3 branches: user: gutworth date: 2011-05-27 02:09:42 summary: refactor common config/session protocol code for main() functions affected #: 2 files (232 bytes) --- a/_pytest/main.py Thu May 26 18:58:31 2011 -0500 +++ b/_pytest/main.py Thu May 26 19:09:42 2011 -0500 @@ -54,16 +54,14 @@ if config.option.exitfirst: config.option.maxfail = 1 -def pytest_cmdline_main(config): - """ default command line protocol for initialization, session, - running tests and reporting. """ +def wrap_session(config, doit): + """Skeleton command line program""" session = Session(config) session.exitstatus = EXIT_OK try: config.pluginmanager.do_configure(config) config.hook.pytest_sessionstart(session=session) - config.hook.pytest_collection(session=session) - config.hook.pytest_runtestloop(session=session) + doit(config, session) except pytest.UsageError: raise except KeyboardInterrupt: @@ -83,6 +81,15 @@ config.pluginmanager.do_unconfigure(config) return session.exitstatus +def pytest_cmdline_main(config): + return wrap_session(config, _main) + +def _main(config, session): + """ default command line protocol for initialization, session, + running tests and reporting. """ + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + def pytest_collection(session): session.perform_collect() hook = session.config.hook --- a/_pytest/python.py Thu May 26 18:58:31 2011 -0500 +++ b/_pytest/python.py Thu May 26 19:09:42 2011 -0500 @@ -754,8 +754,10 @@ raise self.LookupError(msg) def showfuncargs(config): - from _pytest.main import Session - session = Session(config) + from _pytest.main import wrap_session + return wrap_session(config, _showfuncargs_main) + +def _showfuncargs_main(config, session): session.perform_collect() if session.items: plugins = session.items[0].getplugins() http://bitbucket.org/hpk42/pytest/changeset/533c443f3fd5/ changeset: 533c443f3fd5 branches: user: gutworth date: 2011-05-27 02:43:02 summary: remove after_initial_collect hook affected #: 2 files (210 bytes) --- a/_pytest/hookspec.py Thu May 26 19:09:42 2011 -0500 +++ b/_pytest/hookspec.py Thu May 26 19:43:02 2011 -0500 @@ -79,9 +79,6 @@ def pytest_collectstart(collector): """ collector starts collecting. """ -def pytest_after_initial_collect(collector): - """ after the initial file system walk before genitems""" - def pytest_itemcollected(item): """ we just collected a test item. """ --- a/_pytest/main.py Thu May 26 19:09:42 2011 -0500 +++ b/_pytest/main.py Thu May 26 19:43:02 2011 -0500 @@ -394,10 +394,7 @@ self._initialparts.append(parts) self._initialpaths.add(parts[0]) self.ihook.pytest_collectstart(collector=self) - try: - rep = self.ihook.pytest_make_collect_report(collector=self) - finally: - self.ihook.pytest_after_initial_collect(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) self.ihook.pytest_collectreport(report=rep) self.trace.root.indent -= 1 if self._notfound: http://bitbucket.org/hpk42/pytest/changeset/59992a61e05a/ changeset: 59992a61e05a branches: user: gutworth date: 2011-05-27 02:53:47 summary: stuff contents of pytest_collection hook into perform_collect affected #: 1 file (157 bytes) --- a/_pytest/main.py Thu May 26 19:43:02 2011 -0500 +++ b/_pytest/main.py Thu May 26 19:53:47 2011 -0500 @@ -91,12 +91,7 @@ config.hook.pytest_runtestloop(session=session) def pytest_collection(session): - session.perform_collect() - hook = session.config.hook - hook.pytest_collection_modifyitems(session=session, - config=session.config, items=session.items) - hook.pytest_collection_finish(session=session) - return True + return session.perform_collect() def pytest_runtestloop(session): if session.config.option.collectonly: @@ -382,6 +377,16 @@ return HookProxy(fspath, self.config) def perform_collect(self, args=None, genitems=True): + hook = self.config.hook + try: + items = self._perform_collect(args, genitems) + hook.pytest_collection_modifyitems(session=self, + config=self.config, items=items) + finally: + hook.pytest_collection_finish(session=self) + return items + + def _perform_collect(self, args, genitems): if args is None: args = self.config.args self.trace("perform_collect", self, args) http://bitbucket.org/hpk42/pytest/changeset/8c61493e8a94/ changeset: 8c61493e8a94 branches: user: gutworth date: 2011-05-27 02:57:30 summary: new way to rewrite tests: do it all during fs collection This should allow modules to be rewritten before some other test module loads them. affected #: 1 file (241 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 19:53:47 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 19:57:30 2011 -0500 @@ -62,6 +62,10 @@ config._assertstate = AssertionState(config, mode) config._assertstate.trace("configured with mode set to %r" % (mode,)) +def pytest_collectstart(collector): + if isinstance(collector, pytest.Session): + collector._rewritten_pycs = [] + def _write_pyc(co, source_path): if hasattr(imp, "cache_from_source"): # Handle PEP 3147 pycs. @@ -79,9 +83,9 @@ fp.close() return pyc -def pytest_pycollect_before_module_import(mod): - if mod.config._assertstate.mode != "on": - return +def pytest_pycollect_onmodule(mod): + if mod is None or mod.config._assertstate.mode != "on": + return mod # Some deep magic: load the source, rewrite the asserts, and write a # fake pyc, so that it'll be loaded when the module is imported. source = mod.fspath.read() @@ -90,7 +94,7 @@ except SyntaxError: # Let this pop up again in the real import. mod.config._assertstate.trace("failed to parse: %r" % (mod.fspath,)) - return + return mod rewrite_asserts(tree) try: co = compile(tree, str(mod.fspath), "exec") @@ -98,20 +102,25 @@ # It's possible that this error is from some bug in the assertion # rewriting, but I don't know of a fast way to tell. mod.config._assertstate.trace("failed to compile: %r" % (mod.fspath,)) + return mod + pyc = _write_pyc(co, mod.fspath) + mod.session._rewritten_pycs.append(pyc) + mod.config._assertstate.trace("wrote pyc: %r" % (pyc,)) + return mod + +def pytest_collection_finish(session): + if not hasattr(session, "_rewritten_pycs"): return - mod._pyc = _write_pyc(co, mod.fspath) - mod.config._assertstate.trace("wrote pyc: %r" % (mod._pyc,)) - -def pytest_pycollect_after_module_import(mod): - if mod.config._assertstate.mode != "on" or not hasattr(mod, "_pyc"): - return - # Remove our tweaked pyc to avoid subtle bugs. - try: - mod._pyc.remove() - except py.error.ENOENT: - mod.config._assertstate.trace("couldn't find pyc: %r" % (mod._pyc,)) - else: - mod.config._assertstate.trace("removed pyc: %r" % (mod._pyc,)) + state = session.config._assertstate + # Remove our tweaked pycs to avoid subtle bugs. + for pyc in session._rewritten_pycs: + try: + pyc.remove() + except py.error.ENOENT: + state.trace("couldn't find pyc: %r" % (pyc,)) + else: + state.trace("removed pyc: %r" % (pyc,)) + del session._rewritten_pycs[:] def warn_about_missing_assertion(): try: http://bitbucket.org/hpk42/pytest/changeset/b02cd644217a/ changeset: b02cd644217a branches: user: gutworth date: 2011-05-27 03:00:29 summary: remove module before/after import hooks affected #: 2 files (371 bytes) --- a/_pytest/hookspec.py Thu May 26 19:57:30 2011 -0500 +++ b/_pytest/hookspec.py Thu May 26 20:00:29 2011 -0500 @@ -107,12 +107,6 @@ def pytest_pycollect_onmodule(mod): """ Called when a module is collected.""" -def pytest_pycollect_before_module_import(mod): - """Called before a module is imported.""" - -def pytest_pycollect_after_module_import(mod): - """Called after a module is imported.""" - def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ pytest_pycollect_makeitem.firstresult = True --- a/_pytest/python.py Thu May 26 19:57:30 2011 -0500 +++ b/_pytest/python.py Thu May 26 20:00:29 2011 -0500 @@ -228,13 +228,9 @@ return self._memoizedcall('_obj', self._importtestmodule) def _importtestmodule(self): - self.ihook.pytest_pycollect_before_module_import(mod=self) # we assume we are only called once per module try: - try: - mod = self.fspath.pyimport(ensuresyspath=True) - finally: - self.ihook.pytest_pycollect_after_module_import(mod=self) + mod = self.fspath.pyimport(ensuresyspath=True) except SyntaxError: excinfo = py.code.ExceptionInfo() raise self.CollectError(excinfo.getrepr(style="short")) http://bitbucket.org/hpk42/pytest/changeset/27b17662edcb/ changeset: 27b17662edcb branches: user: gutworth date: 2011-05-27 03:06:11 summary: test that tests get rewritten affected #: 1 file (118 bytes) --- a/testing/test_assertion.py Thu May 26 20:00:29 2011 -0500 +++ b/testing/test_assertion.py Thu May 26 20:06:11 2011 -0500 @@ -120,6 +120,10 @@ expl = ' '.join(callequal('foo', 'bar')) assert 'raised in repr()' not in expl + at pytest.mark.skipif("config._assertstate.mode != 'on'") +def test_rewritten(): + assert "@py_builtins" in globals() + def test_reprcompare_notin(): detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:] assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++'] http://bitbucket.org/hpk42/pytest/changeset/78797081569e/ changeset: 78797081569e branches: user: gutworth date: 2011-05-27 03:33:12 summary: beef up --assertmode help affected #: 1 file (200 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 20:06:11 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 20:33:12 2011 -0500 @@ -22,7 +22,11 @@ group._addoption('--assertmode', action="store", dest="assertmode", choices=("on", "old", "off", "default"), default="default", metavar="on|old|off", - help="Control assertion debugging tools") + help="""control assertion debugging tools. +'off' performs no assertion debugging. +'old' reinterprets the expressions in asserts to glean information. +'new' rewrites the assert statements in test modules to provide sub-expression +results.""") group._addoption('--no-assert', action="store_true", default=False, dest="noassert", help="DEPRECATED equivalent to --assertmode=off") group._addoption('--nomagic', action="store_true", default=False, http://bitbucket.org/hpk42/pytest/changeset/d778f3e1cf8c/ changeset: d778f3e1cf8c branches: user: gutworth date: 2011-05-27 03:59:43 summary: fix help for --assertmode affected #: 1 file (13 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 20:33:12 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 20:59:43 2011 -0500 @@ -25,8 +25,8 @@ help="""control assertion debugging tools. 'off' performs no assertion debugging. 'old' reinterprets the expressions in asserts to glean information. -'new' rewrites the assert statements in test modules to provide sub-expression -results.""") +'on' (the default) rewrites the assert statements in test modules to provide +sub-expression results.""") group._addoption('--no-assert', action="store_true", default=False, dest="noassert", help="DEPRECATED equivalent to --assertmode=off") group._addoption('--nomagic', action="store_true", default=False, http://bitbucket.org/hpk42/pytest/changeset/1f8b526c3e60/ changeset: 1f8b526c3e60 branches: user: gutworth date: 2011-05-27 04:08:55 summary: doc updates for new assertion debugging affected #: 2 files (1.5 KB) --- a/doc/assert.txt Thu May 26 20:59:43 2011 -0500 +++ b/doc/assert.txt Thu May 26 21:08:55 2011 -0500 @@ -39,23 +39,6 @@ test_assert1.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= -Reporting details about the failing assertion is achieved by re-evaluating -the assert expression and recording the intermediate values. - -Note: If evaluating the assert expression has side effects you may get a -warning that the intermediate values could not be determined safely. A -common example of this issue is an assertion which reads from a file:: - - assert f.read() != '...' - -If this assertion fails then the re-evaluation will probably succeed! -This is because ``f.read()`` will return an empty string when it is -called the second time during the re-evaluation. However, it is -easy to rewrite the assertion and avoid any trouble:: - - content = f.read() - assert content != '...' - assertions about expected exceptions ------------------------------------------ @@ -137,6 +120,46 @@ See the :ref:`reporting demo ` for many more examples. + +Assertion debugging details +--------------------------- + +Reporting details about the failing assertion is achieved either by rewriting +assert statements before they are run or re-evaluating the assert expression and +recording the intermediate values. Which technique is used depends on the +location of the assert, py.test's configuration, and Python version being used +to run py.test. + +By default, if the Python version is greater than or equal to 2.6, py.test +rewrites assert statements in test modules. Rewritten assert statements put +debugging information into the assertion failure message. Note py.test only +rewrites test modules directly discovered by its test collection process, so +asserts in supporting modules will not be rewritten. + +If an assert statement has not been rewritten or the Python version is less than +2.6, py.test falls back on assert reinterpretation. In assert reinterpretation, +py.test walks the frame of the function containing the assert statement to +discover sub-expression results of the failing assert statement. You can force +py.test to always use assertion reinterpretation by passing the +``--assertmode=old`` option. + +Assert reinterpretation has a caveat not present with assert rewriting: If +evaluating the assert expression has side effects you may get a warning that the +intermediate values could not be determined safely. A common example of this +issue is an assertion which reads from a file:: + + assert f.read() != '...' + +If this assertion fails then the re-evaluation will probably succeed! +This is because ``f.read()`` will return an empty string when it is +called the second time during the re-evaluation. However, it is +easy to rewrite the assertion and avoid any trouble:: + + content = f.read() + assert content != '...' + +All assert debugging can be turned off by passing ``--assertmode=off``. + .. Defining your own comparison ---------------------------------------------- --- a/doc/faq.txt Thu May 26 20:59:43 2011 -0500 +++ b/doc/faq.txt Thu May 26 21:08:55 2011 -0500 @@ -47,13 +47,17 @@ ``py.test`` still uses many metaprogramming techniques and reading its source is thus likely not something for Python beginners. -A second "magic" issue arguably the assert statement re-intepreation: -When an ``assert`` statement fails, py.test re-interprets the expression -to show intermediate values if a test fails. If your expression -has side effects (better to avoid them anyway!) the intermediate values -may not be the same, obfuscating the initial error (this is also -explained at the command line if it happens). -``py.test --no-assert`` turns off assert re-interpretation. +A second "magic" issue arguably the assert statement debugging feature. When +loading test modules py.test rewrites the source code of assert statements. When +a rewritten assert statement fails, its error message has more information than +the original. py.test has a second assert debugging technique. When an +``assert`` statement that was missed by the rewriter fails, py.test +re-interprets the expression to show intermediate values if a test fails. This +second technique suffers from caveat that the rewriting does not: If your +expression has side effects (better to avoid them anyway!) the intermediate +values may not be the same, confusing the reinterpreter and obfuscating the +initial error (this is also explained at the command line if it happens). +You can turn off all assertion debugging with ``py.test --assertmode=off``. .. _`py namespaces`: index.html .. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py http://bitbucket.org/hpk42/pytest/changeset/74701799b434/ changeset: 74701799b434 branches: user: gutworth date: 2011-05-27 04:15:40 summary: write an explicit raise if the assertion fails affected #: 1 file (242 bytes) --- a/_pytest/assertion/rewrite.py Thu May 26 21:08:55 2011 -0500 +++ b/_pytest/assertion/rewrite.py Thu May 26 21:15:40 2011 -0500 @@ -3,6 +3,7 @@ import ast import collections import itertools +import sys import py from _pytest.assertion import util @@ -212,7 +213,13 @@ template = ast.Str(explanation) msg = self.pop_format_context(template) fmt = self.helper("format_explanation", msg) - body.append(ast.Assert(top_condition, fmt)) + err_name = ast.Name("AssertionError", ast.Load()) + exc = ast.Call(err_name, [fmt], [], None, None) + if sys.version_info[0] >= 3: + raise_ = ast.Raise(exc, None) + else: + raise_ = ast.Raise(exc, None, None) + body.append(raise_) # Delete temporary variables. names = [ast.Name(name, ast.Del()) for name in self.variables] if names: http://bitbucket.org/hpk42/pytest/changeset/7d0e6cd44a90/ changeset: 7d0e6cd44a90 branches: user: gutworth date: 2011-05-27 06:13:39 summary: versionadded and versionchanged for asserts affected #: 1 file (201 bytes) --- a/doc/assert.txt Thu May 26 21:15:40 2011 -0500 +++ b/doc/assert.txt Thu May 26 23:13:39 2011 -0500 @@ -160,6 +160,15 @@ All assert debugging can be turned off by passing ``--assertmode=off``. +.. versionadded:: 2.1 + + Add assert rewriting as an alternate debugging technique. + +.. versionchanged:: 2.1 + + Introduce the ``--assertmode`` option. Deprecate ``--no-assert`` and + ``--nomagic``. + .. Defining your own comparison ---------------------------------------------- http://bitbucket.org/hpk42/pytest/changeset/db572fae751b/ changeset: db572fae751b branches: user: gutworth date: 2011-05-27 06:15:33 summary: can use non-underscored addoption affected #: 1 file (40 bytes) --- a/_pytest/assertion/__init__.py Thu May 26 23:13:39 2011 -0500 +++ b/_pytest/assertion/__init__.py Thu May 26 23:15:33 2011 -0500 @@ -19,19 +19,18 @@ def pytest_addoption(parser): group = parser.getgroup("debugconfig") - group._addoption('--assertmode', action="store", dest="assertmode", - choices=("on", "old", "off", "default"), default="default", - metavar="on|old|off", - help="""control assertion debugging tools. + group.addoption('--assertmode', action="store", dest="assertmode", + choices=("on", "old", "off", "default"), default="default", + metavar="on|old|off", + help="""control assertion debugging tools. 'off' performs no assertion debugging. 'old' reinterprets the expressions in asserts to glean information. 'on' (the default) rewrites the assert statements in test modules to provide sub-expression results.""") - group._addoption('--no-assert', action="store_true", default=False, + group.addoption('--no-assert', action="store_true", default=False, dest="noassert", help="DEPRECATED equivalent to --assertmode=off") - group._addoption('--nomagic', action="store_true", default=False, - dest="nomagic", - help="DEPRECATED equivalent to --assertmode=off") + group.addoption('--nomagic', action="store_true", default=False, + dest="nomagic", help="DEPRECATED equivalent to --assertmode=off") class AssertionState: """State for the assertion plugin.""" http://bitbucket.org/hpk42/pytest/changeset/03c91c41515c/ changeset: 03c91c41515c branches: user: gutworth date: 2011-05-27 19:30:27 summary: a few more sentences affected #: 1 file (367 bytes) --- a/doc/assert.txt Thu May 26 23:15:33 2011 -0500 +++ b/doc/assert.txt Fri May 27 12:30:27 2011 -0500 @@ -18,8 +18,8 @@ def test_function(): assert f() == 4 -to assert that your object returns a certain value. If this -assertion fails you will see the value of ``x``:: +to assert that your function returns a certain value. If this assertion fails +you will see the value of ``x``:: $ py.test test_assert1.py =========================== test session starts ============================ @@ -39,6 +39,13 @@ test_assert1.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= +py.test has support for showing the values of the most common subexpressions +including calls, attributes, comparisons, and binary and unary operators. This +allows you to use the idiomatic python constructs without boilerplate code while +not losing debugging information. + +See :ref:`assert-details` for more information on assertion debugging. + assertions about expected exceptions ------------------------------------------ @@ -121,6 +128,8 @@ See the :ref:`reporting demo ` for many more examples. +.. _assert-details: + Assertion debugging details --------------------------- http://bitbucket.org/hpk42/pytest/changeset/a33bd828ad09/ changeset: a33bd828ad09 branches: user: gutworth date: 2011-05-28 17:02:51 summary: bump pylib required affected #: 1 file (1 byte) --- a/setup.py Fri May 27 12:30:27 2011 -0500 +++ b/setup.py Sat May 28 10:02:51 2011 -0500 @@ -29,7 +29,7 @@ author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), - install_requires=['py>1.4.1'], + install_requires=['py>1.4.3'], classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', @@ -67,4 +67,4 @@ return {'console_scripts': l} if __name__ == '__main__': - main() \ No newline at end of file + main() http://bitbucket.org/hpk42/pytest/changeset/02310412058b/ changeset: 02310412058b branches: user: gutworth date: 2011-05-28 23:01:02 summary: s/debugging/introspection/ affected #: 1 file (28 bytes) --- a/doc/assert.txt Sat May 28 10:02:51 2011 -0500 +++ b/doc/assert.txt Sat May 28 16:01:02 2011 -0500 @@ -42,9 +42,9 @@ py.test has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary operators. This allows you to use the idiomatic python constructs without boilerplate code while -not losing debugging information. +not losing introspection information. -See :ref:`assert-details` for more information on assertion debugging. +See :ref:`assert-details` for more information on assertion introspection. assertions about expected exceptions @@ -130,8 +130,8 @@ .. _assert-details: -Assertion debugging details ---------------------------- +Assertion introspection details +------------------------------- Reporting details about the failing assertion is achieved either by rewriting assert statements before they are run or re-evaluating the assert expression and @@ -141,7 +141,7 @@ By default, if the Python version is greater than or equal to 2.6, py.test rewrites assert statements in test modules. Rewritten assert statements put -debugging information into the assertion failure message. Note py.test only +introspection information into the assertion failure message. Note py.test only rewrites test modules directly discovered by its test collection process, so asserts in supporting modules will not be rewritten. @@ -167,11 +167,11 @@ content = f.read() assert content != '...' -All assert debugging can be turned off by passing ``--assertmode=off``. +All assert introspeciton can be turned off by passing ``--assertmode=off``. .. versionadded:: 2.1 - Add assert rewriting as an alternate debugging technique. + Add assert rewriting as an alternate introspection technique. .. versionchanged:: 2.1 http://bitbucket.org/hpk42/pytest/changeset/3f4b3a45470b/ changeset: 3f4b3a45470b branches: user: gutworth date: 2011-05-28 23:04:36 summary: typo affected #: 1 file (0 bytes) --- a/doc/assert.txt Sat May 28 16:01:02 2011 -0500 +++ b/doc/assert.txt Sat May 28 16:04:36 2011 -0500 @@ -167,7 +167,7 @@ content = f.read() assert content != '...' -All assert introspeciton can be turned off by passing ``--assertmode=off``. +All assert introspection can be turned off by passing ``--assertmode=off``. .. versionadded:: 2.1 http://bitbucket.org/hpk42/pytest/changeset/7506b3ba8be3/ changeset: 7506b3ba8be3 branches: user: gutworth date: 2011-05-29 01:47:16 summary: return to the old scheme of rewriting test modules from _importtestmodule affected #: 3 files (513 bytes) --- a/_pytest/assertion/__init__.py Sat May 28 16:04:36 2011 -0500 +++ b/_pytest/assertion/__init__.py Sat May 28 18:47:16 2011 -0500 @@ -65,10 +65,6 @@ config._assertstate = AssertionState(config, mode) config._assertstate.trace("configured with mode set to %r" % (mode,)) -def pytest_collectstart(collector): - if isinstance(collector, pytest.Session): - collector._rewritten_pycs = [] - def _write_pyc(co, source_path): if hasattr(imp, "cache_from_source"): # Handle PEP 3147 pycs. @@ -86,9 +82,9 @@ fp.close() return pyc -def pytest_pycollect_onmodule(mod): - if mod is None or mod.config._assertstate.mode != "on": - return mod +def before_module_import(mod): + if mod.config._assertstate.mode != "on": + return # Some deep magic: load the source, rewrite the asserts, and write a # fake pyc, so that it'll be loaded when the module is imported. source = mod.fspath.read() @@ -97,7 +93,7 @@ except SyntaxError: # Let this pop up again in the real import. mod.config._assertstate.trace("failed to parse: %r" % (mod.fspath,)) - return mod + return rewrite_asserts(tree) try: co = compile(tree, str(mod.fspath), "exec") @@ -105,25 +101,20 @@ # It's possible that this error is from some bug in the assertion # rewriting, but I don't know of a fast way to tell. mod.config._assertstate.trace("failed to compile: %r" % (mod.fspath,)) - return mod - pyc = _write_pyc(co, mod.fspath) - mod.session._rewritten_pycs.append(pyc) - mod.config._assertstate.trace("wrote pyc: %r" % (pyc,)) - return mod + return + mod._pyc = _write_pyc(co, mod.fspath) + mod.config._assertstate.trace("wrote pyc: %r" % (mod._pyc,)) -def pytest_collection_finish(session): - if not hasattr(session, "_rewritten_pycs"): +def after_module_import(mod): + if not hasattr(mod, "_pyc"): return - state = session.config._assertstate - # Remove our tweaked pycs to avoid subtle bugs. - for pyc in session._rewritten_pycs: - try: - pyc.remove() - except py.error.ENOENT: - state.trace("couldn't find pyc: %r" % (pyc,)) - else: - state.trace("removed pyc: %r" % (pyc,)) - del session._rewritten_pycs[:] + state = mod.config._assertstate + try: + mod._pyc.remove() + except py.error.ENOENT: + state.trace("couldn't find pyc: %r" % (mod._pyc,)) + else: + state.trace("removed pyc: %r" % (mod._pyc,)) def warn_about_missing_assertion(): try: --- a/_pytest/hookspec.py Sat May 28 16:04:36 2011 -0500 +++ b/_pytest/hookspec.py Sat May 28 18:47:16 2011 -0500 @@ -104,9 +104,6 @@ """ pytest_pycollect_makemodule.firstresult = True -def pytest_pycollect_onmodule(mod): - """ Called when a module is collected.""" - def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ pytest_pycollect_makeitem.firstresult = True --- a/_pytest/python.py Sat May 28 16:04:36 2011 -0500 +++ b/_pytest/python.py Sat May 28 18:47:16 2011 -0500 @@ -4,6 +4,7 @@ import sys import pytest from py._code.code import TerminalRepr +from _pytest import assertion import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -60,11 +61,8 @@ break else: return - mod = parent.ihook.pytest_pycollect_makemodule( + return parent.ihook.pytest_pycollect_makemodule( path=path, parent=parent) - if mod is not None: - parent.ihook.pytest_pycollect_onmodule(mod=mod) - return mod def pytest_pycollect_makemodule(path, parent): return Module(path, parent) @@ -229,8 +227,12 @@ def _importtestmodule(self): # we assume we are only called once per module + assertion.before_module_import(self) try: - mod = self.fspath.pyimport(ensuresyspath=True) + try: + mod = self.fspath.pyimport(ensuresyspath=True) + finally: + assertion.after_module_import(self) except SyntaxError: excinfo = py.code.ExceptionInfo() raise self.CollectError(excinfo.getrepr(style="short")) http://bitbucket.org/hpk42/pytest/changeset/95a95a68748b/ changeset: 95a95a68748b branches: user: gutworth date: 2011-05-29 02:00:47 summary: describe how assert rewriting interacts with cross test imports affected #: 1 file (451 bytes) --- a/doc/assert.txt Sat May 28 18:47:16 2011 -0500 +++ b/doc/assert.txt Sat May 28 19:00:47 2011 -0500 @@ -145,6 +145,15 @@ rewrites test modules directly discovered by its test collection process, so asserts in supporting modules will not be rewritten. +.. note:: + + py.test rewrites test modules as it collects tests from them. It does this by + writing a new pyc file which Python loads when the test module is + imported. If the module has already been loaded (it is in sys.modules), + though, Python will not load the rewritten module. This means if a test + module imports another test module which has not already been rewritten, then + py.test will not be able to rewrite the second module. + If an assert statement has not been rewritten or the Python version is less than 2.6, py.test falls back on assert reinterpretation. In assert reinterpretation, py.test walks the frame of the function containing the assert statement to http://bitbucket.org/hpk42/pytest/changeset/4502ab0c1860/ changeset: 4502ab0c1860 branches: user: hpk42 date: 2011-05-31 14:11:53 summary: merge Benjamin's assertion-rewrite branch: all assertion related code is now part of py.test core distribution - the builtin assertion plugin to be precise. See doc/assert.txt for details on how what has been improved. affected #: 23 files (75.7 KB) --- a/CHANGELOG Sun May 29 09:21:48 2011 +0200 +++ b/CHANGELOG Tue May 31 14:11:53 2011 +0200 @@ -1,6 +1,10 @@ -Changes between 2.0.3 and DEV +Changes between 2.0.3 and 2.1.0.DEV ---------------------------------------------- +- merge Benjamin's assertionrewrite branch: now assertions + for test modules on python 2.6 and above are done by rewriting + the AST and saving the pyc file before the test module is imported. + see doc/assert.txt for more info. - fix issue43: improve doctests with better traceback reporting on unexpected exceptions - fix issue47: timing output in junitxml for test cases is now correct --- a/_pytest/__init__.py Sun May 29 09:21:48 2011 +0200 +++ b/_pytest/__init__.py Tue May 31 14:11:53 2011 +0200 @@ -1,2 +1,2 @@ # -__version__ = '2.1.0.dev1' +__version__ = '2.1.0.dev2' --- a/_pytest/assertion.py Sun May 29 09:21:48 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,177 +0,0 @@ -""" -support for presented detailed information in failing assertions. -""" -import py -import sys -from _pytest.monkeypatch import monkeypatch - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group._addoption('--no-assert', action="store_true", default=False, - dest="noassert", - help="disable python assert expression reinterpretation."), - -def pytest_configure(config): - # The _reprcompare attribute on the py.code module is used by - # py._code._assertionnew to detect this plugin was loaded and in - # turn call the hooks defined here as part of the - # DebugInterpreter. - m = monkeypatch() - config._cleanup.append(m.undo) - warn_about_missing_assertion() - if not config.getvalue("noassert") and not config.getvalue("nomagic"): - def callbinrepr(op, left, right): - hook_result = config.hook.pytest_assertrepr_compare( - config=config, op=op, left=left, right=right) - for new_expl in hook_result: - if new_expl: - return '\n~'.join(new_expl) - m.setattr(py.builtin.builtins, - 'AssertionError', py.code._AssertionError) - m.setattr(py.code, '_reprcompare', callbinrepr) - -def warn_about_missing_assertion(): - try: - assert False - except AssertionError: - pass - else: - sys.stderr.write("WARNING: failing tests may report as passing because " - "assertions are turned off! (are you using python -O?)\n") - -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - -def pytest_assertrepr_compare(op, left, right): - """return specialised explanations for some operators/operands""" - width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = py.io.saferepr(left, maxsize=int(width/2)) - right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) - summary = '%s %s %s' % (left_repr, op, right_repr) - - issequence = lambda x: isinstance(x, (list, tuple)) - istext = lambda x: isinstance(x, basestring) - isdict = lambda x: isinstance(x, dict) - isset = lambda x: isinstance(x, set) - - explanation = None - try: - if op == '==': - if istext(left) and istext(right): - explanation = _diff_text(left, right) - elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right) - elif isdict(left) and isdict(right): - explanation = _diff_text(py.std.pprint.pformat(left), - py.std.pprint.pformat(right)) - elif op == 'not in': - if istext(left) and istext(right): - explanation = _notin_text(left, right) - except py.builtin._sysex: - raise - except: - excinfo = py.code.ExceptionInfo() - explanation = ['(pytest_assertion plugin: representation of ' - 'details failed. Probably an object has a faulty __repr__.)', - str(excinfo) - ] - - - if not explanation: - return None - - # Don't include pageloads of data, should be configurable - if len(''.join(explanation)) > 80*8: - explanation = ['Detailed information too verbose, truncated'] - - return [summary] + explanation - - -def _diff_text(left, right): - """Return the explanation for the diff between text - - This will skip leading and trailing characters which are - identical to keep the diff minimal. - """ - explanation = [] - i = 0 # just in case left or right has zero length - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - break - if i > 42: - i -= 10 # Provide some context - explanation = ['Skipping %s identical ' - 'leading characters in diff' % i] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += ['Skipping %s identical ' - 'trailing characters in diff' % i] - left = left[:-i] - right = right[:-i] - explanation += [line.strip('\n') - for line in py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] - return explanation - - -def _compare_eq_sequence(left, right): - explanation = [] - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (i, left[i], right[i])] - break - if len(left) > len(right): - explanation += ['Left contains more items, ' - 'first extra item: %s' % py.io.saferepr(left[len(right)],)] - elif len(left) < len(right): - explanation += ['Right contains more items, ' - 'first extra item: %s' % py.io.saferepr(right[len(left)],)] - return explanation # + _diff_text(py.std.pprint.pformat(left), - # py.std.pprint.pformat(right)) - - -def _compare_eq_set(left, right): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append('Extra items in the left set:') - for item in diff_left: - explanation.append(py.io.saferepr(item)) - if diff_right: - explanation.append('Extra items in the right set:') - for item in diff_right: - explanation.append(py.io.saferepr(item)) - return explanation - - -def _notin_text(term, text): - index = text.find(term) - head = text[:index] - tail = text[index+len(term):] - correct_text = head + tail - diff = _diff_text(correct_text, text) - newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] - for line in diff: - if line.startswith('Skipping'): - continue - if line.startswith('- '): - continue - if line.startswith('+ '): - newdiff.append(' ' + line[2:]) - else: - newdiff.append(line) - return newdiff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/__init__.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,128 @@ +""" +support for presenting detailed information in failing assertions. +""" +import py +import imp +import marshal +import struct +import sys +import pytest +from _pytest.monkeypatch import monkeypatch +from _pytest.assertion import reinterpret, util + +try: + from _pytest.assertion.rewrite import rewrite_asserts +except ImportError: + rewrite_asserts = None +else: + import ast + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group.addoption('--assertmode', action="store", dest="assertmode", + choices=("on", "old", "off", "default"), default="default", + metavar="on|old|off", + help="""control assertion debugging tools. +'off' performs no assertion debugging. +'old' reinterprets the expressions in asserts to glean information. +'on' (the default) rewrites the assert statements in test modules to provide +sub-expression results.""") + group.addoption('--no-assert', action="store_true", default=False, + dest="noassert", help="DEPRECATED equivalent to --assertmode=off") + group.addoption('--nomagic', action="store_true", default=False, + dest="nomagic", help="DEPRECATED equivalent to --assertmode=off") + +class AssertionState: + """State for the assertion plugin.""" + + def __init__(self, config, mode): + self.mode = mode + self.trace = config.trace.root.get("assertion") + +def pytest_configure(config): + warn_about_missing_assertion() + mode = config.getvalue("assertmode") + if config.getvalue("noassert") or config.getvalue("nomagic"): + if mode not in ("off", "default"): + raise pytest.UsageError("assertion options conflict") + mode = "off" + elif mode == "default": + mode = "on" + if mode != "off": + def callbinrepr(op, left, right): + hook_result = config.hook.pytest_assertrepr_compare( + config=config, op=op, left=left, right=right) + for new_expl in hook_result: + if new_expl: + return '\n~'.join(new_expl) + m = monkeypatch() + config._cleanup.append(m.undo) + m.setattr(py.builtin.builtins, 'AssertionError', + reinterpret.AssertionError) + m.setattr(util, '_reprcompare', callbinrepr) + if mode == "on" and rewrite_asserts is None: + mode = "old" + config._assertstate = AssertionState(config, mode) + config._assertstate.trace("configured with mode set to %r" % (mode,)) + +def _write_pyc(co, source_path): + if hasattr(imp, "cache_from_source"): + # Handle PEP 3147 pycs. + pyc = py.path(imp.cache_from_source(source_math)) + pyc.dirname.ensure(dir=True) + else: + pyc = source_path + "c" + mtime = int(source_path.mtime()) + fp = pyc.open("wb") + try: + fp.write(imp.get_magic()) + fp.write(struct.pack(">", + 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 + + 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) + 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 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/oldinterpret.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,556 @@ +import py +import sys, inspect +from compiler import parse, ast, pycodegen +from _pytest.assertion.util import format_explanation +from _pytest.assertion.reinterpret import 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(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): + 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, py.std.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: + 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): + 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())") --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/reinterpret.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,48 @@ +import sys +import py + +BuiltinAssertionError = py.builtin.builtins.AssertionError + +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 = py.code.Frame(sys._getframe(1)) + try: + source = f.code.fullsource + if source is not None: + try: + source = source.getstatement(f.lineno, assertion=True) + except IndexError: + source = None + else: + source = str(source.deindent()).strip() + except py.error.ENOENT: + source = None + # this can also occur during reinterpretation, when the + # co_filename is set to "". + if source: + self.msg = reinterpret(source, f, should_fail=True) + else: + self.msg = "" + if not self.args: + self.args = (self.msg,) + +if sys.version_info > (3, 0): + AssertionError.__module__ = "builtins" + reinterpret_old = "old reinterpretation not available for py3" +else: + from _pytest.assertion.oldinterpret import interpret as reinterpret_old +if sys.version_info >= (2, 6) or (sys.platform.startswith("java")): + from _pytest.assertion.newinterpret import interpret as reinterpret +else: + reinterpret = reinterpret_old + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/rewrite.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,340 @@ +"""Rewrite assertion AST to produce nice error messages""" + +import ast +import collections +import itertools +import sys + +import py +from _pytest.assertion import util + + +def rewrite_asserts(mod): + """Rewrite the assert statements in mod.""" + AssertionRewriter().run(mod) + + +_saferepr = py.io.saferepr +from _pytest.assertion.util import format_explanation as _format_explanation + +def _format_boolop(operands, explanations, is_or): + show_explanations = [] + for operand, expl in zip(operands, explanations): + show_explanations.append(expl) + if operand == is_or: + break + return "(" + (is_or and " or " or " and ").join(show_explanations) + ")" + +def _call_reprcompare(ops, results, expls, each_obj): + for i, res, expl in zip(range(len(ops)), results, expls): + try: + done = not res + except Exception: + done = True + if done: + break + if util._reprcompare is not None: + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if custom is not None: + return custom + return expl + + +unary_map = { + ast.Not : "not %s", + ast.Invert : "~%s", + ast.USub : "-%s", + ast.UAdd : "+%s" +} + +binop_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" +} + + +def set_location(node, lineno, col_offset): + """Set node location information recursively.""" + def _fix(node, lineno, col_offset): + if "lineno" in node._attributes: + node.lineno = lineno + if "col_offset" in node._attributes: + node.col_offset = col_offset + for child in ast.iter_child_nodes(node): + _fix(child, lineno, col_offset) + _fix(node, lineno, col_offset) + return node + + +class AssertionRewriter(ast.NodeVisitor): + + def run(self, mod): + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return + # Insert some special imports at the top of the module but after any + # docstrings and __future__ imports. + aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), + ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] + expect_docstring = True + pos = 0 + lineno = 0 + for item in mod.body: + if (expect_docstring and isinstance(item, ast.Expr) and + isinstance(item.value, ast.Str)): + doc = item.value.s + if "PYTEST_DONT_REWRITE" in doc: + # The module has disabled assertion rewriting. + return + lineno += len(doc) - 1 + expect_docstring = False + elif (not isinstance(item, ast.ImportFrom) or item.level > 0 and + item.identifier != "__future__"): + lineno = item.lineno + break + pos += 1 + imports = [ast.Import([alias], lineno=lineno, col_offset=0) + for alias in aliases] + mod.body[pos:pos] = imports + # Collect asserts. + nodes = collections.deque([mod]) + while nodes: + node = nodes.popleft() + for name, field in ast.iter_fields(node): + if isinstance(field, list): + new = [] + for i, child in enumerate(field): + if isinstance(child, ast.Assert): + # Transform assert. + new.extend(self.visit(child)) + else: + new.append(child) + if isinstance(child, ast.AST): + nodes.append(child) + setattr(node, name, new) + elif (isinstance(field, ast.AST) and + # Don't recurse into expressions as they can't contain + # asserts. + not isinstance(field, ast.expr)): + nodes.append(field) + + def variable(self): + """Get a new variable.""" + # Use a character invalid in python identifiers to avoid clashing. + name = "@py_assert" + str(next(self.variable_counter)) + self.variables.add(name) + return name + + def assign(self, expr): + """Give *expr* a name.""" + name = self.variable() + self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) + return ast.Name(name, ast.Load()) + + def display(self, expr): + """Call py.io.saferepr on the expression.""" + return self.helper("saferepr", expr) + + def helper(self, name, *args): + """Call a helper in this module.""" + py_name = ast.Name("@pytest_ar", ast.Load()) + attr = ast.Attribute(py_name, "_" + name, ast.Load()) + return ast.Call(attr, list(args), [], None, None) + + def builtin(self, name): + """Return the builtin called *name*.""" + builtin_name = ast.Name("@py_builtins", ast.Load()) + return ast.Attribute(builtin_name, name, ast.Load()) + + def explanation_param(self, expr): + specifier = "py" + str(next(self.variable_counter)) + self.explanation_specifiers[specifier] = expr + return "%(" + specifier + ")s" + + def push_format_context(self): + self.explanation_specifiers = {} + self.stack.append(self.explanation_specifiers) + + def pop_format_context(self, expl_expr): + current = self.stack.pop() + if self.stack: + self.explanation_specifiers = self.stack[-1] + keys = [ast.Str(key) for key in current.keys()] + format_dict = ast.Dict(keys, list(current.values())) + form = ast.BinOp(expl_expr, ast.Mod(), format_dict) + name = "@py_format" + str(next(self.variable_counter)) + self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) + return ast.Name(name, ast.Load()) + + def generic_visit(self, node): + """Handle expressions we don't have custom code for.""" + assert isinstance(node, ast.expr) + res = self.assign(node) + return res, self.explanation_param(self.display(res)) + + def visit_Assert(self, assert_): + if assert_.msg: + # There's already a message. Don't mess with it. + return [assert_] + self.statements = [] + self.variables = set() + self.variable_counter = itertools.count() + self.stack = [] + self.on_failure = [] + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) + # Create failure message. + body = self.on_failure + negation = ast.UnaryOp(ast.Not(), top_condition) + self.statements.append(ast.If(negation, body, [])) + explanation = "assert " + explanation + template = ast.Str(explanation) + msg = self.pop_format_context(template) + fmt = self.helper("format_explanation", msg) + err_name = ast.Name("AssertionError", ast.Load()) + exc = ast.Call(err_name, [fmt], [], None, None) + if sys.version_info[0] >= 3: + raise_ = ast.Raise(exc, None) + else: + raise_ = ast.Raise(exc, None, None) + body.append(raise_) + # Delete temporary variables. + names = [ast.Name(name, ast.Del()) for name in self.variables] + if names: + delete = ast.Delete(names) + self.statements.append(delete) + # Fix line numbers. + for stmt in self.statements: + set_location(stmt, assert_.lineno, assert_.col_offset) + return self.statements + + def visit_Name(self, name): + # Check if the name is local or not. + locs = ast.Call(self.builtin("locals"), [], [], None, None) + globs = ast.Call(self.builtin("globals"), [], [], None, None) + ops = [ast.In(), ast.IsNot()] + test = ast.Compare(ast.Str(name.id), ops, [locs, globs]) + expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) + return name, self.explanation_param(expr) + + def visit_BoolOp(self, boolop): + operands = [] + explanations = [] + self.push_format_context() + for operand in boolop.values: + res, explanation = self.visit(operand) + operands.append(res) + explanations.append(explanation) + expls = ast.Tuple([ast.Str(expl) for expl in explanations], ast.Load()) + is_or = ast.Num(isinstance(boolop.op, ast.Or)) + expl_template = self.helper("format_boolop", + ast.Tuple(operands, ast.Load()), expls, + is_or) + expl = self.pop_format_context(expl_template) + res = self.assign(ast.BoolOp(boolop.op, operands)) + return res, self.explanation_param(expl) + + def visit_UnaryOp(self, unary): + pattern = unary_map[unary.op.__class__] + operand_res, operand_expl = self.visit(unary.operand) + res = self.assign(ast.UnaryOp(unary.op, operand_res)) + return res, pattern % (operand_expl,) + + def visit_BinOp(self, binop): + symbol = binop_map[binop.op.__class__] + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) + explanation = "(%s %s %s)" % (left_expl, symbol, right_expl) + res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) + return res, explanation + + def visit_Call(self, call): + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + new_star = new_kwarg = None + for arg in call.args: + res, expl = self.visit(arg) + new_args.append(res) + arg_expls.append(expl) + for keyword in call.keywords: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + arg_expls.append(keyword.arg + "=" + expl) + if call.starargs: + new_star, expl = self.visit(call.starargs) + arg_expls.append("*" + expl) + if call.kwargs: + new_kwarg, expl = self.visit(call.kwarg) + arg_expls.append("**" + expl) + expl = "%s(%s)" % (func_expl, ', '.join(arg_expls)) + new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) + return res, outer_expl + + def visit_Attribute(self, attr): + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + value, value_expl = self.visit(attr.value) + res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) + res_expl = self.explanation_param(self.display(res)) + pat = "%s\n{%s = %s.%s\n}" + expl = pat % (res_expl, res_expl, value_expl, attr.attr) + return res, expl + + def visit_Compare(self, comp): + self.push_format_context() + left_res, left_expl = self.visit(comp.left) + res_variables = [self.variable() for i in range(len(comp.ops))] + load_names = [ast.Name(v, ast.Load()) for v in res_variables] + store_names = [ast.Name(v, ast.Store()) for v in res_variables] + it = zip(range(len(comp.ops)), comp.ops, comp.comparators) + expls = [] + syms = [] + results = [left_res] + for i, op, next_operand in it: + next_res, next_expl = self.visit(next_operand) + results.append(next_res) + sym = binop_map[op.__class__] + syms.append(ast.Str(sym)) + expl = "%s %s %s" % (left_expl, sym, next_expl) + expls.append(ast.Str(expl)) + res_expr = ast.Compare(left_res, [op], [next_res]) + self.statements.append(ast.Assign([store_names[i]], res_expr)) + left_res, left_expl = next_res, next_expl + # Use py.code._reprcompare if that's available. + expl_call = self.helper("call_reprcompare", + ast.Tuple(syms, ast.Load()), + ast.Tuple(load_names, ast.Load()), + ast.Tuple(expls, ast.Load()), + ast.Tuple(results, ast.Load())) + if len(comp.ops) > 1: + res = ast.BoolOp(ast.And(), load_names) + else: + res = load_names[0] + return res, self.explanation_param(self.pop_format_context(expl_call)) --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/_pytest/assertion/util.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,191 @@ +"""Utilities for assertion debugging""" + +import py + + +# The _reprcompare attribute on the util module is used by the new assertion +# interpretation code and assertion rewriter to detect this plugin was +# loaded and in turn call the hooks defined here as part of the +# DebugInterpreter. +_reprcompare = None + +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) + + +# Provide basestring in python3 +try: + basestring = basestring +except NameError: + basestring = str + + +def assertrepr_compare(op, left, right): + """return specialised explanations for some operators/operands""" + width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op + left_repr = py.io.saferepr(left, maxsize=int(width/2)) + right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) + summary = '%s %s %s' % (left_repr, op, right_repr) + + issequence = lambda x: isinstance(x, (list, tuple)) + istext = lambda x: isinstance(x, basestring) + isdict = lambda x: isinstance(x, dict) + isset = lambda x: isinstance(x, set) + + explanation = None + try: + if op == '==': + if istext(left) and istext(right): + explanation = _diff_text(left, right) + elif issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right) + elif isdict(left) and isdict(right): + explanation = _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) + elif op == 'not in': + if istext(left) and istext(right): + explanation = _notin_text(left, right) + except py.builtin._sysex: + raise + except: + excinfo = py.code.ExceptionInfo() + explanation = ['(pytest_assertion plugin: representation of ' + 'details failed. Probably an object has a faulty __repr__.)', + str(excinfo) + ] + + + if not explanation: + return None + + # Don't include pageloads of data, should be configurable + if len(''.join(explanation)) > 80*8: + explanation = ['Detailed information too verbose, truncated'] + + return [summary] + explanation + + +def _diff_text(left, right): + """Return the explanation for the diff between text + + This will skip leading and trailing characters which are + identical to keep the diff minimal. + """ + explanation = [] + i = 0 # just in case left or right has zero length + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = ['Skipping %s identical ' + 'leading characters in diff' % i] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += ['Skipping %s identical ' + 'trailing characters in diff' % i] + left = left[:-i] + right = right[:-i] + explanation += [line.strip('\n') + for line in py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + return explanation + + +def _compare_eq_sequence(left, right): + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + explanation += ['At index %s diff: %r != %r' % + (i, left[i], right[i])] + break + if len(left) > len(right): + explanation += ['Left contains more items, ' + 'first extra item: %s' % py.io.saferepr(left[len(right)],)] + elif len(left) < len(right): + explanation += ['Right contains more items, ' + 'first extra item: %s' % py.io.saferepr(right[len(left)],)] + return explanation # + _diff_text(py.std.pprint.pformat(left), + # py.std.pprint.pformat(right)) + + +def _compare_eq_set(left, right): + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append('Extra items in the left set:') + for item in diff_left: + explanation.append(py.io.saferepr(item)) + if diff_right: + explanation.append('Extra items in the right set:') + for item in diff_right: + explanation.append(py.io.saferepr(item)) + return explanation + + +def _notin_text(term, text): + index = text.find(term) + head = text[:index] + tail = text[index+len(term):] + correct_text = head + tail + diff = _diff_text(correct_text, text) + newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] + for line in diff: + if line.startswith('Skipping'): + continue + if line.startswith('- '): + continue + if line.startswith('+ '): + newdiff.append(' ' + line[2:]) + else: + newdiff.append(line) + return newdiff --- a/_pytest/helpconfig.py Sun May 29 09:21:48 2011 +0200 +++ b/_pytest/helpconfig.py Tue May 31 14:11:53 2011 +0200 @@ -16,9 +16,6 @@ group.addoption('--traceconfig', action="store_true", dest="traceconfig", default=False, help="trace considerations of conftest.py files."), - group._addoption('--nomagic', - action="store_true", dest="nomagic", default=False, - help="don't reinterpret asserts, no traceback cutting. ") group.addoption('--debug', action="store_true", dest="debug", default=False, help="generate and show internal debugging information.") --- a/_pytest/main.py Sun May 29 09:21:48 2011 +0200 +++ b/_pytest/main.py Tue May 31 14:11:53 2011 +0200 @@ -46,23 +46,22 @@ def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, File=File)) + collect = dict(Item=Item, Collector=Collector, File=File, Session=Session) + return dict(collect=collect) def pytest_configure(config): py.test.config = config # compatibiltiy if config.option.exitfirst: config.option.maxfail = 1 -def pytest_cmdline_main(config): - """ default command line protocol for initialization, session, - running tests and reporting. """ +def wrap_session(config, doit): + """Skeleton command line program""" session = Session(config) session.exitstatus = EXIT_OK try: config.pluginmanager.do_configure(config) config.hook.pytest_sessionstart(session=session) - config.hook.pytest_collection(session=session) - config.hook.pytest_runtestloop(session=session) + doit(config, session) except pytest.UsageError: raise except KeyboardInterrupt: @@ -82,13 +81,17 @@ config.pluginmanager.do_unconfigure(config) return session.exitstatus +def pytest_cmdline_main(config): + return wrap_session(config, _main) + +def _main(config, session): + """ default command line protocol for initialization, session, + running tests and reporting. """ + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + def pytest_collection(session): - session.perform_collect() - hook = session.config.hook - hook.pytest_collection_modifyitems(session=session, - config=session.config, items=session.items) - hook.pytest_collection_finish(session=session) - return True + return session.perform_collect() def pytest_runtestloop(session): if session.config.option.collectonly: @@ -374,6 +377,16 @@ return HookProxy(fspath, self.config) def perform_collect(self, args=None, genitems=True): + hook = self.config.hook + try: + items = self._perform_collect(args, genitems) + hook.pytest_collection_modifyitems(session=self, + config=self.config, items=items) + finally: + hook.pytest_collection_finish(session=self) + return items + + def _perform_collect(self, args, genitems): if args is None: args = self.config.args self.trace("perform_collect", self, args) --- a/_pytest/pytester.py Sun May 29 09:21:48 2011 +0200 +++ b/_pytest/pytester.py Tue May 31 14:11:53 2011 +0200 @@ -6,7 +6,7 @@ import inspect import time from fnmatch import fnmatch -from _pytest.main import Session +from _pytest.main import Session, EXIT_OK from py.builtin import print_ from _pytest.core import HookRelay @@ -292,13 +292,19 @@ assert '::' not in str(arg) p = py.path.local(arg) x = session.fspath.bestrelpath(p) - return session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + return res def getpathnode(self, path): - config = self.parseconfig(path) + config = self.parseconfigure(path) session = Session(config) x = session.fspath.bestrelpath(path) - return session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + return res def genitems(self, colitems): session = colitems[0].session @@ -312,7 +318,9 @@ config = self.parseconfigure(*args) rec = self.getreportrecorder(config) session = Session(config) + config.hook.pytest_sessionstart(session=session) session.perform_collect() + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) return session.items, rec def runitem(self, source): @@ -382,6 +390,8 @@ c.basetemp = py.path.local.make_numbered_dir(prefix="reparse", keep=0, rootdir=self.tmpdir, lock_timeout=None) c.parse(args) + c.pluginmanager.do_configure(c) + self.request.addfinalizer(lambda: c.pluginmanager.do_unconfigure(c)) return c finally: py.test.config = oldconfig --- a/_pytest/python.py Sun May 29 09:21:48 2011 +0200 +++ b/_pytest/python.py Tue May 31 14:11:53 2011 +0200 @@ -4,6 +4,7 @@ import sys import pytest from py._code.code import TerminalRepr +from _pytest import assertion import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -226,8 +227,12 @@ def _importtestmodule(self): # we assume we are only called once per module + assertion.before_module_import(self) try: - mod = self.fspath.pyimport(ensuresyspath=True) + try: + mod = self.fspath.pyimport(ensuresyspath=True) + finally: + assertion.after_module_import(self) except SyntaxError: excinfo = py.code.ExceptionInfo() raise self.CollectError(excinfo.getrepr(style="short")) @@ -374,7 +379,7 @@ # test generators are seen as collectors but they also # invoke setup/teardown on popular request # (induced by the common "test_*" naming shared with normal tests) - self.config._setupstate.prepare(self) + self.session._setupstate.prepare(self) # see FunctionMixin.setup and test_setupstate_is_preserved_134 self._preservedparent = self.parent.obj l = [] @@ -726,7 +731,7 @@ def _addfinalizer(self, finalizer, scope): colitem = self._getscopeitem(scope) - self.config._setupstate.addfinalizer( + self._pyfuncitem.session._setupstate.addfinalizer( finalizer=finalizer, colitem=colitem) def __repr__(self): @@ -747,8 +752,10 @@ raise self.LookupError(msg) def showfuncargs(config): - from _pytest.main import Session - session = Session(config) + from _pytest.main import wrap_session + return wrap_session(config, _showfuncargs_main) + +def _showfuncargs_main(config, session): session.perform_collect() if session.items: plugins = session.items[0].getplugins() --- a/_pytest/runner.py Sun May 29 09:21:48 2011 +0200 +++ b/_pytest/runner.py Tue May 31 14:11:53 2011 +0200 @@ -14,17 +14,15 @@ # # pytest plugin hooks -# XXX move to pytest_sessionstart and fix py.test owns tests -def pytest_configure(config): - config._setupstate = SetupState() +def pytest_sessionstart(session): + session._setupstate = SetupState() def pytest_sessionfinish(session, exitstatus): - if hasattr(session.config, '_setupstate'): - hook = session.config.hook - rep = hook.pytest__teardown_final(session=session) - if rep: - hook.pytest__teardown_final_logerror(session=session, report=rep) - session.exitstatus = 1 + hook = session.config.hook + rep = hook.pytest__teardown_final(session=session) + if rep: + hook.pytest__teardown_final_logerror(session=session, report=rep) + session.exitstatus = 1 class NodeInfo: def __init__(self, location): @@ -46,16 +44,16 @@ return reports def pytest_runtest_setup(item): - item.config._setupstate.prepare(item) + item.session._setupstate.prepare(item) def pytest_runtest_call(item): item.runtest() def pytest_runtest_teardown(item): - item.config._setupstate.teardown_exact(item) + item.session._setupstate.teardown_exact(item) def pytest__teardown_final(session): - call = CallInfo(session.config._setupstate.teardown_all, when="teardown") + call = CallInfo(session._setupstate.teardown_all, when="teardown") if call.excinfo: ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) call.excinfo.traceback = ntraceback.filter() --- a/doc/assert.txt Sun May 29 09:21:48 2011 +0200 +++ b/doc/assert.txt Tue May 31 14:11:53 2011 +0200 @@ -18,8 +18,8 @@ def test_function(): assert f() == 4 -to assert that your object returns a certain value. If this -assertion fails you will see the value of ``x``:: +to assert that your function returns a certain value. If this assertion fails +you will see the value of ``x``:: $ py.test test_assert1.py =========================== test session starts ============================ @@ -39,22 +39,12 @@ test_assert1.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= -Reporting details about the failing assertion is achieved by re-evaluating -the assert expression and recording the intermediate values. +py.test has support for showing the values of the most common subexpressions +including calls, attributes, comparisons, and binary and unary operators. This +allows you to use the idiomatic python constructs without boilerplate code while +not losing introspection information. -Note: If evaluating the assert expression has side effects you may get a -warning that the intermediate values could not be determined safely. A -common example of this issue is an assertion which reads from a file:: - - assert f.read() != '...' - -If this assertion fails then the re-evaluation will probably succeed! -This is because ``f.read()`` will return an empty string when it is -called the second time during the re-evaluation. However, it is -easy to rewrite the assertion and avoid any trouble:: - - content = f.read() - assert content != '...' +See :ref:`assert-details` for more information on assertion introspection. assertions about expected exceptions @@ -137,6 +127,66 @@ See the :ref:`reporting demo ` for many more examples. + +.. _assert-details: + +Assertion introspection details +------------------------------- + +Reporting details about the failing assertion is achieved either by rewriting +assert statements before they are run or re-evaluating the assert expression and +recording the intermediate values. Which technique is used depends on the +location of the assert, py.test's configuration, and Python version being used +to run py.test. + +By default, if the Python version is greater than or equal to 2.6, py.test +rewrites assert statements in test modules. Rewritten assert statements put +introspection information into the assertion failure message. Note py.test only +rewrites test modules directly discovered by its test collection process, so +asserts in supporting modules will not be rewritten. + +.. note:: + + py.test rewrites test modules as it collects tests from them. It does this by + writing a new pyc file which Python loads when the test module is + imported. If the module has already been loaded (it is in sys.modules), + though, Python will not load the rewritten module. This means if a test + module imports another test module which has not already been rewritten, then + py.test will not be able to rewrite the second module. + +If an assert statement has not been rewritten or the Python version is less than +2.6, py.test falls back on assert reinterpretation. In assert reinterpretation, +py.test walks the frame of the function containing the assert statement to +discover sub-expression results of the failing assert statement. You can force +py.test to always use assertion reinterpretation by passing the +``--assertmode=old`` option. + +Assert reinterpretation has a caveat not present with assert rewriting: If +evaluating the assert expression has side effects you may get a warning that the +intermediate values could not be determined safely. A common example of this +issue is an assertion which reads from a file:: + + assert f.read() != '...' + +If this assertion fails then the re-evaluation will probably succeed! +This is because ``f.read()`` will return an empty string when it is +called the second time during the re-evaluation. However, it is +easy to rewrite the assertion and avoid any trouble:: + + content = f.read() + assert content != '...' + +All assert introspection can be turned off by passing ``--assertmode=off``. + +.. versionadded:: 2.1 + + Add assert rewriting as an alternate introspection technique. + +.. versionchanged:: 2.1 + + Introduce the ``--assertmode`` option. Deprecate ``--no-assert`` and + ``--nomagic``. + .. Defining your own comparison ---------------------------------------------- --- a/doc/faq.txt Sun May 29 09:21:48 2011 +0200 +++ b/doc/faq.txt Tue May 31 14:11:53 2011 +0200 @@ -47,13 +47,17 @@ ``py.test`` still uses many metaprogramming techniques and reading its source is thus likely not something for Python beginners. -A second "magic" issue arguably the assert statement re-intepreation: -When an ``assert`` statement fails, py.test re-interprets the expression -to show intermediate values if a test fails. If your expression -has side effects (better to avoid them anyway!) the intermediate values -may not be the same, obfuscating the initial error (this is also -explained at the command line if it happens). -``py.test --no-assert`` turns off assert re-interpretation. +A second "magic" issue arguably the assert statement debugging feature. When +loading test modules py.test rewrites the source code of assert statements. When +a rewritten assert statement fails, its error message has more information than +the original. py.test also has a second assert debugging technique. When an +``assert`` statement that was missed by the rewriter fails, py.test +re-interprets the expression to show intermediate values if a test fails. This +second technique suffers from caveat that the rewriting does not: If your +expression has side effects (better to avoid them anyway!) the intermediate +values may not be the same, confusing the reinterpreter and obfuscating the +initial error (this is also explained at the command line if it happens). +You can turn off all assertion debugging with ``py.test --assertmode=off``. .. _`py namespaces`: index.html .. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py --- a/setup.py Sun May 29 09:21:48 2011 +0200 +++ b/setup.py Tue May 31 14:11:53 2011 +0200 @@ -22,14 +22,14 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.1.0.dev1', + version='2.1.0.dev2', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), - install_requires=['py>1.4.1'], + install_requires=['py>1.4.3'], classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/testing/test_assertinterpret.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,327 @@ +"PYTEST_DONT_REWRITE" +import pytest, py + +from _pytest.assertion import util + +def exvalue(): + return py.std.sys.exc_info()[1] + +def f(): + return 2 + +def test_not_being_rewritten(): + assert "@py_builtins" not in globals() + +def test_assert(): + try: + assert f() == 3 + except AssertionError: + e = exvalue() + s = str(e) + assert s.startswith('assert 2 == 3\n') + +def test_assert_with_explicit_message(): + try: + assert f() == 3, "hello" + except AssertionError: + e = exvalue() + assert e.msg == 'hello' + +def test_assert_within_finally(): + class A: + def f(): + pass + excinfo = py.test.raises(TypeError, """ + try: + A().f() + finally: + i = 42 + """) + s = excinfo.exconly() + assert s.find("takes no argument") != -1 + + #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") + + + at py.test.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") + + at py.test.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_non_string_message(): + class A: + def __str__(self): + return "hello" + try: + assert 0 == 1, A() + except AssertionError: + e = exvalue() + assert e.msg == "hello" + +def test_assert_keyword_arg(): + def f(x=3): + return False + try: + assert f(x=5) + except AssertionError: + e = exvalue() + assert "x=5" in e.msg + +# 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: + py.test.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.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"] + + at py.test.mark.skipif("sys.version_info < (2,6)") +def test_assert_customizable_reprcompare(monkeypatch): + monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') + try: + assert 3 == 4 + except AssertionError: + e = exvalue() + s = str(e) + assert "hello" in s + +def test_assert_long_source_1(): + try: + assert len == [ + (None, ['somet text', 'more text']), + ] + except AssertionError: + e = exvalue() + s = str(e) + assert 're-run' not in s + assert 'somet text' in s + +def test_assert_long_source_2(): + try: + assert(len == [ + (None, ['somet text', 'more text']), + ]) + except AssertionError: + e = exvalue() + s = str(e) + assert 're-run' not in s + assert 'somet text' in s + +def test_assert_raise_alias(testdir): + testdir.makepyfile(""" + "PYTEST_DONT_REWRITE" + import sys + EX = AssertionError + def test_hello(): + raise EX("hello" + "multi" + "line") + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello*", + "*raise EX*", + "*1 failed*", + ]) + + + at 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 --- a/testing/test_assertion.py Sun May 29 09:21:48 2011 +0200 +++ b/testing/test_assertion.py Tue May 31 14:11:53 2011 +0200 @@ -2,11 +2,12 @@ import py, pytest import _pytest.assertion as plugin +from _pytest.assertion import reinterpret, util needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") def interpret(expr): - return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) + return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1))) class TestBinReprIntegration: pytestmark = needsnewassert @@ -25,7 +26,7 @@ self.right = right mockhook = MockHook() monkeypatch = request.getfuncargvalue("monkeypatch") - monkeypatch.setattr(py.code, '_reprcompare', mockhook) + monkeypatch.setattr(util, '_reprcompare', mockhook) return mockhook def test_pytest_assertrepr_compare_called(self, hook): @@ -40,13 +41,13 @@ assert hook.right == [0, 2] def test_configure_unconfigure(self, testdir, hook): - assert hook == py.code._reprcompare + assert hook == util._reprcompare config = testdir.parseconfig() plugin.pytest_configure(config) - assert hook != py.code._reprcompare + assert hook != util._reprcompare from _pytest.config import pytest_unconfigure pytest_unconfigure(config) - assert hook == py.code._reprcompare + assert hook == util._reprcompare def callequal(left, right): return plugin.pytest_assertrepr_compare('==', left, right) @@ -119,6 +120,10 @@ expl = ' '.join(callequal('foo', 'bar')) assert 'raised in repr()' not in expl + at pytest.mark.skipif("config._assertstate.mode != 'on'") +def test_rewritten(): + assert "@py_builtins" in globals() + def test_reprcompare_notin(): detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:] assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++'] @@ -159,7 +164,7 @@ ]) -def test_functional(testdir): +def test_assertion_options(testdir): testdir.makepyfile(""" def test_hello(): x = 3 @@ -167,8 +172,30 @@ """) result = testdir.runpytest() assert "3 == 4" in result.stdout.str() - result = testdir.runpytest("--no-assert") - assert "3 == 4" not in result.stdout.str() + off_options = (("--no-assert",), + ("--nomagic",), + ("--no-assert", "--nomagic"), + ("--assertmode=off",), + ("--assertmode=off", "--no-assert"), + ("--assertmode=off", "--nomagic"), + ("--assertmode=off," "--no-assert", "--nomagic")) + for opt in off_options: + result = testdir.runpytest(*opt) + assert "3 == 4" not in result.stdout.str() + for mode in "on", "old": + for other_opt in off_options[:3]: + opt = ("--assertmode=" + mode,) + other_opt + result = testdir.runpytest(*opt) + assert result.ret == 3 + assert "assertion options conflict" in result.stderr.str() + +def test_old_assert_mode(testdir): + testdir.makepyfile(""" + def test_in_old_mode(): + assert "@py_builtins" not in globals() + """) + result = testdir.runpytest("--assertmode=old") + assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" @@ -221,3 +248,10 @@ result.stderr.fnmatch_lines([ "*WARNING*assertion*", ]) + +def test_load_fake_pyc(testdir): + path = testdir.makepyfile("x = 'hello'") + co = compile("x = 'bye'", str(path), "exec") + plugin._write_pyc(co, path) + mod = path.pyimport() + assert mod.x == "bye" --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/testing/test_assertrewrite.py Tue May 31 14:11:53 2011 +0200 @@ -0,0 +1,256 @@ +import sys +import py +import pytest + +ast = pytest.importorskip("ast") + +from _pytest.assertion import util +from _pytest.assertion.rewrite import rewrite_asserts + + +def setup_module(mod): + mod._old_reprcompare = util._reprcompare + py.code._reprcompare = None + +def teardown_module(mod): + util._reprcompare = mod._old_reprcompare + del mod._old_reprcompare + + +def rewrite(src): + tree = ast.parse(src) + rewrite_asserts(tree) + return tree + +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) + mod = rewrite(src) + code = compile(mod, "", "exec") + ns = {} + if extra_ns is not None: + ns.update(extra_ns) + py.builtin.exec_(code, ns) + func = ns[f.__name__] + try: + func() + except AssertionError: + if must_pass: + pytest.fail("shouldn't have raised") + s = str(sys.exc_info()[1]) + if not s.startswith("assert"): + return "AssertionError: " + s + return s + else: + if not must_pass: + pytest.fail("function didn't raise at all") + + +class TestAssertionRewrite: + + def test_place_initial_imports(self): + s = """'Doc string'\nother = stuff""" + m = rewrite(s) + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + for imp in m.body[1:3]: + assert isinstance(imp, ast.Import) + assert imp.lineno == 2 + assert imp.col_offset == 0 + assert isinstance(m.body[3], ast.Assign) + s = """from __future__ import with_statement\nother_stuff""" + m = rewrite(s) + assert isinstance(m.body[0], ast.ImportFrom) + for imp in m.body[1:3]: + assert isinstance(imp, ast.Import) + assert imp.lineno == 2 + assert imp.col_offset == 0 + assert isinstance(m.body[3], ast.Expr) + s = """'doc string'\nfrom __future__ import with_statement\nother""" + m = rewrite(s) + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + assert isinstance(m.body[1], ast.ImportFrom) + for imp in m.body[2:4]: + assert isinstance(imp, ast.Import) + assert imp.lineno == 3 + assert imp.col_offset == 0 + assert isinstance(m.body[4], ast.Expr) + + def test_dont_rewrite(self): + s = """'PYTEST_DONT_REWRITE'\nassert 14""" + m = rewrite(s) + assert len(m.body) == 2 + assert isinstance(m.body[0].value, ast.Str) + assert isinstance(m.body[1], ast.Assert) + assert m.body[1].msg is None + + def test_name(self): + def f(): + assert False + assert getmsg(f) == "assert False" + def f(): + f = False + assert f + assert getmsg(f) == "assert False" + def f(): + assert a_global + assert getmsg(f, {"a_global" : False}) == "assert a_global" + + def test_assert_already_has_message(self): + def f(): + assert False, "something bad!" + assert getmsg(f) == "AssertionError: something bad!" + + def test_boolop(self): + def f(): + f = g = False + assert f and g + assert getmsg(f) == "assert (False)" + def f(): + f = True + g = False + assert f and g + assert getmsg(f) == "assert (True and False)" + def f(): + f = False + g = True + assert f and g + assert getmsg(f) == "assert (False)" + def f(): + f = g = False + assert f or g + assert getmsg(f) == "assert (False or False)" + def f(): + f = True + g = False + assert f or g + getmsg(f, must_pass=True) + + def test_short_circut_evaluation(self): + pytest.xfail("complicated fix; I'm not sure if it's important") + def f(): + assert True or explode + getmsg(f, must_pass=True) + + def test_unary_op(self): + def f(): + x = True + assert not x + assert getmsg(f) == "assert not True" + def f(): + x = 0 + assert ~x + 1 + assert getmsg(f) == "assert (~0 + 1)" + def f(): + x = 3 + assert -x + x + assert getmsg(f) == "assert (-3 + 3)" + def f(): + x = 0 + assert +x + x + assert getmsg(f) == "assert (+0 + 0)" + + def test_binary_op(self): + def f(): + x = 1 + y = -1 + assert x + y + assert getmsg(f) == "assert (1 + -1)" + + def test_call(self): + def g(a=42, *args, **kwargs): + return False + ns = {"g" : g} + def f(): + assert g() + assert getmsg(f, ns) == """assert False + + where False = g()""" + def f(): + assert g(1) + assert getmsg(f, ns) == """assert False + + where False = g(1)""" + def f(): + assert g(1, 2) + assert getmsg(f, ns) == """assert False + + where False = g(1, 2)""" + def f(): + assert g(1, g=42) + assert getmsg(f, ns) == """assert False + + where False = g(1, g=42)""" + def f(): + assert g(1, 3, g=23) + assert getmsg(f, ns) == """assert False + + where False = g(1, 3, g=23)""" + + def test_attribute(self): + class X(object): + g = 3 + ns = {"X" : X, "x" : X()} + def f(): + assert not x.g + assert getmsg(f, ns) == """assert not 3 + + where 3 = x.g""" + def f(): + x.a = False + assert x.a + assert getmsg(f, ns) == """assert False + + where False = x.a""" + + def test_comparisons(self): + def f(): + a, b = range(2) + assert b < a + assert getmsg(f) == """assert 1 < 0""" + def f(): + a, b, c = range(3) + assert a > b > c + assert getmsg(f) == """assert 0 > 1""" + def f(): + a, b, c = range(3) + assert a < b > c + assert getmsg(f) == """assert 1 > 2""" + def f(): + a, b, c = range(3) + assert a < b <= c + getmsg(f, must_pass=True) + def f(): + a, b, c = range(3) + assert a < b + assert b < c + getmsg(f, must_pass=True) + + def test_len(self): + def f(): + l = list(range(10)) + assert len(l) == 11 + assert getmsg(f).startswith("""assert 10 == 11 + + where 10 = len([""") + + def test_custom_reprcompare(self, monkeypatch): + def my_reprcompare(op, left, right): + return "42" + monkeypatch.setattr(util, "_reprcompare", my_reprcompare) + def f(): + assert 42 < 3 + assert getmsg(f) == "assert 42" + def my_reprcompare(op, left, right): + return "%s %s %s" % (left, op, right) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare) + def f(): + assert 1 < 3 < 5 <= 4 < 7 + assert getmsg(f) == "assert 5 <= 4" + + def test_assert_raising_nonzero_in_comparison(self): + def f(): + class A(object): + def __nonzero__(self): + raise ValueError(42) + def __lt__(self, other): + return A() + def __repr__(self): + return "" + def myany(x): + return False + assert myany(A() < 0) + assert "< 0" in getmsg(f) --- a/testing/test_collection.py Sun May 29 09:21:48 2011 +0200 +++ b/testing/test_collection.py Tue May 31 14:11:53 2011 +0200 @@ -313,7 +313,7 @@ def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) topdir = testdir.tmpdir rcol = Session(config) assert topdir == rcol.fspath @@ -328,7 +328,7 @@ def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) topdir = testdir.tmpdir rcol = Session(config) assert topdir == rcol.fspath @@ -363,7 +363,7 @@ p.basename + "::TestClass::()", normid, ]: - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) rcol = Session(config=config) rcol.perform_collect() items = rcol.items @@ -388,7 +388,7 @@ """ % p.basename) id = p.basename - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -413,7 +413,7 @@ aaa = testdir.mkpydir("aaa") test_aaa = aaa.join("test_aaa.py") p.move(test_aaa) - config = testdir.parseconfig() + config = testdir.parseconfigure() rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -437,7 +437,7 @@ p.move(test_bbb) id = "." - config = testdir.parseconfig(id) + config = testdir.parseconfigure(id) rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -455,7 +455,7 @@ def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") - config = testdir.parseconfig() + config = testdir.parseconfigure() rcol = Session(config) rcol.perform_collect() items = rcol.items @@ -476,7 +476,7 @@ pass """) arg = p.basename + ("::TestClass::test_method") - config = testdir.parseconfig(arg) + config = testdir.parseconfigure(arg) rcol = Session(config) rcol.perform_collect() items = rcol.items --- a/testing/test_python.py Sun May 29 09:21:48 2011 +0200 +++ b/testing/test_python.py Tue May 31 14:11:53 2011 +0200 @@ -705,11 +705,11 @@ def test_func(something): pass """) req = funcargs.FuncargRequest(item) - req.config._setupstate.prepare(item) # XXX + req._pyfuncitem.session._setupstate.prepare(item) # XXX req._fillfuncargs() # successively check finalization calls teardownlist = item.getparent(pytest.Module).obj.teardownlist - ss = item.config._setupstate + ss = item.session._setupstate assert not teardownlist ss.teardown_exact(item) print(ss.stack) @@ -834,11 +834,11 @@ ret1 = req1.cached_setup(setup, teardown, scope="function") assert l == ['setup'] # artificial call of finalizer - req1.config._setupstate._callfinalizers(item1) + req1._pyfuncitem.session._setupstate._callfinalizers(item1) assert l == ["setup", "teardown"] ret2 = req1.cached_setup(setup, teardown, scope="function") assert l == ["setup", "teardown", "setup"] - req1.config._setupstate._callfinalizers(item1) + req1._pyfuncitem.session._setupstate._callfinalizers(item1) assert l == ["setup", "teardown", "setup", "teardown"] def test_request_cached_setup_two_args(self, testdir): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue May 31 15:21:53 2011 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 31 May 2011 13:21:53 -0000 Subject: [py-svn] commit/pytest: hpk42: - properly include _pytest.assertion in distribution Message-ID: <20110531132153.8674.44185@bitbucket02.managed.contegix.com> 1 new changeset in pytest: http://bitbucket.org/hpk42/pytest/changeset/65f610a0f390/ changeset: 65f610a0f390 branches: user: hpk42 date: 2011-05-31 15:21:08 summary: - properly include _pytest.assertion in distribution - import assertion only at import-test module time affected #: 3 files (27 bytes) --- a/_pytest/__init__.py Tue May 31 14:11:53 2011 +0200 +++ b/_pytest/__init__.py Tue May 31 15:21:08 2011 +0200 @@ -1,2 +1,2 @@ # -__version__ = '2.1.0.dev2' +__version__ = '2.1.0.dev3' --- a/_pytest/python.py Tue May 31 14:11:53 2011 +0200 +++ b/_pytest/python.py Tue May 31 15:21:08 2011 +0200 @@ -4,7 +4,6 @@ import sys import pytest from py._code.code import TerminalRepr -from _pytest import assertion import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -227,6 +226,7 @@ def _importtestmodule(self): # we assume we are only called once per module + from _pytest import assertion assertion.before_module_import(self) try: try: --- a/setup.py Tue May 31 14:11:53 2011 +0200 +++ b/setup.py Tue May 31 15:21:08 2011 +0200 @@ -22,7 +22,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.1.0.dev2', + version='2.1.0.dev3', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -41,7 +41,7 @@ 'Topic :: Utilities', 'Programming Language :: Python', 'Programming Language :: Python :: 3'], - packages=['_pytest', ], + packages=['_pytest', '_pytest.assertion'], py_modules=['pytest'], zip_safe=False, ) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.