diff --git a/.travis.yml b/.travis.yml index c3e301ca5..3a8f36e95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,25 +7,25 @@ install: "pip install -U tox" # # command to run tests env: matrix: + # coveralls is not listed in tox's envlist, but should run in travis - TESTENV=coveralls - - TESTENV=doctesting - - TESTENV=flakes + # note: please use "tox --listenvs" to populate the build matrix below + - TESTENV=linting - TESTENV=py26 - TESTENV=py27 - - TESTENV=py27-cxfreeze - - TESTENV=py27-nobyte - - TESTENV=py27-pexpect - - TESTENV=py27-subprocess - - TESTENV=py27-trial - - TESTENV=py27-xdist - - TESTENV=py33 - TESTENV=py33 - TESTENV=py34 - - TESTENV=py35-pexpect - - TESTENV=py35-trial - - TESTENV=py35-xdist - TESTENV=py35 - TESTENV=pypy + - TESTENV=py27-pexpect + - TESTENV=py27-xdist + - TESTENV=py27-trial + - TESTENV=py35-pexpect + - TESTENV=py35-xdist + - TESTENV=py35-trial + - TESTENV=py27-nobyte + - TESTENV=doctesting + - TESTENV=py27-cxfreeze script: tox --recreate -e $TESTENV diff --git a/AUTHORS b/AUTHORS index 7b22002d1..2f0c268e3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,8 +26,10 @@ Daniel Grana Daniel Nuri Dave Hunt David Mohr +David Vierra Edison Gustavo Muenz Eduardo Schettino +Endre Galaczi Elizaveta Shashkova Eric Hunsberger Eric Siegerman @@ -45,6 +47,7 @@ Jaap Broekhuizen Jan Balster Janne Vanhala Jason R. Coombs +Joshua Bronson Jurko Gospodnetić Katarzyna Jachim Kevin Cox @@ -55,6 +58,7 @@ Marc Schlaich Mark Abramowitz Markus Unterwaditzer Martijn Faassen +Michael Aquilina Michael Birtwell Michael Droettboom Nicolas Delaby diff --git a/CHANGELOG b/CHANGELOG.rst similarity index 94% rename from CHANGELOG rename to CHANGELOG.rst index ceac3efe4..acbe12c87 100644 --- a/CHANGELOG +++ b/CHANGELOG.rst @@ -1,5 +1,89 @@ -2.8.8.dev1 ----------- +2.9.0.dev +========= + +**New Features** + +* New ``pytest.mark.skip`` mark, which unconditional skips marked tests. + Thanks `@MichaelAquilina`_ for the complete PR (`#1040`_). + +* ``--doctest-glob`` may now be passed multiple times in the command-line. + Thanks `@jab`_ and `@nicoddemus`_ for the PR. + +* New ``-rp`` and ``-rP`` reporting options give the summary and full output + of passing tests, respectively. Thanks to `@codewarrior0`_ for the PR. + +* New ``ALLOW_BYTES`` doctest option strips ``b`` prefixes from byte strings + in doctest output (similar to ``ALLOW_UNICODE``). + Thanks `@jaraco`_ for the request and `@nicoddemus`_ for the PR (`#1287`_). +* give a hint on KeyboardInterrupt to use the --fulltrace option to show the errors, + this fixes `#1366`_. + Thanks to `@hpk42`_ for the report and `@RonnyPfannschmidt`_ for the PR. + +**Changes** + +* **Important**: `py.code `_ has been + merged into the ``pytest`` repository as ``pytest._code``. This decision + was made because ``py.code`` had very few uses outside ``pytest`` and the + fact that it was in a different repository made it difficult to fix bugs on + its code in a timely manner. The team hopes with this to be able to better + refactor out and improve that code. + This change shouldn't affect users, but it is useful to let users aware + if they encounter any strange behavior. + + Keep in mind that the code for ``pytest._code`` is **private** and + **experimental**, so you definitely should not import it explicitly! + + Please note that the original ``py.code`` is still available in + `pylib `_. + +* ``pytest_enter_pdb`` now optionally receives the pytest config object. + Thanks `@nicoddemus`_ for the PR. + +* Removed code and documentation for Python 2.5 or lower versions, + including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. + Thanks `@nicoddemus`_ for the PR (`#1226`_). + +* Comparisons now always show up in full when ``CI`` or ``BUILD_NUMBER`` is + found in the environment, even when -vv isn't used. + Thanks `@The-Compiler`_ for the PR. + +* ``--lf`` and ``--ff`` now support long names: ``--last-failed`` and + ``--failed-first`` respectively. + Thanks `@MichaelAquilina`_ for the PR. + +* Added expected exceptions to pytest.raises fail message + +**Bug Fixes** + +* The ``-s`` and ``-c`` options should now work under ``xdist``; + ``Config.fromdictargs`` now represents its input much more faithfully. + Thanks to `@bukzor`_ for the complete PR (`#680`_). + +* Fix (`#1290`_): support Python 3.5's `@` operator in assertion rewriting. + Thanks `@Shinkenjoe`_ for report with test case and `@tomviner`_ for the PR. + +* Fix formatting utf-8 explanation messages (`#1379`_). + Thanks `@biern`_ for the PR. + +.. _#1379: https://github.com/pytest-dev/pytest/issues/1379 +.. _#1366: https://github.com/pytest-dev/pytest/issues/1366 +.. _#1040: https://github.com/pytest-dev/pytest/pull/1040 +.. _#680: https://github.com/pytest-dev/pytest/issues/680 +.. _#1287: https://github.com/pytest-dev/pytest/pull/1287 +.. _#1226: https://github.com/pytest-dev/pytest/pull/1226 +.. _#1290: https://github.com/pytest-dev/pytest/pull/1290 +.. _@biern: https://github.com/biern +.. _@MichaelAquilina: https://github.com/MichaelAquilina +.. _@bukzor: https://github.com/bukzor +.. _@hpk42: https://github.com/hpk42 +.. _@nicoddemus: https://github.com/nicoddemus +.. _@jab: https://github.com/jab +.. _@codewarrior0: https://github.com/codewarrior0 +.. _@jaraco: https://github.com/jaraco +.. _@The-Compiler: https://github.com/The-Compiler +.. _@Shinkenjoe: https://github.com/Shinkenjoe +.. _@tomviner: https://github.com/tomviner +.. _@RonnyPfannschmidt: https://github.com/RonnyPfannschmidt 2.8.7 ----- @@ -39,7 +123,7 @@ 2.8.5 ------ +===== - fix #1243: fixed issue where class attributes injected during collection could break pytest. PR by Alexei Kozlenok, thanks Ronny Pfannschmidt and Bruno Oliveira for the review and help. @@ -53,7 +137,7 @@ 2.8.4 ------ +===== - fix #1190: ``deprecated_call()`` now works when the deprecated function has been already called by another test in the same @@ -77,7 +161,7 @@ Thanks Bruno Oliveira for the PR. 2.8.3 ------ +===== - fix #1169: add __name__ attribute to testcases in TestCaseFunction to support the @unittest.skip decorator on functions and methods. @@ -104,10 +188,8 @@ system integrity protection (thanks Florian) - - 2.8.2 ------ +===== - fix #1085: proper handling of encoding errors when passing encoded byte strings to pytest.parametrize in Python 2. @@ -127,7 +209,7 @@ Oliveira for the PR. 2.8.1 ------ +===== - fix #1034: Add missing nodeid on pytest_logwarning call in addhook. Thanks Simon Gomizelj for the PR. @@ -166,6 +248,7 @@ - Fix issue #411: Add __eq__ method to assertion comparison example. Thanks Ben Webb. +- Fix issue #653: deprecated_call can be used as context manager. - fix issue 877: properly handle assertion explanations with non-ascii repr Thanks Mathieu Agopian for the report and Ronny Pfannschmidt for the PR. @@ -173,7 +256,7 @@ - fix issue 1029: transform errors when writing cache values into pytest-warnings 2.8.0 ------------------------------ +============================= - new ``--lf`` and ``-ff`` options to run only the last failing tests or "failing tests first" from the last run. This functionality is provided @@ -363,7 +446,7 @@ Thanks Peter Lauri for the report and Bruno Oliveira for the PR. 2.7.3 (compared to 2.7.2) ------------------------------ +============================= - Allow 'dev', 'rc', or other non-integer version strings in `importorskip`. Thanks to Eric Hunsberger for the PR. @@ -406,7 +489,7 @@ Thanks Bruno Oliveira for the PR. 2.7.2 (compared to 2.7.1) ------------------------------ +============================= - fix issue767: pytest.raises value attribute does not contain the exception instance on Python 2.6. Thanks Eric Siegerman for providing the test @@ -435,7 +518,7 @@ 2.7.1 (compared to 2.7.0) ------------------------------ +============================= - fix issue731: do not get confused by the braces which may be present and unbalanced in an object's repr while collapsing False @@ -468,7 +551,7 @@ at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ------------------------------ +============================= - fix issue435: make reload() work when assert rewriting is active. Thanks Daniel Hahler. @@ -527,7 +610,7 @@ it from the "decorator" case. Thanks Tom Viner. - "python_classes" and "python_functions" options now support glob-patterns - for test discovery, as discussed in issue600. Thanks Ldiary Translations. + for test discovery, as discussed in issue600. Thanks Ldiary Translations. - allow to override parametrized fixtures with non-parametrized ones and vice versa (bubenkoff). @@ -538,7 +621,7 @@ via postmortem debugging (almarklein). 2.6.4 ----------- +========== - Improve assertion failure reporting on iterables, by using ndiff and pprint. @@ -567,7 +650,7 @@ - fix issue614: fixed pastebin support. 2.6.3 ------------ +=========== - fix issue575: xunit-xml was reporting collection errors as failures instead of errors, thanks Oleg Sinyavskiy. @@ -594,7 +677,7 @@ Floris Bruynooghe. 2.6.2 ------------ +=========== - Added function pytest.freeze_includes(), which makes it easy to embed pytest into executables using tools like cx_freeze. @@ -623,7 +706,7 @@ to them. 2.6.1 ------------------------------------ +=================================== - No longer show line numbers in the --verbose output, the output is now purely the nodeid. The line number is still shown in failure reports. @@ -655,7 +738,7 @@ Thanks Bruno Oliveira. 2.6 ------------------------------------ +=================================== - Cache exceptions from fixtures according to their scope (issue 467). @@ -760,7 +843,7 @@ 2.5.2 ------------------------------------ +=================================== - fix issue409 -- better interoperate with cx_freeze by not trying to import from collections.abc which causes problems @@ -788,7 +871,7 @@ 2.5.1 ------------------------------------ +=================================== - merge new documentation styling PR from Tobias Bieniek. @@ -809,7 +892,7 @@ 2.5.0 ------------------------------------ +=================================== - dropped python2.5 from automated release testing of pytest itself which means it's probably going to break soon (but still works @@ -945,7 +1028,7 @@ - fix verbose reporting for @mock'd test functions v2.4.2 ------------------------------------ +=================================== - on Windows require colorama and a newer py lib so that py.io.TerminalWriter() now uses colorama instead of its own ctypes hacks. (fixes issue365) @@ -976,7 +1059,7 @@ v2.4.2 config.do_configure() for plugin-compatibility v2.4.1 ------------------------------------ +=================================== - When using parser.addoption() unicode arguments to the "type" keyword should also be converted to the respective types. @@ -992,7 +1075,7 @@ v2.4.1 - merge doc typo fixes, thanks Andy Dirnberger v2.4 ------------------------------------ +=================================== known incompatibilities: @@ -1161,7 +1244,7 @@ Bug fixes: information at the end of a test run. v2.3.5 ------------------------------------ +=================================== - fix issue169: respect --tb=style with setup/teardown errors as well. @@ -1226,7 +1309,7 @@ v2.3.5 - fix issue266 - accept unicode in MarkEvaluator expressions v2.3.4 ------------------------------------ +=================================== - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to @@ -1246,7 +1329,7 @@ v2.3.4 method in a certain test class. v2.3.3 ------------------------------------ +=================================== - fix issue214 - parse modules that contain special objects like e. g. flask's request object which blows up on getattr access if no request @@ -1278,7 +1361,7 @@ v2.3.3 add a ``config.getoption(name)`` helper function for consistency. v2.3.2 ------------------------------------ +=================================== - fix issue208 and fix issue29 use new py version to avoid long pauses when printing tracebacks in long modules @@ -1311,7 +1394,7 @@ v2.3.2 bits are properly distributed for maintainers who run pytest-own tests v2.3.1 ------------------------------------ +=================================== - fix issue202 - fix regression: using "self" from fixture functions now works as expected (it's the same "self" instance that a test method @@ -1324,7 +1407,7 @@ v2.3.1 pytest.mark.* usage. v2.3.0 ------------------------------------ +=================================== - fix issue202 - better automatic names for parametrized test functions - fix issue139 - introduce @pytest.fixture which allows direct scoping @@ -1403,7 +1486,7 @@ v2.3.0 - py.test -vv will show all of assert comparisations instead of truncating v2.2.4 ------------------------------------ +=================================== - fix error message for rewritten assertions involving the % operator - fix issue 126: correctly match all invalid xml characters for junitxml @@ -1420,12 +1503,12 @@ v2.2.4 - upgrade distribute_setup.py to 0.6.27 v2.2.3 ----------------------------------------- +======================================== - fix uploaded package to only include neccesary files v2.2.2 ----------------------------------------- +======================================== - fix issue101: wrong args to unittest.TestCase test function now produce better output @@ -1445,7 +1528,7 @@ v2.2.2 with distributed testing (no upgrade of pytest-xdist needed) v2.2.1 ----------------------------------------- +======================================== - fix issue99 (in pytest and py) internallerrors with resultlog now produce better output - fixed by normalizing pytest_internalerror @@ -1462,7 +1545,7 @@ v2.2.1 to Ralf Schmitt (fixed by depending on a more recent pylib) v2.2.0 ----------------------------------------- +======================================== - fix issue90: introduce eager tearing down of test items so that teardown function are called earlier. @@ -1497,7 +1580,7 @@ v2.2.0 - add support for skip properties on unittest classes and functions v2.1.3 ----------------------------------------- +======================================== - fix issue79: assertion rewriting failed on some comparisons in boolops - correctly handle zero length arguments (a la pytest '') @@ -1506,7 +1589,7 @@ v2.1.3 - fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests v2.1.2 ----------------------------------------- +======================================== - fix assertion rewriting on files with windows newlines on some Python versions - refine test discovery by package/module name (--pyargs), thanks Florian Mayer @@ -1516,7 +1599,7 @@ v2.1.2 - don't try assertion rewriting on Jython, use reinterp v2.1.1 ----------------------------------------------- +============================================== - fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks - fix issue60 / fix error conditions involving the creation of __pycache__ @@ -1529,7 +1612,7 @@ v2.1.1 - you can now build a man page with "cd doc ; make man" v2.1.0 ----------------------------------------------- +============================================== - fix issue53 call nosestyle setup functions with correct ordering - fix issue58 and issue59: new assertion code fixes @@ -1549,7 +1632,7 @@ v2.1.0 - fix issue 35 - provide PDF doc version and download link from index page v2.0.3 ----------------------------------------------- +============================================== - fix issue38: nicer tracebacks on calls to hooks, particularly early configure/sessionstart ones @@ -1569,7 +1652,7 @@ v2.0.3 - fix issue37: avoid invalid characters in junitxml's output v2.0.2 ----------------------------------------------- +============================================== - tackle issue32 - speed up test runs of very quick test functions by reducing the relative overhead @@ -1621,7 +1704,7 @@ v2.0.2 - avoid std unittest assertion helper code in tracebacks (thanks Ronny) v2.0.1 ----------------------------------------------- +============================================== - refine and unify initial capturing so that it works nicely even if the logging module is used on an early-loaded conftest.py @@ -1670,7 +1753,7 @@ v2.0.1 mechanism, see the docs. v2.0.0 ----------------------------------------------- +============================================== - pytest-2.0 is now its own package and depends on pylib-2.0 - new ability: python -m pytest / python -m pytest.main ability @@ -1715,7 +1798,7 @@ v2.0.0 - fix strangeness: mark.* objects are now immutable, create new instances v1.3.4 ----------------------------------------------- +============================================== - fix issue111: improve install documentation for windows - fix issue119: fix custom collectability of __init__.py as a module @@ -1724,7 +1807,7 @@ v1.3.4 - fix issue118: new --tb=native for presenting cpython-standard exceptions v1.3.3 ----------------------------------------------- +============================================== - fix issue113: assertion representation problem with triple-quoted strings (and possibly other cases) @@ -1739,10 +1822,9 @@ v1.3.3 - remove trailing whitespace in all py/text distribution files v1.3.2 ----------------------------------------------- +============================================== -New features -++++++++++++++++++ +**New features** - fix issue103: introduce py.test.raises as context manager, examples:: @@ -1777,8 +1859,7 @@ New features - introduce '--junitprefix=STR' option to prepend a prefix to all reports in the junitxml file. -Bug fixes / Maintenance -++++++++++++++++++++++++++ +**Bug fixes** - make tests and the ``pytest_recwarn`` plugin in particular fully compatible to Python2.7 (if you use the ``recwarn`` funcarg warnings will be enabled so that @@ -1814,10 +1895,9 @@ Bug fixes / Maintenance - ship distribute_setup.py version 0.6.13 v1.3.1 ---------------------------------------------- +============================================= -New features -++++++++++++++++++ +**New features** - issue91: introduce new py.test.xfail(reason) helper to imperatively mark a test as expected to fail. Can @@ -1855,8 +1935,7 @@ New features course requires that your application and tests are properly teared down and don't have global state. -Fixes / Maintenance -++++++++++++++++++++++ +**Bug Fixes** - improved traceback presentation: - improved and unified reporting for "--tb=short" option @@ -1886,7 +1965,7 @@ Fixes / Maintenance v1.3.0 ---------------------------------------------- +============================================= - deprecate --report option in favour of a new shorter and easier to remember -r option: it takes a string argument consisting of any @@ -1951,7 +2030,7 @@ v1.3.0 v1.2.0 ---------------------------------------------- +============================================= - refined usage and options for "py.cleanup":: @@ -1990,7 +2069,7 @@ v1.2.0 - fix plugin links v1.1.1 ---------------------------------------------- +============================================= - moved dist/looponfailing from py.test core into a new separately released pytest-xdist plugin. @@ -2074,7 +2153,7 @@ v1.1.1 v1.1.0 ---------------------------------------------- +============================================= - introduce automatic plugin registration via 'pytest11' entrypoints via setuptools' pkg_resources.iter_entry_points @@ -2092,8 +2171,8 @@ v1.1.0 - try harder to have deprecation warnings for py.compat.* accesses report a correct location -v1.0.2 ---------------------------------------------- +v1.0.3 +============================================= * adjust and improve docs @@ -2178,7 +2257,7 @@ v1.0.2 * simplified internal localpath implementation v1.0.2 -------------------------------------------- +=========================================== * fixing packaging issues, triggered by fedora redhat packaging, also added doc, examples and contrib dirs to the tarball. @@ -2186,7 +2265,7 @@ v1.0.2 * added a documentation link to the new django plugin. v1.0.1 -------------------------------------------- +=========================================== * added a 'pytest_nose' plugin which handles nose.SkipTest, nose-style function/method/generator setup/teardown and @@ -2220,13 +2299,13 @@ v1.0.1 renamed some internal methods and argnames v1.0.0 -------------------------------------------- +=========================================== * more terse reporting try to show filesystem path relatively to current dir * improve xfail output a bit v1.0.0b9 -------------------------------------------- +=========================================== * cleanly handle and report final teardown of test setup @@ -2260,7 +2339,7 @@ v1.0.0b9 v1.0.0b8 -------------------------------------------- +=========================================== * pytest_unittest-plugin is now enabled by default @@ -2289,7 +2368,7 @@ v1.0.0b8 thanks Radomir. v1.0.0b7 -------------------------------------------- +=========================================== * renamed py.test.xfail back to py.test.mark.xfail to avoid two ways to decorate for xfail @@ -2314,7 +2393,7 @@ v1.0.0b7 * make __name__ == "__channelexec__" for remote_exec code v1.0.0b3 -------------------------------------------- +=========================================== * plugin classes are removed: one now defines hooks directly in conftest.py or global pytest_*.py @@ -2331,7 +2410,7 @@ v1.0.0b3 v1.0.0b1 -------------------------------------------- +=========================================== * introduced new "funcarg" setup method, see doc/test/funcarg.txt @@ -2355,7 +2434,7 @@ v1.0.0b1 XXX lots of things missing here XXX v0.9.2 -------------------------------------------- +=========================================== * refined installation and metadata, created new setup.py, now based on setuptools/ez_setup (thanks to Ralf Schmitt @@ -2388,7 +2467,7 @@ v0.9.2 * there now is a py.__version__ attribute v0.9.1 -------------------------------------------- +=========================================== This is a fairly complete list of v0.9.1, which can serve as a reference for developers. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4f5007a0a..2bee8b17e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -179,10 +179,10 @@ but here is a simple overview: You need to have Python 2.7 and 3.5 available in your system. Now running tests is as simple as issuing this command:: - $ python runtox.py -e py27,py35,flakes + $ python runtox.py -e linting,py27,py35 This command will run tests via the "tox" tool against Python 2.7 and 3.5 - and also perform "flakes" coding-style checks. ``runtox.py`` is + and also perform "lint" coding-style checks. ``runtox.py`` is a thin wrapper around ``tox`` which installs from a development package index where newer (not yet released to pypi) versions of dependencies (especially ``py``) might be present. diff --git a/MANIFEST.in b/MANIFEST.in index 9fc16c553..266a9184d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include CHANGELOG +include CHANGELOG.rst include LICENSE include AUTHORS diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 2d23a02ec..51751401b 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.8.8.dev1' +__version__ = '2.9.0.dev1' diff --git a/_pytest/_argcomplete.py b/_pytest/_argcomplete.py index 4f4eaf925..955855a96 100644 --- a/_pytest/_argcomplete.py +++ b/_pytest/_argcomplete.py @@ -88,9 +88,6 @@ class FastFilesCompleter: return completion if os.environ.get('_ARGCOMPLETE'): - # argcomplete 0.5.6 is not compatible with python 2.5.6: print/with/format - if sys.version_info[:2] < (2, 6): - sys.exit(1) try: import argcomplete.completers except ImportError: diff --git a/_pytest/_code/__init__.py b/_pytest/_code/__init__.py new file mode 100644 index 000000000..c046b9716 --- /dev/null +++ b/_pytest/_code/__init__.py @@ -0,0 +1,12 @@ +""" python inspection/code generation API """ +from .code import Code # noqa +from .code import ExceptionInfo # noqa +from .code import Frame # noqa +from .code import Traceback # noqa +from .code import getrawcode # noqa +from .code import patch_builtins # noqa +from .code import unpatch_builtins # noqa +from .source import Source # noqa +from .source import compile_ as compile # noqa +from .source import getfslineno # noqa + diff --git a/_pytest/_code/_py2traceback.py b/_pytest/_code/_py2traceback.py new file mode 100644 index 000000000..d65e27cb7 --- /dev/null +++ b/_pytest/_code/_py2traceback.py @@ -0,0 +1,79 @@ +# copied from python-2.7.3's traceback.py +# CHANGES: +# - some_str is replaced, trying to create unicode strings +# +import types + +def format_exception_only(etype, value): + """Format the exception part of a traceback. + + The arguments are the exception type and value such as given by + sys.last_type and sys.last_value. The return value is a list of + strings, each ending in a newline. + + Normally, the list contains a single string; however, for + SyntaxError exceptions, it contains several lines that (when + printed) display detailed information about where the syntax + error occurred. + + The message indicating which exception occurred is always the last + string in the list. + + """ + + # An instance should not have a meaningful value parameter, but + # sometimes does, particularly for string exceptions, such as + # >>> raise string1, string2 # deprecated + # + # Clear these out first because issubtype(string1, SyntaxError) + # would throw another exception and mask the original problem. + if (isinstance(etype, BaseException) or + isinstance(etype, types.InstanceType) or + etype is None or type(etype) is str): + return [_format_final_exc_line(etype, value)] + + stype = etype.__name__ + + if not issubclass(etype, SyntaxError): + return [_format_final_exc_line(stype, value)] + + # It was a syntax error; show exactly where the problem was found. + lines = [] + try: + msg, (filename, lineno, offset, badline) = value.args + except Exception: + pass + else: + filename = filename or "" + lines.append(' File "%s", line %d\n' % (filename, lineno)) + if badline is not None: + lines.append(' %s\n' % badline.strip()) + if offset is not None: + caretspace = badline.rstrip('\n')[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c.isspace() and c or ' ') for c in caretspace) + # only three spaces to account for offset1 == pos 0 + lines.append(' %s^\n' % ''.join(caretspace)) + value = msg + + lines.append(_format_final_exc_line(stype, value)) + return lines + +def _format_final_exc_line(etype, value): + """Return a list of a single line -- normal case for format_exception_only""" + valuestr = _some_str(value) + if value is None or not valuestr: + line = "%s\n" % etype + else: + line = "%s: %s\n" % (etype, valuestr) + return line + +def _some_str(value): + try: + return unicode(value) + except Exception: + try: + return str(value) + except Exception: + pass + return '' % type(value).__name__ diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py new file mode 100644 index 000000000..bc68aac55 --- /dev/null +++ b/_pytest/_code/code.py @@ -0,0 +1,795 @@ +import sys +from inspect import CO_VARARGS, CO_VARKEYWORDS + +import py + +builtin_repr = repr + +reprlib = py.builtin._tryimport('repr', 'reprlib') + +if sys.version_info[0] >= 3: + from traceback import format_exception_only +else: + from ._py2traceback import format_exception_only + +class Code(object): + """ wrapper around Python code objects """ + def __init__(self, rawcode): + if not hasattr(rawcode, "co_filename"): + rawcode = getrawcode(rawcode) + try: + self.filename = rawcode.co_filename + self.firstlineno = rawcode.co_firstlineno - 1 + self.name = rawcode.co_name + except AttributeError: + raise TypeError("not a code object: %r" %(rawcode,)) + self.raw = rawcode + + def __eq__(self, other): + return self.raw == other.raw + + def __ne__(self, other): + return not self == other + + @property + def path(self): + """ return a path object pointing to source code (note that it + might not point to an actually existing file). """ + p = py.path.local(self.raw.co_filename) + # maybe don't try this checking + if not p.check(): + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? + p = self.raw.co_filename + return p + + @property + def fullsource(self): + """ return a _pytest._code.Source object for the full source file of the code + """ + from _pytest._code import source + full, _ = source.findsource(self.raw) + return full + + def source(self): + """ return a _pytest._code.Source object for the code object's source only + """ + # return source only for that part of code + import _pytest._code + return _pytest._code.Source(self.raw) + + def getargs(self, var=False): + """ return a tuple with the argument names for the code object + + if 'var' is set True also return the names of the variable and + keyword arguments when present + """ + # handfull shortcut for getting args + raw = self.raw + argcount = raw.co_argcount + if var: + argcount += raw.co_flags & CO_VARARGS + argcount += raw.co_flags & CO_VARKEYWORDS + return raw.co_varnames[:argcount] + +class Frame(object): + """Wrapper around a Python frame holding f_locals and f_globals + in which expressions can be evaluated.""" + + def __init__(self, frame): + self.lineno = frame.f_lineno - 1 + self.f_globals = frame.f_globals + self.f_locals = frame.f_locals + self.raw = frame + self.code = Code(frame.f_code) + + @property + def statement(self): + """ statement this frame is at """ + import _pytest._code + if self.code.fullsource is None: + return _pytest._code.Source("") + return self.code.fullsource.getstatement(self.lineno) + + def eval(self, code, **vars): + """ evaluate 'code' in the frame + + 'vars' are optional additional local variables + + returns the result of the evaluation + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + return eval(code, self.f_globals, f_locals) + + def exec_(self, code, **vars): + """ exec 'code' in the frame + + 'vars' are optiona; additional local variables + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + py.builtin.exec_(code, self.f_globals, f_locals ) + + def repr(self, object): + """ return a 'safe' (non-recursive, one-line) string repr for 'object' + """ + return py.io.saferepr(object) + + def is_true(self, object): + return object + + def getargs(self, var=False): + """ return a list of tuples (name, value) for all arguments + + if 'var' is set True also include the variable and keyword + arguments when present + """ + retval = [] + for arg in self.code.getargs(var): + try: + retval.append((arg, self.f_locals[arg])) + except KeyError: + pass # this can occur when using Psyco + return retval + +class TracebackEntry(object): + """ a single entry in a traceback """ + + _repr_style = None + exprinfo = None + + def __init__(self, rawentry): + self._rawentry = rawentry + self.lineno = rawentry.tb_lineno - 1 + + def set_repr_style(self, mode): + assert mode in ("short", "long") + self._repr_style = mode + + @property + def frame(self): + import _pytest._code + return _pytest._code.Frame(self._rawentry.tb_frame) + + @property + def relline(self): + return self.lineno - self.frame.code.firstlineno + + def __repr__(self): + return "" %(self.frame.code.path, self.lineno+1) + + @property + def statement(self): + """ _pytest._code.Source object for the current statement """ + source = self.frame.code.fullsource + return source.getstatement(self.lineno) + + @property + def path(self): + """ path to the source code """ + return self.frame.code.path + + def getlocals(self): + return self.frame.f_locals + locals = property(getlocals, None, None, "locals of underlaying frame") + + def reinterpret(self): + """Reinterpret the failing statement and returns a detailed information + about what operations are performed.""" + from _pytest.assertion.reinterpret import reinterpret + if self.exprinfo is None: + source = py.builtin._totext(self.statement).strip() + x = reinterpret(source, self.frame, should_fail=True) + if not py.builtin._istext(x): + raise TypeError("interpret returned non-string %r" % (x,)) + self.exprinfo = x + return self.exprinfo + + def getfirstlinesource(self): + # on Jython this firstlineno can be -1 apparently + return max(self.frame.code.firstlineno, 0) + + def getsource(self, astcache=None): + """ return failing source code. """ + # we use the passed in astcache to not reparse asttrees + # within exception info printing + from _pytest._code.source import getstatementrange_ast + source = self.frame.code.fullsource + if source is None: + return None + key = astnode = None + if astcache is not None: + key = self.frame.code.path + if key is not None: + astnode = astcache.get(key, None) + start = self.getfirstlinesource() + try: + astnode, _, end = getstatementrange_ast(self.lineno, source, + astnode=astnode) + except SyntaxError: + end = self.lineno + 1 + else: + if key is not None: + astcache[key] = astnode + return source[start:end] + + source = property(getsource) + + def ishidden(self): + """ return True if the current frame has a var __tracebackhide__ + resolving to True + + mostly for internal use + """ + try: + return self.frame.f_locals['__tracebackhide__'] + except KeyError: + try: + return self.frame.f_globals['__tracebackhide__'] + except KeyError: + return False + + def __str__(self): + try: + fn = str(self.path) + except py.error.Error: + fn = '???' + name = self.frame.code.name + try: + line = str(self.statement).lstrip() + except KeyboardInterrupt: + raise + except: + line = "???" + return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line) + + def name(self): + return self.frame.code.raw.co_name + name = property(name, None, None, "co_name of underlaying code") + +class Traceback(list): + """ Traceback objects encapsulate and offer higher level + access to Traceback entries. + """ + Entry = TracebackEntry + def __init__(self, tb): + """ initialize from given python traceback object. """ + if hasattr(tb, 'tb_next'): + def f(cur): + while cur is not None: + yield self.Entry(cur) + cur = cur.tb_next + list.__init__(self, f(tb)) + else: + list.__init__(self, tb) + + def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): + """ return a Traceback instance wrapping part of this Traceback + + by provding any combination of path, lineno and firstlineno, the + first frame to start the to-be-returned traceback is determined + + this allows cutting the first part of a Traceback instance e.g. + for formatting reasons (removing some uninteresting bits that deal + with handling of the exception/traceback) + """ + for x in self: + code = x.frame.code + codepath = code.path + if ((path is None or codepath == path) and + (excludepath is None or not hasattr(codepath, 'relto') or + not codepath.relto(excludepath)) and + (lineno is None or x.lineno == lineno) and + (firstlineno is None or x.frame.code.firstlineno == firstlineno)): + return Traceback(x._rawentry) + return self + + def __getitem__(self, key): + val = super(Traceback, self).__getitem__(key) + if isinstance(key, type(slice(0))): + val = self.__class__(val) + return val + + def filter(self, fn=lambda x: not x.ishidden()): + """ return a Traceback instance with certain items removed + + fn is a function that gets a single argument, a TracebackItem + instance, and should return True when the item should be added + to the Traceback, False when not + + by default this removes all the TracebackItems which are hidden + (see ishidden() above) + """ + return Traceback(filter(fn, self)) + + def getcrashentry(self): + """ return last non-hidden traceback entry that lead + to the exception of a traceback. + """ + for i in range(-1, -len(self)-1, -1): + entry = self[i] + if not entry.ishidden(): + return entry + return self[-1] + + def recursionindex(self): + """ return the index of the frame/TracebackItem where recursion + originates if appropriate, None if no recursion occurred + """ + cache = {} + for i, entry in enumerate(self): + # id for the code.raw is needed to work around + # the strange metaprogramming in the decorator lib from pypi + # which generates code objects that have hash/value equality + #XXX needs a test + key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno + #print "checking for recursion at", key + l = cache.setdefault(key, []) + if l: + f = entry.frame + loc = f.f_locals + for otherloc in l: + if f.is_true(f.eval(co_equal, + __recursioncache_locals_1=loc, + __recursioncache_locals_2=otherloc)): + return i + l.append(entry.frame.f_locals) + return None + +co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', + '?', 'eval') + +class ExceptionInfo(object): + """ wraps sys.exc_info() objects and offers + help for navigating the traceback. + """ + _striptext = '' + def __init__(self, tup=None, exprinfo=None): + import _pytest._code + if tup is None: + tup = sys.exc_info() + if exprinfo is None and isinstance(tup[1], AssertionError): + exprinfo = getattr(tup[1], 'msg', None) + if exprinfo is None: + exprinfo = str(tup[1]) + if exprinfo and exprinfo.startswith('assert '): + self._striptext = 'AssertionError: ' + self._excinfo = tup + #: the exception class + self.type = tup[0] + #: the exception instance + self.value = tup[1] + #: the exception raw traceback + self.tb = tup[2] + #: the exception type name + self.typename = self.type.__name__ + #: the exception traceback (_pytest._code.Traceback instance) + self.traceback = _pytest._code.Traceback(self.tb) + + def __repr__(self): + return "" % (self.typename, len(self.traceback)) + + def exconly(self, tryshort=False): + """ return the exception as a string + + when 'tryshort' resolves to True, and the exception is a + _pytest._code._AssertionError, only the actual exception part of + the exception representation is returned (so 'AssertionError: ' is + removed from the beginning) + """ + lines = format_exception_only(self.type, self.value) + text = ''.join(lines) + text = text.rstrip() + if tryshort: + if text.startswith(self._striptext): + text = text[len(self._striptext):] + return text + + def errisinstance(self, exc): + """ return True if the exception is an instance of exc """ + return isinstance(self.value, exc) + + def _getreprcrash(self): + exconly = self.exconly(tryshort=True) + entry = self.traceback.getcrashentry() + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + return ReprFileLocation(path, lineno+1, exconly) + + def getrepr(self, showlocals=False, style="long", + abspath=False, tbfilter=True, funcargs=False): + """ return str()able representation of this exception info. + showlocals: show locals per traceback entry + style: long|short|no|native traceback style + tbfilter: hide entries (where __tracebackhide__ is true) + + in case of style==native, tbfilter and showlocals is ignored. + """ + if style == 'native': + return ReprExceptionInfo(ReprTracebackNative( + py.std.traceback.format_exception( + self.type, + self.value, + self.traceback[0]._rawentry, + )), self._getreprcrash()) + + fmt = FormattedExcinfo(showlocals=showlocals, style=style, + abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) + return fmt.repr_excinfo(self) + + def __str__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return str(loc) + + def __unicode__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return unicode(loc) + + +class FormattedExcinfo(object): + """ presenting information about failing Functions and Generators. """ + # for traceback entries + flow_marker = ">" + fail_marker = "E" + + def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True, funcargs=False): + self.showlocals = showlocals + self.style = style + self.tbfilter = tbfilter + self.funcargs = funcargs + self.abspath = abspath + self.astcache = {} + + def _getindent(self, source): + # figure out indent for given source + try: + s = str(source.getstatement(len(source)-1)) + except KeyboardInterrupt: + raise + except: + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise + except: + return 0 + return 4 + (len(s) - len(s.lstrip())) + + def _getentrysource(self, entry): + source = entry.getsource(self.astcache) + if source is not None: + source = source.deindent() + return source + + def _saferepr(self, obj): + return py.io.saferepr(obj) + + def repr_args(self, entry): + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(var=True): + args.append((argname, self._saferepr(argvalue))) + return ReprFuncArgs(args) + + def get_source(self, source, line_index=-1, excinfo=None, short=False): + """ return formatted and marked up source lines. """ + import _pytest._code + lines = [] + if source is None or line_index >= len(source.lines): + source = _pytest._code.Source("???") + line_index = 0 + if line_index < 0: + line_index += len(source) + space_prefix = " " + if short: + lines.append(space_prefix + source.lines[line_index].strip()) + else: + for line in source.lines[:line_index]: + lines.append(space_prefix + line) + lines.append(self.flow_marker + " " + source.lines[line_index]) + for line in source.lines[line_index+1:]: + lines.append(space_prefix + line) + if excinfo is not None: + indent = 4 if short else self._getindent(source) + lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) + return lines + + def get_exconly(self, excinfo, indent=4, markall=False): + lines = [] + indent = " " * indent + # get the real exception information out + exlines = excinfo.exconly(tryshort=True).split('\n') + failindent = self.fail_marker + indent[1:] + for line in exlines: + lines.append(failindent + line) + if not markall: + failindent = indent + return lines + + def repr_locals(self, locals): + if self.showlocals: + lines = [] + keys = [loc for loc in locals if loc[0] != "@"] + keys.sort() + for name in keys: + value = locals[name] + if name == '__builtins__': + lines.append("__builtins__ = ") + else: + # This formatting could all be handled by the + # _repr() function, which is only reprlib.Repr in + # disguise, so is very configurable. + str_repr = self._saferepr(value) + #if len(str_repr) < 70 or not isinstance(value, + # (list, tuple, dict)): + lines.append("%-10s = %s" %(name, str_repr)) + #else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # py.std.pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) + + def repr_traceback_entry(self, entry, excinfo=None): + import _pytest._code + source = self._getentrysource(entry) + if source is None: + source = _pytest._code.Source("???") + line_index = 0 + else: + # entry.getfirstlinesource() can be -1, should be 0 on jython + line_index = entry.lineno - max(entry.getfirstlinesource(), 0) + + lines = [] + style = entry._repr_style + if style is None: + style = self.style + if style in ("short", "long"): + short = style == "short" + reprargs = self.repr_args(entry) if not short else None + s = self.get_source(source, line_index, excinfo, short=short) + lines.extend(s) + if short: + message = "in %s" %(entry.name) + else: + message = excinfo and excinfo.typename or "" + path = self._makepath(entry.path) + filelocrepr = ReprFileLocation(path, entry.lineno+1, message) + localsrepr = None + if not short: + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, style) + + def _makepath(self, path): + if not self.abspath: + try: + np = py.path.local().bestrelpath(path) + except OSError: + return path + if len(np) < len(str(path)): + path = np + return path + + def repr_traceback(self, excinfo): + traceback = excinfo.traceback + if self.tbfilter: + traceback = traceback.filter() + recursionindex = None + if excinfo.errisinstance(RuntimeError): + if "maximum recursion depth exceeded" in str(excinfo.value): + recursionindex = traceback.recursionindex() + last = traceback[-1] + entries = [] + extraline = None + for index, entry in enumerate(traceback): + einfo = (last == entry) and excinfo or None + reprentry = self.repr_traceback_entry(entry, einfo) + entries.append(reprentry) + if index == recursionindex: + extraline = "!!! Recursion detected (same locals & position)" + break + return ReprTraceback(entries, extraline, style=self.style) + + def repr_excinfo(self, excinfo): + reprtraceback = self.repr_traceback(excinfo) + reprcrash = excinfo._getreprcrash() + return ReprExceptionInfo(reprtraceback, reprcrash) + +class TerminalRepr: + def __str__(self): + s = self.__unicode__() + if sys.version_info[0] < 3: + s = s.encode('utf-8') + return s + + def __unicode__(self): + # FYI this is called from pytest-xdist's serialization of exception + # information. + io = py.io.TextIO() + tw = py.io.TerminalWriter(file=io) + self.toterminal(tw) + return io.getvalue().strip() + + def __repr__(self): + return "<%s instance at %0x>" %(self.__class__, id(self)) + + +class ReprExceptionInfo(TerminalRepr): + def __init__(self, reprtraceback, reprcrash): + self.reprtraceback = reprtraceback + self.reprcrash = reprcrash + self.sections = [] + + def addsection(self, name, content, sep="-"): + self.sections.append((name, content, sep)) + + def toterminal(self, tw): + self.reprtraceback.toterminal(tw) + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + +class ReprTraceback(TerminalRepr): + entrysep = "_ " + + def __init__(self, reprentries, extraline, style): + self.reprentries = reprentries + self.extraline = extraline + self.style = style + + def toterminal(self, tw): + # the entries might have different styles + for i, entry in enumerate(self.reprentries): + if entry.style == "long": + tw.line("") + entry.toterminal(tw) + if i < len(self.reprentries) - 1: + next_entry = self.reprentries[i+1] + if entry.style == "long" or \ + entry.style == "short" and next_entry.style == "long": + tw.sep(self.entrysep) + + if self.extraline: + tw.line(self.extraline) + +class ReprTracebackNative(ReprTraceback): + def __init__(self, tblines): + self.style = "native" + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = None + +class ReprEntryNative(TerminalRepr): + style = "native" + + def __init__(self, tblines): + self.lines = tblines + + def toterminal(self, tw): + tw.write("".join(self.lines)) + +class ReprEntry(TerminalRepr): + localssep = "_ " + + def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): + self.lines = lines + self.reprfuncargs = reprfuncargs + self.reprlocals = reprlocals + self.reprfileloc = filelocrepr + self.style = style + + def toterminal(self, tw): + if self.style == "short": + self.reprfileloc.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + #tw.line("") + return + if self.reprfuncargs: + self.reprfuncargs.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + if self.reprlocals: + #tw.sep(self.localssep, "Locals") + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + if self.lines: + tw.line("") + self.reprfileloc.toterminal(tw) + + def __str__(self): + return "%s\n%s\n%s" % ("\n".join(self.lines), + self.reprlocals, + self.reprfileloc) + +class ReprFileLocation(TerminalRepr): + def __init__(self, path, lineno, message): + self.path = str(path) + self.lineno = lineno + self.message = message + + def toterminal(self, tw): + # filename and lineno output for each entry, + # using an output format that most editors unterstand + msg = self.message + i = msg.find("\n") + if i != -1: + msg = msg[:i] + tw.line("%s:%s: %s" %(self.path, self.lineno, msg)) + +class ReprLocals(TerminalRepr): + def __init__(self, lines): + self.lines = lines + + def toterminal(self, tw): + for line in self.lines: + tw.line(line) + +class ReprFuncArgs(TerminalRepr): + def __init__(self, args): + self.args = args + + def toterminal(self, tw): + if self.args: + linesofar = "" + for name, value in self.args: + ns = "%s = %s" %(name, value) + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + else: + if linesofar: + linesofar += ", " + ns + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") + + + +oldbuiltins = {} + +def patch_builtins(assertion=True, compile=True): + """ put compile and AssertionError builtins to Python's builtins. """ + if assertion: + from _pytest.assertion import reinterpret + l = oldbuiltins.setdefault('AssertionError', []) + l.append(py.builtin.builtins.AssertionError) + py.builtin.builtins.AssertionError = reinterpret.AssertionError + if compile: + import _pytest._code + l = oldbuiltins.setdefault('compile', []) + l.append(py.builtin.builtins.compile) + py.builtin.builtins.compile = _pytest._code.compile + +def unpatch_builtins(assertion=True, compile=True): + """ remove compile and AssertionError builtins from Python builtins. """ + if assertion: + py.builtin.builtins.AssertionError = oldbuiltins['AssertionError'].pop() + if compile: + py.builtin.builtins.compile = oldbuiltins['compile'].pop() + +def getrawcode(obj, trycall=True): + """ return code object for given function. """ + try: + return obj.__code__ + except AttributeError: + obj = getattr(obj, 'im_func', obj) + obj = getattr(obj, 'func_code', obj) + obj = getattr(obj, 'f_code', obj) + obj = getattr(obj, '__code__', obj) + if trycall and not hasattr(obj, 'co_firstlineno'): + if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj): + x = getrawcode(obj.__call__, trycall=False) + if hasattr(x, 'co_firstlineno'): + return x + return obj + diff --git a/_pytest/_code/source.py b/_pytest/_code/source.py new file mode 100644 index 000000000..a1521f8a2 --- /dev/null +++ b/_pytest/_code/source.py @@ -0,0 +1,421 @@ +from __future__ import generators + +from bisect import bisect_right +import sys +import inspect, tokenize +import py +from types import ModuleType +cpy_compile = compile + +try: + import _ast + from _ast import PyCF_ONLY_AST as _AST_FLAG +except ImportError: + _AST_FLAG = 0 + _ast = None + + +class Source(object): + """ a immutable object holding a source code fragment, + possibly deindenting it. + """ + _compilecounter = 0 + def __init__(self, *parts, **kwargs): + self.lines = lines = [] + de = kwargs.get('deindent', True) + rstrip = kwargs.get('rstrip', True) + for part in parts: + if not part: + partlines = [] + if isinstance(part, Source): + partlines = part.lines + elif isinstance(part, (tuple, list)): + partlines = [x.rstrip("\n") for x in part] + elif isinstance(part, py.builtin._basestring): + partlines = part.split('\n') + if rstrip: + while partlines: + if partlines[-1].strip(): + break + partlines.pop() + else: + partlines = getsource(part, deindent=de).lines + if de: + partlines = deindent(partlines) + lines.extend(partlines) + + def __eq__(self, other): + try: + return self.lines == other.lines + except AttributeError: + if isinstance(other, str): + return str(self) == other + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self.lines[key] + else: + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + return self.__getslice__(key.start, key.stop) + + def __len__(self): + return len(self.lines) + + def __getslice__(self, start, end): + newsource = Source() + newsource.lines = self.lines[start:end] + return newsource + + def strip(self): + """ return new source object with trailing + and leading blank lines removed. + """ + start, end = 0, len(self) + while start < end and not self.lines[start].strip(): + start += 1 + while end > start and not self.lines[end-1].strip(): + end -= 1 + source = Source() + source.lines[:] = self.lines[start:end] + return source + + def putaround(self, before='', after='', indent=' ' * 4): + """ return a copy of the source object with + 'before' and 'after' wrapped around it. + """ + before = Source(before) + after = Source(after) + newsource = Source() + lines = [ (indent + line) for line in self.lines] + newsource.lines = before.lines + lines + after.lines + return newsource + + def indent(self, indent=' ' * 4): + """ return a copy of the source object with + all lines indented by the given indent-string. + """ + newsource = Source() + newsource.lines = [(indent+line) for line in self.lines] + return newsource + + def getstatement(self, lineno, assertion=False): + """ return Source statement which contains the + given linenumber (counted from 0). + """ + start, end = self.getstatementrange(lineno, assertion) + return self[start:end] + + def getstatementrange(self, lineno, assertion=False): + """ return (start, end) tuple which spans the minimal + statement region which containing the given lineno. + """ + if not (0 <= lineno < len(self)): + raise IndexError("lineno out of range") + ast, start, end = getstatementrange_ast(lineno, self) + return start, end + + def deindent(self, offset=None): + """ return a new source object deindented by offset. + If offset is None then guess an indentation offset from + the first non-blank line. Subsequent lines which have a + lower indentation offset will be copied verbatim as + they are assumed to be part of multilines. + """ + # XXX maybe use the tokenizer to properly handle multiline + # strings etc.pp? + newsource = Source() + newsource.lines[:] = deindent(self.lines, offset) + return newsource + + def isparseable(self, deindent=True): + """ return True if source is parseable, heuristically + deindenting it by default. + """ + try: + import parser + except ImportError: + syntax_checker = lambda x: compile(x, 'asd', 'exec') + else: + syntax_checker = parser.suite + + if deindent: + source = str(self.deindent()) + else: + source = str(self) + try: + #compile(source+'\n', "x", "exec") + syntax_checker(source+'\n') + except KeyboardInterrupt: + raise + except Exception: + return False + else: + return True + + def __str__(self): + return "\n".join(self.lines) + + def compile(self, filename=None, mode='exec', + flag=generators.compiler_flag, + dont_inherit=0, _genframe=None): + """ return compiled code object. if filename is None + invent an artificial filename which displays + the source/line position of the caller frame. + """ + if not filename or py.path.local(filename).check(file=0): + if _genframe is None: + _genframe = sys._getframe(1) # the caller + fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno + base = "<%d-codegen " % self._compilecounter + self.__class__._compilecounter += 1 + if not filename: + filename = base + '%s:%d>' % (fn, lineno) + else: + filename = base + '%r %s:%d>' % (filename, fn, lineno) + source = "\n".join(self.lines) + '\n' + try: + co = cpy_compile(source, filename, mode, flag) + except SyntaxError: + ex = sys.exc_info()[1] + # re-represent syntax errors from parsing python strings + msglines = self.lines[:ex.lineno] + if ex.offset: + msglines.append(" "*ex.offset + '^') + msglines.append("(code was compiled probably from here: %s)" % filename) + newex = SyntaxError('\n'.join(msglines)) + newex.offset = ex.offset + newex.lineno = ex.lineno + newex.text = ex.text + raise newex + else: + if flag & _AST_FLAG: + return co + lines = [(x + "\n") for x in self.lines] + if sys.version_info[0] >= 3: + # XXX py3's inspect.getsourcefile() checks for a module + # and a pep302 __loader__ ... we don't have a module + # at code compile-time so we need to fake it here + m = ModuleType("_pycodecompile_pseudo_module") + py.std.inspect.modulesbyfile[filename] = None + py.std.sys.modules[None] = m + m.__loader__ = 1 + py.std.linecache.cache[filename] = (1, None, lines, filename) + return co + +# +# public API shortcut functions +# + +def compile_(source, filename=None, mode='exec', flags= + generators.compiler_flag, dont_inherit=0): + """ compile the given source to a raw code object, + and maintain an internal cache which allows later + retrieval of the source code for the code object + and any recursively created code objects. + """ + if _ast is not None and isinstance(source, _ast.AST): + # XXX should Source support having AST? + return cpy_compile(source, filename, mode, flags, dont_inherit) + _genframe = sys._getframe(1) # the caller + s = Source(source) + co = s.compile(filename, mode, flags, _genframe=_genframe) + return co + + +def getfslineno(obj): + """ Return source location (path, lineno) for the given object. + If the source cannot be determined return ("", -1) + """ + import _pytest._code + try: + code = _pytest._code.Code(obj) + except TypeError: + try: + fn = (py.std.inspect.getsourcefile(obj) or + py.std.inspect.getfile(obj)) + except TypeError: + return "", -1 + + fspath = fn and py.path.local(fn) or None + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except IOError: + pass + else: + fspath = code.path + lineno = code.firstlineno + assert isinstance(lineno, int) + return fspath, lineno + +# +# helper functions +# + +def findsource(obj): + try: + sourcelines, lineno = py.std.inspect.findsource(obj) + except py.builtin._sysex: + raise + except: + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + return source, lineno + +def getsource(obj, **kwargs): + import _pytest._code + obj = _pytest._code.getrawcode(obj) + try: + strsrc = inspect.getsource(obj) + except IndentationError: + strsrc = "\"Buggy python version consider upgrading, cannot get source\"" + assert isinstance(strsrc, str) + return Source(strsrc, **kwargs) + +def deindent(lines, offset=None): + if offset is None: + for line in lines: + line = line.expandtabs() + s = line.lstrip() + if s: + offset = len(line)-len(s) + break + else: + offset = 0 + if offset == 0: + return list(lines) + newlines = [] + def readline_generator(lines): + for line in lines: + yield line + '\n' + while True: + yield '' + + it = readline_generator(lines) + + try: + for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)): + if sline > len(lines): + break # End of input reached + if sline > len(newlines): + line = lines[sline - 1].expandtabs() + if line.lstrip() and line[:offset].isspace(): + line = line[offset:] # Deindent + newlines.append(line) + + for i in range(sline, eline): + # Don't deindent continuing lines of + # multiline tokens (i.e. multiline strings) + newlines.append(lines[i]) + except (IndentationError, tokenize.TokenError): + pass + # Add any lines we didn't see. E.g. if an exception was raised. + newlines.extend(lines[len(newlines):]) + return newlines + + +def get_statement_startend2(lineno, node): + import ast + # flatten all statements and except handlers into one lineno-list + # AST's line numbers start indexing at 1 + l = [] + for x in ast.walk(node): + if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler): + l.append(x.lineno - 1) + for name in "finalbody", "orelse": + val = getattr(x, name, None) + if val: + # treat the finally/orelse part as its own statement + l.append(val[0].lineno - 1 - 1) + l.sort() + insert_index = bisect_right(l, lineno) + start = l[insert_index - 1] + if insert_index >= len(l): + end = None + else: + end = l[insert_index] + return start, end + + +def getstatementrange_ast(lineno, source, assertion=False, astnode=None): + if astnode is None: + content = str(source) + if sys.version_info < (2,7): + content += "\n" + try: + astnode = compile(content, "source", "exec", 1024) # 1024 for AST + except ValueError: + start, end = getstatementrange_old(lineno, source, assertion) + return None, start, end + start, end = get_statement_startend2(lineno, astnode) + # we need to correct the end: + # - ast-parsing strips comments + # - there might be empty lines + # - we might have lesser indented code blocks at the end + if end is None: + end = len(source.lines) + + if end > start + 1: + # make sure we don't span differently indented code blocks + # by using the BlockFinder helper used which inspect.getsource() uses itself + block_finder = inspect.BlockFinder() + # if we start with an indented line, put blockfinder to "started" mode + block_finder.started = source.lines[start][0].isspace() + it = ((x + "\n") for x in source.lines[start:end]) + try: + for tok in tokenize.generate_tokens(lambda: next(it)): + block_finder.tokeneater(*tok) + except (inspect.EndOfBlock, IndentationError): + end = block_finder.last + start + except Exception: + pass + + # the end might still point to a comment or empty line, correct it + while end: + line = source.lines[end - 1].lstrip() + if line.startswith("#") or not line: + end -= 1 + else: + break + return astnode, start, end + + +def getstatementrange_old(lineno, source, assertion=False): + """ return (start, end) tuple which spans the minimal + statement region which containing the given lineno. + raise an IndexError if no such statementrange can be found. + """ + # XXX this logic is only used on python2.4 and below + # 1. find the start of the statement + from codeop import compile_command + for start in range(lineno, -1, -1): + if assertion: + line = source.lines[start] + # the following lines are not fully tested, change with care + if 'super' in line and 'self' in line and '__init__' in line: + raise IndexError("likely a subclass") + if "assert" not in line and "raise" not in line: + continue + trylines = source.lines[start:lineno+1] + # quick hack to prepare parsing an indented line with + # compile_command() (which errors on "return" outside defs) + trylines.insert(0, 'def xxx():') + trysource = '\n '.join(trylines) + # ^ space here + try: + compile_command(trysource) + except (SyntaxError, OverflowError, ValueError): + continue + + # 2. find the end of the statement + for end in range(lineno+1, len(source)+1): + trysource = source[start:end] + if trysource.isparseable(): + return start, end + raise SyntaxError("no valid source range around line %d " % (lineno,)) + + diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index 865b33c8c..6921deb2a 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -2,6 +2,7 @@ support for presenting detailed information in failing assertions. """ import py +import os import sys from _pytest.monkeypatch import monkeypatch from _pytest.assertion import util @@ -86,6 +87,12 @@ def pytest_collection(session): hook.set_session(session) +def _running_on_ci(): + """Check if we're currently running on a CI system.""" + env_vars = ['CI', 'BUILD_NUMBER'] + return any(var in os.environ for var in env_vars) + + def pytest_runtest_setup(item): """Setup the pytest_assertrepr_compare hook @@ -99,7 +106,8 @@ def pytest_runtest_setup(item): This uses the first result from the hook and then ensures the following: - * Overly verbose explanations are dropped unles -vv was used. + * Overly verbose explanations are dropped unless -vv was used or + running on a CI. * Embedded newlines are escaped to help util.format_explanation() later. * If the rewrite mode is used embedded %-characters are replaced @@ -113,7 +121,8 @@ def pytest_runtest_setup(item): for new_expl in hook_result: if new_expl: if (sum(len(p) for p in new_expl[1:]) > 80*8 and - item.config.option.verbose < 2): + item.config.option.verbose < 2 and + not _running_on_ci()): show_max = 10 truncated_lines = len(new_expl) - show_max new_expl[show_max:] = [py.builtin._totext( diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py deleted file mode 100644 index d8e741162..000000000 --- a/_pytest/assertion/newinterpret.py +++ /dev/null @@ -1,365 +0,0 @@ -""" -Find intermediate evalutation results in assert statements through builtin AST. -This should replace oldinterpret.py eventually. -""" - -import sys -import ast - -import py -from _pytest.assertion import util -from _pytest.assertion.reinterpret import BuiltinAssertionError - - -if sys.platform.startswith("java"): - # See http://bugs.jython.org/issue1497 - _exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict", - "ListComp", "GeneratorExp", "Yield", "Compare", "Call", - "Repr", "Num", "Str", "Attribute", "Subscript", "Name", - "List", "Tuple") - _stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign", - "AugAssign", "Print", "For", "While", "If", "With", "Raise", - "TryExcept", "TryFinally", "Assert", "Import", "ImportFrom", - "Exec", "Global", "Expr", "Pass", "Break", "Continue") - _expr_nodes = set(getattr(ast, name) for name in _exprs) - _stmt_nodes = set(getattr(ast, name) for name in _stmts) - def _is_ast_expr(node): - return node.__class__ in _expr_nodes - def _is_ast_stmt(node): - return node.__class__ in _stmt_nodes -else: - def _is_ast_expr(node): - return isinstance(node, ast.expr) - def _is_ast_stmt(node): - return isinstance(node, ast.stmt) - -try: - _Starred = ast.Starred -except AttributeError: - # Python 2. Define a dummy class so isinstance() will always be False. - class _Starred(object): pass - - -class Failure(Exception): - """Error found while interpreting AST.""" - - def __init__(self, explanation=""): - self.cause = sys.exc_info() - self.explanation = explanation - - -def interpret(source, frame, should_fail=False): - mod = ast.parse(source) - visitor = DebugInterpreter(frame) - try: - visitor.visit(mod) - except Failure: - failure = sys.exc_info()[1] - return getfailure(failure) - if should_fail: - return ("(assertion failed, but when it was re-run for " - "printing intermediate values, it did not fail. Suggestions: " - "compute assert expression before the assert or use --assert=plain)") - -def run(offending_line, frame=None): - if frame is None: - frame = py.code.Frame(sys._getframe(1)) - return interpret(offending_line, frame) - -def getfailure(e): - explanation = util.format_explanation(e.explanation) - value = e.cause[1] - if str(value): - lines = explanation.split('\n') - lines[0] += " << %s" % (value,) - explanation = '\n'.join(lines) - text = "%s: %s" % (e.cause[0].__name__, explanation) - if text.startswith('AssertionError: assert '): - text = text[16:] - return text - -operator_map = { - ast.BitOr : "|", - ast.BitXor : "^", - ast.BitAnd : "&", - ast.LShift : "<<", - ast.RShift : ">>", - ast.Add : "+", - ast.Sub : "-", - ast.Mult : "*", - ast.Div : "/", - ast.FloorDiv : "//", - ast.Mod : "%", - ast.Eq : "==", - ast.NotEq : "!=", - ast.Lt : "<", - ast.LtE : "<=", - ast.Gt : ">", - ast.GtE : ">=", - ast.Pow : "**", - ast.Is : "is", - ast.IsNot : "is not", - ast.In : "in", - ast.NotIn : "not in" -} - -unary_map = { - ast.Not : "not %s", - ast.Invert : "~%s", - ast.USub : "-%s", - ast.UAdd : "+%s" -} - - -class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information. """ - - def __init__(self, frame): - self.frame = frame - - def generic_visit(self, node): - # Fallback when we don't have a special implementation. - if _is_ast_expr(node): - mod = ast.Expression(node) - co = self._compile(mod) - try: - result = self.frame.eval(co) - except Exception: - raise Failure() - explanation = self.frame.repr(result) - return explanation, result - elif _is_ast_stmt(node): - mod = ast.Module([node]) - co = self._compile(mod, "exec") - try: - self.frame.exec_(co) - except Exception: - raise Failure() - return None, None - else: - raise AssertionError("can't handle %s" %(node,)) - - def _compile(self, source, mode="eval"): - return compile(source, "", mode) - - def visit_Expr(self, expr): - return self.visit(expr.value) - - def visit_Module(self, mod): - for stmt in mod.body: - self.visit(stmt) - - def visit_Name(self, name): - explanation, result = self.generic_visit(name) - # See if the name is local. - source = "%r in locals() is not globals()" % (name.id,) - co = self._compile(source) - try: - local = self.frame.eval(co) - except Exception: - # have to assume it isn't - local = None - if local is None or not self.frame.is_true(local): - return name.id, result - return explanation, result - - def visit_Compare(self, comp): - left = comp.left - left_explanation, left_result = self.visit(left) - for op, next_op in zip(comp.ops, comp.comparators): - next_explanation, next_result = self.visit(next_op) - op_symbol = operator_map[op.__class__] - explanation = "%s %s %s" % (left_explanation, op_symbol, - next_explanation) - source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_left=left_result, - __exprinfo_right=next_result) - except Exception: - raise Failure(explanation) - try: - if not self.frame.is_true(result): - break - except KeyboardInterrupt: - raise - except: - break - left_explanation, left_result = next_explanation, next_result - - if util._reprcompare is not None: - res = util._reprcompare(op_symbol, left_result, next_result) - if res: - explanation = res - return explanation, result - - def visit_BoolOp(self, boolop): - is_or = isinstance(boolop.op, ast.Or) - explanations = [] - for operand in boolop.values: - explanation, result = self.visit(operand) - explanations.append(explanation) - if result == is_or: - break - name = is_or and " or " or " and " - explanation = "(" + name.join(explanations) + ")" - return explanation, result - - def visit_UnaryOp(self, unary): - pattern = unary_map[unary.op.__class__] - operand_explanation, operand_result = self.visit(unary.operand) - explanation = pattern % (operand_explanation,) - co = self._compile(pattern % ("__exprinfo_expr",)) - try: - result = self.frame.eval(co, __exprinfo_expr=operand_result) - except Exception: - raise Failure(explanation) - return explanation, result - - def visit_BinOp(self, binop): - left_explanation, left_result = self.visit(binop.left) - right_explanation, right_result = self.visit(binop.right) - symbol = operator_map[binop.op.__class__] - explanation = "(%s %s %s)" % (left_explanation, symbol, - right_explanation) - source = "__exprinfo_left %s __exprinfo_right" % (symbol,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_left=left_result, - __exprinfo_right=right_result) - except Exception: - raise Failure(explanation) - return explanation, result - - def visit_Call(self, call): - func_explanation, func = self.visit(call.func) - arg_explanations = [] - ns = {"__exprinfo_func" : func} - arguments = [] - for arg in call.args: - arg_explanation, arg_result = self.visit(arg) - if isinstance(arg, _Starred): - arg_name = "__exprinfo_star" - ns[arg_name] = arg_result - arguments.append("*%s" % (arg_name,)) - arg_explanations.append("*%s" % (arg_explanation,)) - else: - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - arguments.append(arg_name) - arg_explanations.append(arg_explanation) - for keyword in call.keywords: - arg_explanation, arg_result = self.visit(keyword.value) - if keyword.arg: - arg_name = "__exprinfo_%s" % (len(ns),) - keyword_source = "%s=%%s" % (keyword.arg) - arguments.append(keyword_source % (arg_name,)) - arg_explanations.append(keyword_source % (arg_explanation,)) - else: - arg_name = "__exprinfo_kwds" - arguments.append("**%s" % (arg_name,)) - arg_explanations.append("**%s" % (arg_explanation,)) - - ns[arg_name] = arg_result - - if getattr(call, 'starargs', None): - arg_explanation, arg_result = self.visit(call.starargs) - arg_name = "__exprinfo_star" - ns[arg_name] = arg_result - arguments.append("*%s" % (arg_name,)) - arg_explanations.append("*%s" % (arg_explanation,)) - - if getattr(call, 'kwargs', None): - arg_explanation, arg_result = self.visit(call.kwargs) - arg_name = "__exprinfo_kwds" - ns[arg_name] = arg_result - arguments.append("**%s" % (arg_name,)) - arg_explanations.append("**%s" % (arg_explanation,)) - args_explained = ", ".join(arg_explanations) - explanation = "%s(%s)" % (func_explanation, args_explained) - args = ", ".join(arguments) - source = "__exprinfo_func(%s)" % (args,) - co = self._compile(source) - try: - result = self.frame.eval(co, **ns) - except Exception: - raise Failure(explanation) - pattern = "%s\n{%s = %s\n}" - rep = self.frame.repr(result) - explanation = pattern % (rep, rep, explanation) - return explanation, result - - def _is_builtin_name(self, name): - pattern = "%r not in globals() and %r not in locals()" - source = pattern % (name.id, name.id) - co = self._compile(source) - try: - return self.frame.eval(co) - except Exception: - return False - - def visit_Attribute(self, attr): - if not isinstance(attr.ctx, ast.Load): - return self.generic_visit(attr) - source_explanation, source_result = self.visit(attr.value) - explanation = "%s.%s" % (source_explanation, attr.attr) - source = "__exprinfo_expr.%s" % (attr.attr,) - co = self._compile(source) - try: - try: - result = self.frame.eval(co, __exprinfo_expr=source_result) - except AttributeError: - # Maybe the attribute name needs to be mangled? - if not attr.attr.startswith("__") or attr.attr.endswith("__"): - raise - source = "getattr(__exprinfo_expr.__class__, '__name__', '')" - co = self._compile(source) - class_name = self.frame.eval(co, __exprinfo_expr=source_result) - mangled_attr = "_" + class_name + attr.attr - source = "__exprinfo_expr.%s" % (mangled_attr,) - co = self._compile(source) - result = self.frame.eval(co, __exprinfo_expr=source_result) - except Exception: - raise Failure(explanation) - explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result), - self.frame.repr(result), - source_explanation, attr.attr) - # Check if the attr is from an instance. - source = "%r in getattr(__exprinfo_expr, '__dict__', {})" - source = source % (attr.attr,) - co = self._compile(source) - try: - from_instance = self.frame.eval(co, __exprinfo_expr=source_result) - except Exception: - from_instance = None - if from_instance is None or self.frame.is_true(from_instance): - rep = self.frame.repr(result) - pattern = "%s\n{%s = %s\n}" - explanation = pattern % (rep, rep, explanation) - return explanation, result - - def visit_Assert(self, assrt): - test_explanation, test_result = self.visit(assrt.test) - explanation = "assert %s" % (test_explanation,) - if not self.frame.is_true(test_result): - try: - raise BuiltinAssertionError - except Exception: - raise Failure(explanation) - return explanation, test_result - - def visit_Assign(self, assign): - value_explanation, value_result = self.visit(assign.value) - explanation = "... = %s" % (value_explanation,) - name = ast.Name("__exprinfo_expr", ast.Load(), - lineno=assign.value.lineno, - col_offset=assign.value.col_offset) - new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno, - col_offset=assign.col_offset) - mod = ast.Module([new_assign]) - co = self._compile(mod, "exec") - try: - self.frame.exec_(co, __exprinfo_expr=value_result) - except Exception: - raise Failure(explanation) - return explanation, value_result diff --git a/_pytest/assertion/oldinterpret.py b/_pytest/assertion/oldinterpret.py deleted file mode 100644 index 8ca1d8f5c..000000000 --- a/_pytest/assertion/oldinterpret.py +++ /dev/null @@ -1,566 +0,0 @@ -import traceback -import types -import py -import sys, inspect -from compiler import parse, ast, pycodegen -from _pytest.assertion.util import format_explanation, BuiltinAssertionError - -passthroughex = py.builtin._sysex - -class Failure: - def __init__(self, node): - self.exc, self.value, self.tb = sys.exc_info() - self.node = node - -class View(object): - """View base class. - - If C is a subclass of View, then C(x) creates a proxy object around - the object x. The actual class of the proxy is not C in general, - but a *subclass* of C determined by the rules below. To avoid confusion - we call view class the class of the proxy (a subclass of C, so of View) - and object class the class of x. - - Attributes and methods not found in the proxy are automatically read on x. - Other operations like setting attributes are performed on the proxy, as - determined by its view class. The object x is available from the proxy - as its __obj__ attribute. - - The view class selection is determined by the __view__ tuples and the - optional __viewkey__ method. By default, the selected view class is the - most specific subclass of C whose __view__ mentions the class of x. - If no such subclass is found, the search proceeds with the parent - object classes. For example, C(True) will first look for a subclass - of C with __view__ = (..., bool, ...) and only if it doesn't find any - look for one with __view__ = (..., int, ...), and then ..., object,... - If everything fails the class C itself is considered to be the default. - - Alternatively, the view class selection can be driven by another aspect - of the object x, instead of the class of x, by overriding __viewkey__. - See last example at the end of this module. - """ - - _viewcache = {} - __view__ = () - - def __new__(rootclass, obj, *args, **kwds): - self = object.__new__(rootclass) - self.__obj__ = obj - self.__rootclass__ = rootclass - key = self.__viewkey__() - try: - self.__class__ = self._viewcache[key] - except KeyError: - self.__class__ = self._selectsubclass(key) - return self - - def __getattr__(self, attr): - # attributes not found in the normal hierarchy rooted on View - # are looked up in the object's real class - return getattr(object.__getattribute__(self, '__obj__'), attr) - - def __viewkey__(self): - return self.__obj__.__class__ - - def __matchkey__(self, key, subclasses): - if inspect.isclass(key): - keys = inspect.getmro(key) - else: - keys = [key] - for key in keys: - result = [C for C in subclasses if key in C.__view__] - if result: - return result - return [] - - def _selectsubclass(self, key): - subclasses = list(enumsubclasses(self.__rootclass__)) - for C in subclasses: - if not isinstance(C.__view__, tuple): - C.__view__ = (C.__view__,) - choices = self.__matchkey__(key, subclasses) - if not choices: - return self.__rootclass__ - elif len(choices) == 1: - return choices[0] - else: - # combine the multiple choices - return type('?', tuple(choices), {}) - - def __repr__(self): - return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) - - -def enumsubclasses(cls): - for subcls in cls.__subclasses__(): - for subsubclass in enumsubclasses(subcls): - yield subsubclass - yield cls - - -class Interpretable(View): - """A parse tree node with a few extra methods.""" - explanation = None - - def is_builtin(self, frame): - return False - - def eval(self, frame): - # fall-back for unknown expression nodes - try: - expr = ast.Expression(self.__obj__) - expr.filename = '' - self.__obj__.filename = '' - co = pycodegen.ExpressionCodeGenerator(expr).getCode() - result = frame.eval(co) - except passthroughex: - raise - except: - raise Failure(self) - self.result = result - self.explanation = self.explanation or frame.repr(self.result) - - def run(self, frame): - # fall-back for unknown statement nodes - try: - expr = ast.Module(None, ast.Stmt([self.__obj__])) - expr.filename = '' - co = pycodegen.ModuleCodeGenerator(expr).getCode() - frame.exec_(co) - except passthroughex: - raise - except: - raise Failure(self) - - def nice_explanation(self): - return format_explanation(self.explanation) - - -class Name(Interpretable): - __view__ = ast.Name - - def is_local(self, frame): - source = '%r in locals() is not globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_global(self, frame): - source = '%r in globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_builtin(self, frame): - source = '%r not in locals() and %r not in globals()' % ( - self.name, self.name) - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - super(Name, self).eval(frame) - if not self.is_local(frame): - self.explanation = self.name - -class Compare(Interpretable): - __view__ = ast.Compare - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - for operation, expr2 in self.ops: - if hasattr(self, 'result'): - # shortcutting in chained expressions - if not frame.is_true(self.result): - break - expr2 = Interpretable(expr2) - expr2.eval(frame) - self.explanation = "%s %s %s" % ( - expr.explanation, operation, expr2.explanation) - source = "__exprinfo_left %s __exprinfo_right" % operation - try: - self.result = frame.eval(source, - __exprinfo_left=expr.result, - __exprinfo_right=expr2.result) - except passthroughex: - raise - except: - raise Failure(self) - expr = expr2 - -class And(Interpretable): - __view__ = ast.And - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if not frame.is_true(expr.result): - break - self.explanation = '(' + ' and '.join(explanations) + ')' - -class Or(Interpretable): - __view__ = ast.Or - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if frame.is_true(expr.result): - break - self.explanation = '(' + ' or '.join(explanations) + ')' - - -# == Unary operations == -keepalive = [] -for astclass, astpattern in { - ast.Not : 'not __exprinfo_expr', - ast.Invert : '(~__exprinfo_expr)', - }.items(): - - class UnaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - expr = Interpretable(self.expr) - expr.eval(frame) - self.explanation = astpattern.replace('__exprinfo_expr', - expr.explanation) - try: - self.result = frame.eval(astpattern, - __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(UnaryArith) - -# == Binary operations == -for astclass, astpattern in { - ast.Add : '(__exprinfo_left + __exprinfo_right)', - ast.Sub : '(__exprinfo_left - __exprinfo_right)', - ast.Mul : '(__exprinfo_left * __exprinfo_right)', - ast.Div : '(__exprinfo_left / __exprinfo_right)', - ast.Mod : '(__exprinfo_left % __exprinfo_right)', - ast.Power : '(__exprinfo_left ** __exprinfo_right)', - }.items(): - - class BinaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - left = Interpretable(self.left) - left.eval(frame) - right = Interpretable(self.right) - right.eval(frame) - self.explanation = (astpattern - .replace('__exprinfo_left', left .explanation) - .replace('__exprinfo_right', right.explanation)) - try: - self.result = frame.eval(astpattern, - __exprinfo_left=left.result, - __exprinfo_right=right.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(BinaryArith) - - -class CallFunc(Interpretable): - __view__ = ast.CallFunc - - def is_bool(self, frame): - source = 'isinstance(__exprinfo_value, bool)' - try: - return frame.is_true(frame.eval(source, - __exprinfo_value=self.result)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - node = Interpretable(self.node) - node.eval(frame) - explanations = [] - vars = {'__exprinfo_fn': node.result} - source = '__exprinfo_fn(' - for a in self.args: - if isinstance(a, ast.Keyword): - keyword = a.name - a = a.expr - else: - keyword = None - a = Interpretable(a) - a.eval(frame) - argname = '__exprinfo_%d' % len(vars) - vars[argname] = a.result - if keyword is None: - source += argname + ',' - explanations.append(a.explanation) - else: - source += '%s=%s,' % (keyword, argname) - explanations.append('%s=%s' % (keyword, a.explanation)) - if self.star_args: - star_args = Interpretable(self.star_args) - star_args.eval(frame) - argname = '__exprinfo_star' - vars[argname] = star_args.result - source += '*' + argname + ',' - explanations.append('*' + star_args.explanation) - if self.dstar_args: - dstar_args = Interpretable(self.dstar_args) - dstar_args.eval(frame) - argname = '__exprinfo_kwds' - vars[argname] = dstar_args.result - source += '**' + argname + ',' - explanations.append('**' + dstar_args.explanation) - self.explanation = "%s(%s)" % ( - node.explanation, ', '.join(explanations)) - if source.endswith(','): - source = source[:-1] - source += ')' - try: - self.result = frame.eval(source, **vars) - except passthroughex: - raise - except: - raise Failure(self) - if not node.is_builtin(frame) or not self.is_bool(frame): - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -class Getattr(Interpretable): - __view__ = ast.Getattr - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - source = '__exprinfo_expr.%s' % self.attrname - try: - try: - self.result = frame.eval(source, __exprinfo_expr=expr.result) - except AttributeError: - # Maybe the attribute name needs to be mangled? - if (not self.attrname.startswith("__") or - self.attrname.endswith("__")): - raise - source = "getattr(__exprinfo_expr.__class__, '__name__', '')" - class_name = frame.eval(source, __exprinfo_expr=expr.result) - mangled_attr = "_" + class_name + self.attrname - source = "__exprinfo_expr.%s" % (mangled_attr,) - self.result = frame.eval(source, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - self.explanation = '%s.%s' % (expr.explanation, self.attrname) - # if the attribute comes from the instance, its value is interesting - source = ('hasattr(__exprinfo_expr, "__dict__") and ' - '%r in __exprinfo_expr.__dict__' % self.attrname) - try: - from_instance = frame.is_true( - frame.eval(source, __exprinfo_expr=expr.result)) - except passthroughex: - raise - except: - from_instance = True - if from_instance: - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -# == Re-interpretation of full statements == - -class Assert(Interpretable): - __view__ = ast.Assert - - def run(self, frame): - test = Interpretable(self.test) - test.eval(frame) - # print the result as 'assert ' - self.result = test.result - self.explanation = 'assert ' + test.explanation - if not frame.is_true(test.result): - try: - raise BuiltinAssertionError - except passthroughex: - raise - except: - raise Failure(self) - -class Assign(Interpretable): - __view__ = ast.Assign - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = '... = ' + expr.explanation - # fall-back-run the rest of the assignment - ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr')) - mod = ast.Module(None, ast.Stmt([ass])) - mod.filename = '' - co = pycodegen.ModuleCodeGenerator(mod).getCode() - try: - frame.exec_(co, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - -class Discard(Interpretable): - __view__ = ast.Discard - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = expr.explanation - -class Stmt(Interpretable): - __view__ = ast.Stmt - - def run(self, frame): - for stmt in self.nodes: - stmt = Interpretable(stmt) - stmt.run(frame) - - -def report_failure(e): - explanation = e.node.nice_explanation() - if explanation: - explanation = ", in: " + explanation - else: - explanation = "" - sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation)) - -def check(s, frame=None): - if frame is None: - frame = sys._getframe(1) - frame = py.code.Frame(frame) - expr = parse(s, 'eval') - assert isinstance(expr, ast.Expression) - node = Interpretable(expr.node) - try: - node.eval(frame) - except passthroughex: - raise - except Failure: - e = sys.exc_info()[1] - report_failure(e) - else: - if not frame.is_true(node.result): - sys.stderr.write("assertion failed: %s\n" % node.nice_explanation()) - - -########################################################### -# API / Entry points -# ######################################################### - -def interpret(source, frame, should_fail=False): - module = Interpretable(parse(source, 'exec').node) - #print "got module", module - if isinstance(frame, types.FrameType): - frame = py.code.Frame(frame) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - return getfailure(e) - except passthroughex: - raise - except: - traceback.print_exc() - if should_fail: - return ("(assertion failed, but when it was re-run for " - "printing intermediate values, it did not fail. Suggestions: " - "compute assert expression before the assert or use --assert=plain)") - else: - return None - -def getmsg(excinfo): - if isinstance(excinfo, tuple): - excinfo = py.code.ExceptionInfo(excinfo) - #frame, line = gettbline(tb) - #frame = py.code.Frame(frame) - #return interpret(line, frame) - - tb = excinfo.traceback[-1] - source = str(tb.statement).strip() - x = interpret(source, tb.frame, should_fail=True) - if not isinstance(x, str): - raise TypeError("interpret returned non-string %r" % (x,)) - return x - -def getfailure(e): - explanation = e.node.nice_explanation() - if str(e.value): - lines = explanation.split('\n') - lines[0] += " << %s" % (e.value,) - explanation = '\n'.join(lines) - text = "%s: %s" % (e.exc.__name__, explanation) - if text.startswith('AssertionError: assert '): - text = text[16:] - return text - -def run(s, frame=None): - if frame is None: - frame = sys._getframe(1) - frame = py.code.Frame(frame) - module = Interpretable(parse(s, 'exec').node) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - report_failure(e) - - -if __name__ == '__main__': - # example: - def f(): - return 5 - - def g(): - return 3 - - def h(x): - return 'never' - - check("f() * g() == 5") - check("not f()") - check("not (f() and g() or 0)") - check("f() == g()") - i = 4 - check("i == f()") - check("len(f()) == 0") - check("isinstance(2+3+4, float)") - - run("x = i") - check("x == 5") - - run("assert not f(), 'oops'") - run("a, b, c = 1, 2") - run("a, b, c = f()") - - check("max([f(),g()]) == 4") - check("'hello'[g()] == 'h'") - run("'guk%d' % h(f())") diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py index dfb2fec93..f4262c3ac 100644 --- a/_pytest/assertion/reinterpret.py +++ b/_pytest/assertion/reinterpret.py @@ -1,12 +1,18 @@ +""" +Find intermediate evalutation results in assert statements through builtin AST. +""" +import ast import sys + +import _pytest._code import py -from _pytest.assertion.util import BuiltinAssertionError +from _pytest.assertion import util u = py.builtin._totext -class AssertionError(BuiltinAssertionError): +class AssertionError(util.BuiltinAssertionError): def __init__(self, *args): - BuiltinAssertionError.__init__(self, *args) + util.BuiltinAssertionError.__init__(self, *args) if args: # on Python2.6 we get len(args)==2 for: assert 0, (x,y) # on Python2.7 and above we always get len(args) == 1 @@ -22,7 +28,7 @@ class AssertionError(BuiltinAssertionError): "<[broken __repr__] %s at %0xd>" % (toprint.__class__, id(toprint))) else: - f = py.code.Frame(sys._getframe(1)) + f = _pytest._code.Frame(sys._getframe(1)) try: source = f.code.fullsource if source is not None: @@ -46,7 +52,356 @@ class AssertionError(BuiltinAssertionError): if sys.version_info > (3, 0): AssertionError.__module__ = "builtins" -if sys.version_info >= (2, 6) or sys.platform.startswith("java"): - from _pytest.assertion.newinterpret import interpret as reinterpret +if sys.platform.startswith("java"): + # See http://bugs.jython.org/issue1497 + _exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict", + "ListComp", "GeneratorExp", "Yield", "Compare", "Call", + "Repr", "Num", "Str", "Attribute", "Subscript", "Name", + "List", "Tuple") + _stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign", + "AugAssign", "Print", "For", "While", "If", "With", "Raise", + "TryExcept", "TryFinally", "Assert", "Import", "ImportFrom", + "Exec", "Global", "Expr", "Pass", "Break", "Continue") + _expr_nodes = set(getattr(ast, name) for name in _exprs) + _stmt_nodes = set(getattr(ast, name) for name in _stmts) + def _is_ast_expr(node): + return node.__class__ in _expr_nodes + def _is_ast_stmt(node): + return node.__class__ in _stmt_nodes else: - from _pytest.assertion.oldinterpret import interpret as reinterpret + def _is_ast_expr(node): + return isinstance(node, ast.expr) + def _is_ast_stmt(node): + return isinstance(node, ast.stmt) + +try: + _Starred = ast.Starred +except AttributeError: + # Python 2. Define a dummy class so isinstance() will always be False. + class _Starred(object): pass + + +class Failure(Exception): + """Error found while interpreting AST.""" + + def __init__(self, explanation=""): + self.cause = sys.exc_info() + self.explanation = explanation + + +def reinterpret(source, frame, should_fail=False): + mod = ast.parse(source) + visitor = DebugInterpreter(frame) + try: + visitor.visit(mod) + except Failure: + failure = sys.exc_info()[1] + return getfailure(failure) + if should_fail: + return ("(assertion failed, but when it was re-run for " + "printing intermediate values, it did not fail. Suggestions: " + "compute assert expression before the assert or use --assert=plain)") + +def run(offending_line, frame=None): + if frame is None: + frame = _pytest._code.Frame(sys._getframe(1)) + return reinterpret(offending_line, frame) + +def getfailure(e): + explanation = util.format_explanation(e.explanation) + value = e.cause[1] + if str(value): + lines = explanation.split('\n') + lines[0] += " << %s" % (value,) + explanation = '\n'.join(lines) + text = "%s: %s" % (e.cause[0].__name__, explanation) + if text.startswith('AssertionError: assert '): + text = text[16:] + return text + +operator_map = { + ast.BitOr : "|", + ast.BitXor : "^", + ast.BitAnd : "&", + ast.LShift : "<<", + ast.RShift : ">>", + ast.Add : "+", + ast.Sub : "-", + ast.Mult : "*", + ast.Div : "/", + ast.FloorDiv : "//", + ast.Mod : "%", + ast.Eq : "==", + ast.NotEq : "!=", + ast.Lt : "<", + ast.LtE : "<=", + ast.Gt : ">", + ast.GtE : ">=", + ast.Pow : "**", + ast.Is : "is", + ast.IsNot : "is not", + ast.In : "in", + ast.NotIn : "not in" +} + +unary_map = { + ast.Not : "not %s", + ast.Invert : "~%s", + ast.USub : "-%s", + ast.UAdd : "+%s" +} + + +class DebugInterpreter(ast.NodeVisitor): + """Interpret AST nodes to gleam useful debugging information. """ + + def __init__(self, frame): + self.frame = frame + + def generic_visit(self, node): + # Fallback when we don't have a special implementation. + if _is_ast_expr(node): + mod = ast.Expression(node) + co = self._compile(mod) + try: + result = self.frame.eval(co) + except Exception: + raise Failure() + explanation = self.frame.repr(result) + return explanation, result + elif _is_ast_stmt(node): + mod = ast.Module([node]) + co = self._compile(mod, "exec") + try: + self.frame.exec_(co) + except Exception: + raise Failure() + return None, None + else: + raise AssertionError("can't handle %s" %(node,)) + + def _compile(self, source, mode="eval"): + return compile(source, "", mode) + + def visit_Expr(self, expr): + return self.visit(expr.value) + + def visit_Module(self, mod): + for stmt in mod.body: + self.visit(stmt) + + def visit_Name(self, name): + explanation, result = self.generic_visit(name) + # See if the name is local. + source = "%r in locals() is not globals()" % (name.id,) + co = self._compile(source) + try: + local = self.frame.eval(co) + except Exception: + # have to assume it isn't + local = None + if local is None or not self.frame.is_true(local): + return name.id, result + return explanation, result + + def visit_Compare(self, comp): + left = comp.left + left_explanation, left_result = self.visit(left) + for op, next_op in zip(comp.ops, comp.comparators): + next_explanation, next_result = self.visit(next_op) + op_symbol = operator_map[op.__class__] + explanation = "%s %s %s" % (left_explanation, op_symbol, + next_explanation) + source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,) + co = self._compile(source) + try: + result = self.frame.eval(co, __exprinfo_left=left_result, + __exprinfo_right=next_result) + except Exception: + raise Failure(explanation) + try: + if not self.frame.is_true(result): + break + except KeyboardInterrupt: + raise + except: + break + left_explanation, left_result = next_explanation, next_result + + if util._reprcompare is not None: + res = util._reprcompare(op_symbol, left_result, next_result) + if res: + explanation = res + return explanation, result + + def visit_BoolOp(self, boolop): + is_or = isinstance(boolop.op, ast.Or) + explanations = [] + for operand in boolop.values: + explanation, result = self.visit(operand) + explanations.append(explanation) + if result == is_or: + break + name = is_or and " or " or " and " + explanation = "(" + name.join(explanations) + ")" + return explanation, result + + def visit_UnaryOp(self, unary): + pattern = unary_map[unary.op.__class__] + operand_explanation, operand_result = self.visit(unary.operand) + explanation = pattern % (operand_explanation,) + co = self._compile(pattern % ("__exprinfo_expr",)) + try: + result = self.frame.eval(co, __exprinfo_expr=operand_result) + except Exception: + raise Failure(explanation) + return explanation, result + + def visit_BinOp(self, binop): + left_explanation, left_result = self.visit(binop.left) + right_explanation, right_result = self.visit(binop.right) + symbol = operator_map[binop.op.__class__] + explanation = "(%s %s %s)" % (left_explanation, symbol, + right_explanation) + source = "__exprinfo_left %s __exprinfo_right" % (symbol,) + co = self._compile(source) + try: + result = self.frame.eval(co, __exprinfo_left=left_result, + __exprinfo_right=right_result) + except Exception: + raise Failure(explanation) + return explanation, result + + def visit_Call(self, call): + func_explanation, func = self.visit(call.func) + arg_explanations = [] + ns = {"__exprinfo_func" : func} + arguments = [] + for arg in call.args: + arg_explanation, arg_result = self.visit(arg) + if isinstance(arg, _Starred): + arg_name = "__exprinfo_star" + ns[arg_name] = arg_result + arguments.append("*%s" % (arg_name,)) + arg_explanations.append("*%s" % (arg_explanation,)) + else: + arg_name = "__exprinfo_%s" % (len(ns),) + ns[arg_name] = arg_result + arguments.append(arg_name) + arg_explanations.append(arg_explanation) + for keyword in call.keywords: + arg_explanation, arg_result = self.visit(keyword.value) + if keyword.arg: + arg_name = "__exprinfo_%s" % (len(ns),) + keyword_source = "%s=%%s" % (keyword.arg) + arguments.append(keyword_source % (arg_name,)) + arg_explanations.append(keyword_source % (arg_explanation,)) + else: + arg_name = "__exprinfo_kwds" + arguments.append("**%s" % (arg_name,)) + arg_explanations.append("**%s" % (arg_explanation,)) + + ns[arg_name] = arg_result + + if getattr(call, 'starargs', None): + arg_explanation, arg_result = self.visit(call.starargs) + arg_name = "__exprinfo_star" + ns[arg_name] = arg_result + arguments.append("*%s" % (arg_name,)) + arg_explanations.append("*%s" % (arg_explanation,)) + + if getattr(call, 'kwargs', None): + arg_explanation, arg_result = self.visit(call.kwargs) + arg_name = "__exprinfo_kwds" + ns[arg_name] = arg_result + arguments.append("**%s" % (arg_name,)) + arg_explanations.append("**%s" % (arg_explanation,)) + args_explained = ", ".join(arg_explanations) + explanation = "%s(%s)" % (func_explanation, args_explained) + args = ", ".join(arguments) + source = "__exprinfo_func(%s)" % (args,) + co = self._compile(source) + try: + result = self.frame.eval(co, **ns) + except Exception: + raise Failure(explanation) + pattern = "%s\n{%s = %s\n}" + rep = self.frame.repr(result) + explanation = pattern % (rep, rep, explanation) + return explanation, result + + def _is_builtin_name(self, name): + pattern = "%r not in globals() and %r not in locals()" + source = pattern % (name.id, name.id) + co = self._compile(source) + try: + return self.frame.eval(co) + except Exception: + return False + + def visit_Attribute(self, attr): + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + source_explanation, source_result = self.visit(attr.value) + explanation = "%s.%s" % (source_explanation, attr.attr) + source = "__exprinfo_expr.%s" % (attr.attr,) + co = self._compile(source) + try: + try: + result = self.frame.eval(co, __exprinfo_expr=source_result) + except AttributeError: + # Maybe the attribute name needs to be mangled? + if not attr.attr.startswith("__") or attr.attr.endswith("__"): + raise + source = "getattr(__exprinfo_expr.__class__, '__name__', '')" + co = self._compile(source) + class_name = self.frame.eval(co, __exprinfo_expr=source_result) + mangled_attr = "_" + class_name + attr.attr + source = "__exprinfo_expr.%s" % (mangled_attr,) + co = self._compile(source) + result = self.frame.eval(co, __exprinfo_expr=source_result) + except Exception: + raise Failure(explanation) + explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result), + self.frame.repr(result), + source_explanation, attr.attr) + # Check if the attr is from an instance. + source = "%r in getattr(__exprinfo_expr, '__dict__', {})" + source = source % (attr.attr,) + co = self._compile(source) + try: + from_instance = self.frame.eval(co, __exprinfo_expr=source_result) + except Exception: + from_instance = None + if from_instance is None or self.frame.is_true(from_instance): + rep = self.frame.repr(result) + pattern = "%s\n{%s = %s\n}" + explanation = pattern % (rep, rep, explanation) + return explanation, result + + def visit_Assert(self, assrt): + test_explanation, test_result = self.visit(assrt.test) + explanation = "assert %s" % (test_explanation,) + if not self.frame.is_true(test_result): + try: + raise util.BuiltinAssertionError + except Exception: + raise Failure(explanation) + return explanation, test_result + + def visit_Assign(self, assign): + value_explanation, value_result = self.visit(assign.value) + explanation = "... = %s" % (value_explanation,) + name = ast.Name("__exprinfo_expr", ast.Load(), + lineno=assign.value.lineno, + col_offset=assign.value.col_offset) + new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno, + col_offset=assign.col_offset) + mod = ast.Module([new_assign]) + co = self._compile(mod, "exec") + try: + self.frame.exec_(co, __exprinfo_expr=value_result) + except Exception: + raise Failure(explanation) + return explanation, value_result + diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 9484ba4b3..14b8e49db 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -453,6 +453,11 @@ binop_map = { ast.In: "in", ast.NotIn: "not in" } +# Python 3.5+ compatibility +try: + binop_map[ast.MatMult] = "@" +except AttributeError: + pass # Python 3.4+ compatibility if hasattr(ast, "NameConstant"): diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 401d04f10..f2f23efea 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -1,6 +1,7 @@ """Utilities for assertion debugging""" import pprint +import _pytest._code import py try: from collections import Sequence @@ -17,6 +18,15 @@ u = py.builtin._totext _reprcompare = None +# the re-encoding is needed for python2 repr +# with non-ascii characters (see issue 877 and 1379) +def ecu(s): + try: + return u(s, 'utf-8', 'replace') + except TypeError: + return s + + def format_explanation(explanation): """This formats an explanation @@ -27,6 +37,7 @@ def format_explanation(explanation): for when one explanation needs to span multiple lines, e.g. when displaying diffs. """ + explanation = ecu(explanation) explanation = _collapse_false(explanation) lines = _split_explanation(explanation) result = _format_lines(lines) @@ -130,14 +141,6 @@ def assertrepr_compare(config, op, left, right): left_repr = py.io.saferepr(left, maxsize=int(width/2)) right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) - # the re-encoding is needed for python2 repr - # with non-ascii characters (see issue 877) - def ecu(s): - try: - return u(s, 'utf-8', 'replace') - except TypeError: - return s - summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr)) issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) and @@ -179,7 +182,7 @@ def assertrepr_compare(config, op, left, right): explanation = [ u('(pytest_assertion plugin: representation of details failed. ' 'Probably an object has a faulty __repr__.)'), - u(py.code.ExceptionInfo())] + u(_pytest._code.ExceptionInfo())] if not explanation: return None diff --git a/_pytest/cacheprovider.py b/_pytest/cacheprovider.py index afd852de9..e5c11a878 100755 --- a/_pytest/cacheprovider.py +++ b/_pytest/cacheprovider.py @@ -155,11 +155,11 @@ class LFPlugin: def pytest_addoption(parser): group = parser.getgroup("general") group.addoption( - '--lf', action='store_true', dest="lf", + '--lf', '--last-failed', action='store_true', dest="lf", help="rerun only the tests that failed " "at the last run (or all if none failed)") group.addoption( - '--ff', action='store_true', dest="failedfirst", + '--ff', '--failed-first', action='store_true', dest="failedfirst", help="run all tests but run the last failures first. " "This may re-order tests and thus lead to " "repeated fixture setup/teardown") diff --git a/_pytest/config.py b/_pytest/config.py index 0495aa21f..761b0e52e 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -8,6 +8,7 @@ import warnings import py # DON't import pytest here because it causes import cycle troubles import sys, os +import _pytest._code import _pytest.hookspec # the extension point definitions from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker @@ -158,7 +159,7 @@ class PytestPluginManager(PluginManager): Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead. """ warning = dict(code="I2", - fslocation=py.code.getfslineno(sys._getframe(1)), + fslocation=_pytest._code.getfslineno(sys._getframe(1)), nodeid=None, message="use pluginmanager.add_hookspecs instead of " "deprecated addhooks() method.") @@ -195,7 +196,7 @@ class PytestPluginManager(PluginManager): def _verify_hook(self, hook, hookmethod): super(PytestPluginManager, self)._verify_hook(hook, hookmethod) if "__multicall__" in hookmethod.argnames: - fslineno = py.code.getfslineno(hookmethod.function) + fslineno = _pytest._code.getfslineno(hookmethod.function) warning = dict(code="I1", fslocation=fslineno, nodeid=None, @@ -455,11 +456,11 @@ class Parser: """ self._anonymous.addoption(*opts, **attrs) - def parse(self, args): + def parse(self, args, namespace=None): from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() try_argcomplete(self.optparser) - return self.optparser.parse_args([str(x) for x in args]) + return self.optparser.parse_args([str(x) for x in args], namespace=namespace) def _getparser(self): from _pytest._argcomplete import filescompleter @@ -477,25 +478,25 @@ class Parser: optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter return optparser - def parse_setoption(self, args, option): - parsedoption = self.parse(args) + def parse_setoption(self, args, option, namespace=None): + parsedoption = self.parse(args, namespace=namespace) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) return getattr(parsedoption, FILE_OR_DIR) - def parse_known_args(self, args): + def parse_known_args(self, args, namespace=None): """parses and returns a namespace object with known arguments at this point. """ - return self.parse_known_and_unknown_args(args)[0] + return self.parse_known_and_unknown_args(args, namespace=namespace)[0] - def parse_known_and_unknown_args(self, args): + def parse_known_and_unknown_args(self, args, namespace=None): """parses and returns a namespace object with known arguments, and the remaining arguments unknown at this point. """ optparser = self._getparser() args = [str(x) for x in args] - return optparser.parse_known_args(args) + return optparser.parse_known_args(args, namespace=namespace) def addini(self, name, help, type=None, default=None): """ register an ini-file option. @@ -779,10 +780,12 @@ def _ensure_removed_sysmodule(modname): class CmdOptions(object): """ holds cmdline options as attributes.""" - def __init__(self, **kwargs): - self.__dict__.update(kwargs) + def __init__(self, values=()): + self.__dict__.update(values) def __repr__(self): return "" %(self.__dict__,) + def copy(self): + return CmdOptions(self.__dict__) class Notset: def __repr__(self): @@ -879,8 +882,8 @@ class Config(object): def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ config = get_config() - config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) + config.parse(args, addopts=False) for x in config.option.plugins: config.pluginmanager.consider_pluginarg(x) return config @@ -898,7 +901,7 @@ class Config(object): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) def _initini(self, args): - ns, unknown_args = self._parser.parse_known_and_unknown_args(args) + ns, unknown_args = self._parser.parse_known_and_unknown_args(args, namespace=self.option.copy()) r = determine_setup(ns.inifilename, ns.file_or_dir + unknown_args) self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info['rootdir'] = self.rootdir @@ -919,7 +922,7 @@ class Config(object): except ImportError as e: self.warn("I2", "could not load setuptools entry import: %s" % (e,)) self.pluginmanager.consider_env() - self.known_args_namespace = ns = self._parser.parse_known_args(args) + self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy()) if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -947,17 +950,17 @@ class Config(object): self.inicfg.config.path, self.inicfg.lineof('minversion'), minver, pytest.__version__)) - def parse(self, args): + def parse(self, args, addopts=True): # parse given cmdline arguments into this config object. assert not hasattr(self, 'args'), ( "can only parse cmdline args at most once per Config object") self._origargs = args self.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=self.pluginmanager)) - self._preparse(args) + self._preparse(args, addopts=addopts) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) - args = self._parser.parse_setoption(args, self.option) + args = self._parser.parse_setoption(args, self.option, namespace=self.option) if not args: cwd = os.getcwd() if cwd == self.rootdir: diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 3052d0590..a57f7a494 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -1,9 +1,12 @@ """ discover and run doctests in modules and test files.""" from __future__ import absolute_import + import traceback -import pytest, py + +import pytest +from _pytest._code.code import TerminalRepr, ReprFileLocation, ExceptionInfo from _pytest.python import FixtureRequest -from py._code.code import TerminalRepr, ReprFileLocation + def pytest_addoption(parser): @@ -15,7 +18,7 @@ def pytest_addoption(parser): help="run doctests in all .py modules", dest="doctestmodules") group.addoption("--doctest-glob", - action="store", default="test*.txt", metavar="pat", + action="append", default=[], metavar="pat", help="doctests file matching pattern, default: test*.txt", dest="doctestglob") group.addoption("--doctest-ignore-import-errors", @@ -29,11 +32,20 @@ def pytest_collect_file(path, parent): if path.ext == ".py": if config.option.doctestmodules: return DoctestModule(path, parent) - elif (path.ext in ('.txt', '.rst') and parent.session.isinitpath(path)) or \ - path.check(fnmatch=config.getvalue("doctestglob")): + elif _is_doctest(config, path, parent): return DoctestTextfile(path, parent) +def _is_doctest(config, path, parent): + if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path): + return True + globs = config.getoption("doctestglob") or ['test*.txt'] + for glob in globs: + if path.check(fnmatch=glob): + return True + return False + + class ReprFailDoctest(TerminalRepr): def __init__(self, reprlocation, lines): @@ -79,7 +91,7 @@ class DoctestItem(pytest.Item): lineno = test.lineno + example.lineno + 1 message = excinfo.type.__name__ reprlocation = ReprFileLocation(filename, lineno, message) - checker = _get_unicode_checker() + checker = _get_checker() REPORT_UDIFF = doctest.REPORT_UDIFF if lineno is not None: lines = doctestfailure.test.docstring.splitlines(False) @@ -98,7 +110,7 @@ class DoctestItem(pytest.Item): lines += checker.output_difference(example, doctestfailure.got, REPORT_UDIFF).split("\n") else: - inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) + inner_excinfo = ExceptionInfo(excinfo.value.exc_info) lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] lines += traceback.format_exception(*excinfo.value.exc_info) @@ -118,7 +130,9 @@ def _get_flag_lookup(): ELLIPSIS=doctest.ELLIPSIS, IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, - ALLOW_UNICODE=_get_allow_unicode_flag()) + ALLOW_UNICODE=_get_allow_unicode_flag(), + ALLOW_BYTES=_get_allow_bytes_flag(), + ) def get_optionflags(parent): @@ -147,7 +161,7 @@ class DoctestTextfile(DoctestItem, pytest.Module): optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, - checker=_get_unicode_checker()) + checker=_get_checker()) parser = doctest.DocTestParser() test = parser.get_doctest(text, globs, name, filename, 0) @@ -182,7 +196,7 @@ class DoctestModule(pytest.Module): finder = doctest.DocTestFinder() optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, - checker=_get_unicode_checker()) + checker=_get_checker()) for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests yield DoctestItem(test.name, self, runner, test) @@ -204,28 +218,32 @@ def _setup_fixtures(doctest_item): return fixture_request -def _get_unicode_checker(): +def _get_checker(): """ Returns a doctest.OutputChecker subclass that takes in account the - ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful - when the same doctest should run in Python 2 and Python 3. + ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES + to strip b'' prefixes. + Useful when the same doctest should run in Python 2 and Python 3. An inner class is used to avoid importing "doctest" at the module level. """ - if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'): - return _get_unicode_checker.UnicodeOutputChecker() + if hasattr(_get_checker, 'LiteralsOutputChecker'): + return _get_checker.LiteralsOutputChecker() import doctest import re - class UnicodeOutputChecker(doctest.OutputChecker): + class LiteralsOutputChecker(doctest.OutputChecker): """ Copied from doctest_nose_plugin.py from the nltk project: https://github.com/nltk/nltk + + Further extended to also support byte literals. """ - _literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) def check_output(self, want, got, optionflags): res = doctest.OutputChecker.check_output(self, want, got, @@ -233,23 +251,27 @@ def _get_unicode_checker(): if res: return True - if not (optionflags & _get_allow_unicode_flag()): + allow_unicode = optionflags & _get_allow_unicode_flag() + allow_bytes = optionflags & _get_allow_bytes_flag() + if not allow_unicode and not allow_bytes: return False else: # pragma: no cover - # the code below will end up executed only in Python 2 in - # our tests, and our coverage check runs in Python 3 only - def remove_u_prefixes(txt): - return re.sub(self._literal_re, r'\1\2', txt) + def remove_prefixes(regex, txt): + return re.sub(regex, r'\1\2', txt) - want = remove_u_prefixes(want) - got = remove_u_prefixes(got) + if allow_unicode: + want = remove_prefixes(self._unicode_literal_re, want) + got = remove_prefixes(self._unicode_literal_re, got) + if allow_bytes: + want = remove_prefixes(self._bytes_literal_re, want) + got = remove_prefixes(self._bytes_literal_re, got) res = doctest.OutputChecker.check_output(self, want, got, optionflags) return res - _get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker - return _get_unicode_checker.UnicodeOutputChecker() + _get_checker.LiteralsOutputChecker = LiteralsOutputChecker + return _get_checker.LiteralsOutputChecker() def _get_allow_unicode_flag(): @@ -258,3 +280,11 @@ def _get_allow_unicode_flag(): """ import doctest return doctest.register_optionflag('ALLOW_UNICODE') + + +def _get_allow_bytes_flag(): + """ + Registers and returns the ALLOW_BYTES flag. + """ + import doctest + return doctest.register_optionflag('ALLOW_BYTES') diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 6fd849b40..224b7971c 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -289,7 +289,10 @@ def pytest_exception_interact(node, call, report): that is not an internal exception like ``skip.Exception``. """ -def pytest_enter_pdb(): +def pytest_enter_pdb(config): """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. + + :arg config: pytest config object + :type config: _pytest.config.Config """ diff --git a/_pytest/main.py b/_pytest/main.py index 6454ba2ae..70d6896cb 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -1,9 +1,13 @@ """ core implementation of testing process: init, session, runtest loop. """ +import imp +import os import re +import sys +import _pytest +import _pytest._code import py -import pytest, _pytest -import os, sys, imp +import pytest try: from collections import MutableMapping as MappingMixin except ImportError: @@ -91,11 +95,11 @@ def wrap_session(config, doit): except pytest.UsageError: raise except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = EXIT_INTERRUPTED except: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): diff --git a/_pytest/pdb.py b/_pytest/pdb.py index 3269922c3..84c920d17 100644 --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -37,7 +37,6 @@ class pytestPDB: """ invoke PDB set_trace debugging, dropping any IO capturing. """ import _pytest.config frame = sys._getframe().f_back - capman = None if self._pluginmanager is not None: capman = self._pluginmanager.getplugin("capturemanager") if capman: @@ -45,7 +44,7 @@ class pytestPDB: tw = _pytest.config.create_terminal_writer(self._config) tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") - self._pluginmanager.hook.pytest_enter_pdb() + self._pluginmanager.hook.pytest_enter_pdb(config=self._config) pdb.Pdb().set_trace(frame) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 95f92d835..faed7f581 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,19 +1,20 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ -import gc -import sys -import traceback -import os import codecs -import re -import time +import gc +import os import platform -from fnmatch import fnmatch +import re import subprocess +import sys +import time +import traceback +from fnmatch import fnmatch -import py -import pytest from py.builtin import print_ +from _pytest._code import Source +import py +import pytest from _pytest.main import Session, EXIT_OK @@ -472,7 +473,7 @@ class Testdir: ret = None for name, value in items: p = self.tmpdir.join(name).new(ext=ext) - source = py.code.Source(value) + source = Source(value) def my_totext(s, encoding="utf-8"): if py.builtin._isbytes(s): s = py.builtin._totext(s, encoding=encoding) @@ -835,7 +836,7 @@ class Testdir: to the temporarly directory to ensure it is a package. """ - kw = {self.request.function.__name__: py.code.Source(source).strip()} + kw = {self.request.function.__name__: Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") @@ -1041,8 +1042,8 @@ class LineMatcher: def _getlines(self, lines2): if isinstance(lines2, str): - lines2 = py.code.Source(lines2) - if isinstance(lines2, py.code.Source): + lines2 = Source(lines2) + if isinstance(lines2, Source): lines2 = lines2.strip().lines return lines2 diff --git a/_pytest/python.py b/_pytest/python.py index 6f3a717af..d5612a584 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,14 +1,15 @@ """ Python test discovery, setup and run of test functions. """ -import re import fnmatch import functools -import py import inspect +import re import types import sys + +import py import pytest +from _pytest._code.code import TerminalRepr from _pytest.mark import MarkDecorator, MarkerError -from py._code.code import TerminalRepr try: import enum @@ -86,7 +87,7 @@ def getfslineno(obj): obj = get_real_func(obj) if hasattr(obj, 'place_as'): obj = obj.place_as - fslineno = py.code.getfslineno(obj) + fslineno = _pytest._code.getfslineno(obj) assert isinstance(fslineno[1], int), obj return fslineno @@ -331,7 +332,7 @@ def pytest_pycollect_makeitem(collector, name, obj): def is_generator(func): try: - return py.code.getrawcode(func).co_flags & 32 # generator function + return _pytest._code.getrawcode(func).co_flags & 32 # generator function except AttributeError: # builtin functions have no bytecode # assume them to not be generators return False @@ -610,7 +611,7 @@ class Module(pytest.File, PyCollector): mod = self.fspath.pyimport(ensuresyspath=importmode) except SyntaxError: raise self.CollectError( - py.code.ExceptionInfo().getrepr(style="short")) + _pytest._code.ExceptionInfo().getrepr(style="short")) except self.fspath.ImportMismatchError: e = sys.exc_info()[1] raise self.CollectError( @@ -716,7 +717,7 @@ class FunctionMixin(PyobjMixin): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(get_real_func(self.obj)) + code = _pytest._code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1202,10 +1203,10 @@ def getlocation(function, curdir): # builtin pytest.raises helper def raises(expected_exception, *args, **kwargs): - """ assert that a code block/function call raises @expected_exception + """ assert that a code block/function call raises ``expected_exception`` and raise a failure exception otherwise. - This helper produces a ``py.code.ExceptionInfo()`` object. + This helper produces a ``ExceptionInfo()`` object (see below). If using Python 2.5 or above, you may use this function as a context manager:: @@ -1221,19 +1222,19 @@ def raises(expected_exception, *args, **kwargs): Lines of code after that, within the scope of the context manager will not be executed. For example:: - >>> with raises(OSError) as err: + >>> with raises(OSError) as exc_info: assert 1 == 1 # this will execute as expected raise OSError(errno.EEXISTS, 'directory exists') - assert err.errno == errno.EEXISTS # this will not execute + assert exc_info.value.errno == errno.EEXISTS # this will not execute Instead, the following approach must be taken (note the difference in scope):: - >>> with raises(OSError) as err: + >>> with raises(OSError) as exc_info: assert 1 == 1 # this will execute as expected raise OSError(errno.EEXISTS, 'directory exists') - assert err.errno == errno.EEXISTS # this will now execute + assert exc_info.value.errno == errno.EEXISTS # this will now execute Or you can specify a callable by passing a to-be-called lambda:: @@ -1254,21 +1255,22 @@ def raises(expected_exception, *args, **kwargs): >>> raises(ZeroDivisionError, "f(0)") - Performance note: - ----------------- + .. autoclass:: _pytest._code.ExceptionInfo + :members: - Similar to caught exception objects in Python, explicitly clearing - local references to returned ``py.code.ExceptionInfo`` objects can - help the Python interpreter speed up its garbage collection. + .. note:: + Similar to caught exception objects in Python, explicitly clearing + local references to returned ``ExceptionInfo`` objects can + help the Python interpreter speed up its garbage collection. - Clearing those references breaks a reference cycle - (``ExceptionInfo`` --> caught exception --> frame stack raising - the exception --> current frame stack --> local variables --> - ``ExceptionInfo``) which makes Python keep all objects referenced - from that cycle (including all local variables in the current - frame) alive until the next cyclic garbage collection run. See the - official Python ``try`` statement documentation for more detailed - information. + Clearing those references breaks a reference cycle + (``ExceptionInfo`` --> caught exception --> frame stack raising + the exception --> current frame stack --> local variables --> + ``ExceptionInfo``) which makes Python keep all objects referenced + from that cycle (including all local variables in the current + frame) alive until the next cyclic garbage collection run. See the + official Python ``try`` statement documentation for more detailed + information. """ __tracebackhide__ = True @@ -1297,19 +1299,19 @@ def raises(expected_exception, *args, **kwargs): loc.update(kwargs) #print "raises frame scope: %r" % frame.f_locals try: - code = py.code.Source(code).compile() + code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) # XXX didn'T mean f_globals == f_locals something special? # this is destroyed here ... except expected_exception: - return py.code.ExceptionInfo() + return _pytest._code.ExceptionInfo() else: func = args[0] try: func(*args[1:], **kwargs) except expected_exception: - return py.code.ExceptionInfo() - pytest.fail("DID NOT RAISE") + return _pytest._code.ExceptionInfo() + pytest.fail("DID NOT RAISE {0}".format(expected_exception)) class RaisesContext(object): def __init__(self, expected_exception): @@ -1317,7 +1319,7 @@ class RaisesContext(object): self.excinfo = None def __enter__(self): - self.excinfo = object.__new__(py.code.ExceptionInfo) + self.excinfo = object.__new__(_pytest._code.ExceptionInfo) return self.excinfo def __exit__(self, *tp): @@ -1768,7 +1770,6 @@ class FixtureLookupError(LookupError): # the last fixture raise an error, let's present # it at the requesting side stack = stack[:-1] - for function in stack: fspath, lineno = getfslineno(function) try: @@ -2025,7 +2026,7 @@ class FixtureManager: def fail_fixturefunc(fixturefunc, msg): fs, lineno = getfslineno(fixturefunc) location = "%s:%s" % (fs, lineno+1) - source = py.code.Source(fixturefunc) + source = _pytest._code.Source(fixturefunc) pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) @@ -2168,14 +2169,14 @@ def getfuncargnames(function, startindex=None): startindex += num_mock_patch_args(function) function = realfunction if isinstance(function, functools.partial): - argnames = inspect.getargs(py.code.getrawcode(function.func))[0] + argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0] partial = function argnames = argnames[len(partial.args):] if partial.keywords: for kw in partial.keywords: argnames.remove(kw) else: - argnames = inspect.getargs(py.code.getrawcode(function))[0] + argnames = inspect.getargs(_pytest._code.getrawcode(function))[0] defaults = getattr(function, 'func_defaults', getattr(function, '__defaults__', None)) or () numdefaults = len(defaults) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 797327bad..a89474c03 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -1,6 +1,8 @@ """ recording warnings during test function execution. """ import inspect + +import _pytest._code import py import sys import warnings @@ -28,14 +30,22 @@ def pytest_namespace(): 'warns': warns} -def deprecated_call(func, *args, **kwargs): +def deprecated_call(func=None, *args, **kwargs): """ assert that calling ``func(*args, **kwargs)`` triggers a ``DeprecationWarning`` or ``PendingDeprecationWarning``. + This function can be used as a context manager:: + + >>> with deprecated_call(): + ... myobject.deprecated_method() + Note: we cannot use WarningsRecorder here because it is still subject to the mechanism that prevents warnings of the same type from being triggered twice for the same module. See #1190. """ + if not func: + return WarningsChecker(expected_warning=DeprecationWarning) + categories = [] def warn_explicit(message, category, *args, **kwargs): @@ -92,7 +102,7 @@ def warns(expected_warning, *args, **kwargs): loc.update(kwargs) with wcheck: - code = py.code.Source(code).compile() + code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) else: func = args[0] @@ -171,8 +181,8 @@ class WarningsRecorder(object): self._module.showwarning = showwarning # allow the same warning to be raised more than once - self._module.simplefilter('always', append=True) + self._module.simplefilter('always') return self def __exit__(self, *exc_info): diff --git a/_pytest/runner.py b/_pytest/runner.py index 6d0f8b57b..a50c2d738 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -5,7 +5,8 @@ from time import time import py import pytest -from py._code.code import TerminalRepr +from _pytest._code.code import TerminalRepr, ExceptionInfo + def pytest_namespace(): return { @@ -151,7 +152,7 @@ class CallInfo: self.stop = time() raise except: - self.excinfo = py.code.ExceptionInfo() + self.excinfo = ExceptionInfo() self.stop = time() def __repr__(self): @@ -177,9 +178,13 @@ class BaseReport(object): self.__dict__.update(kw) def toterminal(self, out): - longrepr = self.longrepr if hasattr(self, 'node'): out.line(getslaveinfoline(self.node)) + + longrepr = self.longrepr + if longrepr is None: + return + if hasattr(longrepr, 'toterminal'): longrepr.toterminal(out) else: @@ -211,7 +216,7 @@ def pytest_runtest_makereport(item, call): outcome = "passed" longrepr = None else: - if not isinstance(excinfo, py.code.ExceptionInfo): + if not isinstance(excinfo, ExceptionInfo): outcome = "failed" longrepr = excinfo elif excinfo.errisinstance(pytest.skip.Exception): diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 36e54d7d8..5b779c98b 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -5,6 +5,8 @@ import traceback import py import pytest +from _pytest.mark import MarkInfo + def pytest_addoption(parser): group = parser.getgroup("general") @@ -12,6 +14,7 @@ def pytest_addoption(parser): action="store_true", dest="runxfail", default=False, help="run tests even if they are marked xfail") + def pytest_configure(config): if config.option.runxfail: old = pytest.xfail @@ -38,18 +41,22 @@ def pytest_configure(config): "See http://pytest.org/latest/skipping.html" ) + def pytest_namespace(): return dict(xfail=xfail) + class XFailed(pytest.fail.Exception): """ raised from an explicit call to pytest.xfail() """ + def xfail(reason=""): """ xfail an executing test or setup functions with the given reason.""" __tracebackhide__ = True raise XFailed(reason) xfail.Exception = XFailed + class MarkEvaluator: def __init__(self, item, name): self.item = item @@ -147,10 +154,25 @@ class MarkEvaluator: @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): - evalskip = MarkEvaluator(item, 'skipif') - if evalskip.istrue(): - item._evalskip = evalskip - pytest.skip(evalskip.getexplanation()) + # Check if skip or skipif are specified as pytest marks + + skipif_info = item.keywords.get('skipif') + if isinstance(skipif_info, MarkInfo): + eval_skipif = MarkEvaluator(item, 'skipif') + if eval_skipif.istrue(): + item._evalskip = eval_skipif + pytest.skip(eval_skipif.getexplanation()) + + skip_info = item.keywords.get('skip') + if isinstance(skip_info, MarkInfo): + item._evalskip = True + if 'reason' in skip_info.kwargs: + pytest.skip(skip_info.kwargs['reason']) + elif skip_info.args: + pytest.skip(skip_info.args[0]) + else: + pytest.skip("unconditional skip") + item._evalxfail = MarkEvaluator(item, 'xfail') check_xfail_no_run(item) @@ -230,6 +252,9 @@ def pytest_terminal_summary(terminalreporter): show_skipped(terminalreporter, lines) elif char == "E": show_simple(terminalreporter, lines, 'error', "ERROR %s") + elif char == 'p': + show_simple(terminalreporter, lines, 'passed', "PASSED %s") + if lines: tr._tw.sep("=", "short test summary info") for line in lines: @@ -266,9 +291,8 @@ def cached_eval(config, expr, d): try: return config._evalcache[expr] except KeyError: - #import sys - #print >>sys.stderr, ("cache-miss: %r" % expr) - exprcode = py.code.compile(expr, mode="eval") + import _pytest._code + exprcode = _pytest._code.compile(expr, mode="eval") config._evalcache[expr] = x = eval(exprcode, d) return x diff --git a/_pytest/terminal.py b/_pytest/terminal.py index dac134cd2..e17b8dda2 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -22,7 +22,8 @@ def pytest_addoption(parser): group._addoption('-r', action="store", dest="reportchars", default=None, metavar="chars", help="show extra test summary info as specified by chars (f)ailed, " - "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings (a)all.") + "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings " + "(p)passed, (P)passed with output, (a)all except pP.") group._addoption('-l', '--showlocals', action="store_true", dest="showlocals", default=False, help="show locals in tracebacks (disabled by default).") @@ -368,6 +369,7 @@ class TerminalReporter: self.summary_errors() self.summary_failures() self.summary_warnings() + self.summary_passes() if exitstatus == EXIT_INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo @@ -389,6 +391,7 @@ class TerminalReporter: if self.config.option.fulltrace: excrepr.toterminal(self._tw) else: + self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True) excrepr.reprcrash.toterminal(self._tw) def _locationline(self, nodeid, fspath, lineno, domain): @@ -446,6 +449,18 @@ class TerminalReporter: self._tw.line("W%s %s %s" % (w.code, w.fslocation, w.message)) + def summary_passes(self): + if self.config.option.tbstyle != "no": + if self.hasopt("P"): + reports = self.getreports('passed') + if not reports: + return + self.write_sep("=", "PASSES") + for rep in reports: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg) + self._outrep_summary(rep) + def summary_failures(self): if self.config.option.tbstyle != "no": reports = self.getreports('failed') diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 24fa9e921..8120e94fb 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -1,13 +1,12 @@ """ discovery and running of std-library "unittest" style tests. """ from __future__ import absolute_import -import traceback + import sys +import traceback import pytest -import py - - # for transfering markers +import _pytest._code from _pytest.python import transfer_markers from _pytest.skipping import MarkEvaluator @@ -101,7 +100,7 @@ class TestCaseFunction(pytest.Function): # unwrap potential exception info (see twisted trial support below) rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo) try: - excinfo = py.code.ExceptionInfo(rawexcinfo) + excinfo = _pytest._code.ExceptionInfo(rawexcinfo) except TypeError: try: try: @@ -117,7 +116,7 @@ class TestCaseFunction(pytest.Function): except KeyboardInterrupt: raise except pytest.fail.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() self.__dict__.setdefault('_excinfo', []).append(excinfo) def addError(self, testcase, rawexcinfo): diff --git a/appveyor.yml b/appveyor.yml index 73948b5b6..4b73645f7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,19 +1,28 @@ +environment: + COVERALLS_REPO_TOKEN: + secure: 2NJ5Ct55cHJ9WEg3xbSqCuv0rdgzzb6pnzOIG5OkMbTndw3wOBrXntWFoQrXiMFi + # this is pytest's token in coveralls.io, encrypted + # using pytestbot account as detailed here: + # https://www.appveyor.com/docs/build-configuration#secure-variables + install: - echo Installed Pythons - dir c:\Python* + # install pypy using choco (redirect to a file and write to console in case + # choco install returns non-zero, because choco install python.pypy is too + # noisy) + - choco install python.pypy > pypy-inst.log 2>&1 || (type pypy-inst.log & exit /b 1) + - set PATH=C:\tools\pypy\pypy;%PATH% # so tox can find pypy + - echo PyPy installed + - pypy --version + - C:\Python35\python -m pip install tox build: false # Not a C# project, build stuff at the test step instead. test_script: - - 'set TESTENVS= - flakes, - py26, - py27, - py33, - py34, - py27-xdist, - py35-xdist - ' - - C:\Python35\python -m tox -e "%TESTENVS%" + - C:\Python35\python -m tox + # coveralls is not in tox's envlist, plus for PRs the secure variable + # is not defined so we have to check for it + - if defined COVERALLS_REPO_TOKEN C:\Python35\python -m tox -e coveralls diff --git a/doc/en/_templates/layout.html b/doc/en/_templates/layout.html index 5ec94fd7e..1cf675139 100644 --- a/doc/en/_templates/layout.html +++ b/doc/en/_templates/layout.html @@ -1,5 +1,14 @@ {% extends "!layout.html" %} - +{% block header %} + + {{super()}} +{% endblock %} {% block footer %} {{ super() }}