From issues-reply at bitbucket.org Wed Apr 1 11:01:29 2015 From: issues-reply at bitbucket.org (Nils Werner) Date: Wed, 01 Apr 2015 09:01:29 -0000 Subject: [Pytest-commit] Issue #709: monkeypatch.setattr raises awkard exception (pytest-dev/pytest) Message-ID: <20150401090129.2266.49497@app01.ash-private.bitbucket.org> New issue 709: monkeypatch.setattr raises awkard exception https://bitbucket.org/pytest-dev/pytest/issue/709/monkeypatchsetattr-raises-awkard-exception Nils Werner: My code contains a fallback implementation when a dependency doesn't implement a certain method. Now I want to test my fallback code by monkeypatching away the imported implementation: Consider the following example def test_fallback(monkeypatch): import scipy.signal monkeypatch.setattr("scipy.signal.nonsense", raiser) This test works and removes the imported `scipy.signal.nonsense`, if it exists. It fails if it does't exist, however I wanted it to gracefully accept that and just do the tests anyways. def test_fallback(monkeypatch): import scipy.signal > monkeypatch.setattr("scipy.signal.nonsense", raiser) E Failed: object has no attribute 'nonsense' I assumed it would be a simple `AttributeError` but that isn't the case. This does not work: def test_fallback(monkeypatch): import scipy.signal try: monkeypatch.setattr("scipy.signal.nonsense", raiser) except AttributeError: pass Instead, the raised exception is of type `builtins.Failed`, which I don't know how to catch. Is this by design? From commits-noreply at bitbucket.org Thu Apr 2 10:49:17 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 02 Apr 2015 08:49:17 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: - avoid setting of versions and targets in conf.py and Makefile Message-ID: <20150402084917.23044.70647@app13.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/d79d71cff698/ Changeset: d79d71cff698 Branch: release-checklist User: hpk42 Date: 2015-04-02 08:38:25+00:00 Summary: - avoid setting of versions and targets in conf.py and Makefile as discussed on pytest-dev - "make help" now prints pytest specific information. - add a "_getdoctarget.py" helper - make ``setup.py`` read the version from ``_pytest/__init__.py`` Affected #: 7 files diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- streamlined and documented release process. Also all versions + (in setup.py and documentation generation) are now read + from _pytest/__init__.py. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.7.1.dev' +__version__ = '2.7.1.dev1' diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb doc/en/Makefile --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -15,46 +15,39 @@ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest -regen: - PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" + @echo " showtarget to show the pytest.org target directory" + @echo " install to install docs to pytest.org/SITETARGET" + @echo " install-ldf to install the doc pdf to pytest.org/SITETARGET" + @echo " regen to regenerate pytest examples using the installed pytest" @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* -SITETARGET=dev +SITETARGET=$(shell ./_getdoctarget.py) + +showtarget: + @echo $(SITETARGET) install: html # for access talk to someone with login rights to # pytest-dev at pytest.org to add your ssh key - rsync -avz _build/html/ pytest-dev at pytest.org:/www/pytest.org/$(SITETARGET) + rsync -avz _build/html/ pytest-dev at pytest.org:pytest.org/$(SITETARGET) installpdf: latexpdf - @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev at pytest.org:/www/pytest.org/$(SITETARGET) + @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev at pytest.org:pytest.org/$(SITETARGET) installall: clean install installpdf @echo "done" +regen: + PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb doc/en/_getdoctarget.py --- /dev/null +++ b/doc/en/_getdoctarget.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import py + +def get_version_string(): + fn = py.path.local(__file__).join("..", "..", "..", + "_pytest", "__init__.py") + for line in fn.readlines(): + if "version" in line: + return eval(line.split("=")[-1]) + +def get_minor_version_string(): + return ".".join(get_version_string().split(".")[:2]) + +if __name__ == "__main__": + print (get_minor_version_string()) diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,10 +17,13 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = "2.7" -release = "2.7.1.dev" -import sys, os +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import _getdoctarget + +version = _getdoctarget.get_minor_version_string() +release = _getdoctarget.get_version_string() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -54,7 +57,7 @@ # General information about the project. project = u'pytest' -copyright = u'2014, holger krekel' +copyright = u'2015, holger krekel and pytest-dev team' diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb doc/en/release.txt --- a/doc/en/release.txt +++ b/doc/en/release.txt @@ -1,39 +1,36 @@ pytest release checklist ------------------------- -For doing a release of pytest (status March 2015) holger does: +For doing a release of pytest (status April 2015) this checklist is used: -1. change version numbers in ``setup.py``, ``_pytest/__init__.py`` - to a final release version. +1. change version numbers in ``_pytest/__init__.py`` to the to-be-released version. + (the version number in ``setup.py`` reads from that init file as well) 2. finalize ``./CHANGELOG`` (don't forget the the header). 3. write ``doc/en/announce/release-VERSION.txt`` (usually copying from an earlier release version). -4. change ``version`` and ``release`` in doc/en/conf.py, set ``SITETARGET=latest`` - in ``doc/en/Makefile``. - -5. regenerate doc examples with ``tox -e regen`` and check with ``hg diff`` +4. regenerate doc examples with ``tox -e regen`` and check with ``hg diff`` if the differences show regressions. It's a bit of a manual process because there a large part of the diff is about pytest headers or differences in speed ("tests took X.Y seconds"). (XXX automate doc/example diffing to ignore such changes and integrate it into "tox -e regen"). -6. ``devpi upload`` to `your developer devpi index `_. You can create your own user and index on https://devpi.net, +5. ``devpi upload`` to `your developer devpi index `_. You can create your own user and index on https://devpi.net, an inofficial service from the devpi authors. -7. run ``devpi use INDEX`` and ``devpi test`` from linux and windows machines +6. run ``devpi use INDEX`` and ``devpi test`` from linux and windows machines and verify test results on the index. On linux typically all environments - pass (March 2015 there is a setup problem with a cx_freeze environment) + pass (April 2015 there is a setup problem with a cx_freeze environment) but on windows all involving ``pexpect`` fail because pexpect does not exist on windows and tox does not allow to have platform-specific environments. Also on windows ``py33-trial`` fails but should probably pass (March 2015). In any case, py26,py27,py33,py34 are required to pass for all platforms. -8. You can fix tests/code and repeat number 7. until everything passes. +7. You can fix tests/code and repeat number 6. until everything passes. -9. Once you have sufficiently passing tox tests you can do the actual release:: +8. Once you have sufficiently passing tox tests you can do the actual release:: cd doc/en/ make install @@ -44,12 +41,12 @@ hg tag VERSION hg push -10. send out release announcement to pytest-dev at python.org, +9. send out release announcement to pytest-dev at python.org, testing-in-python at lists.idyll.org and python-announce-list at python.org . -11. **after the release** bump the version numbers in ``setup.py``, +10. **after the release** bump the version numbers in ``setup.py``, ``_pytest/__init__.py``, ``doc/en/conf.py`` to the next Minor release version (i.e. if you released ``pytest-2.8.0``, set it to ``pytest-2.9.0.dev1``) and set ``SITETARGET=dev`` in ``doc/en/makefile``. Commit. -12. already done :) +11. already done :) diff -r b527114b58a754010d1a0fc7137d4a66787f1680 -r d79d71cff698b3ff84f3591e66b9b13288d027cb setup.py --- a/setup.py +++ b/setup.py @@ -16,6 +16,15 @@ with open('README.rst') as fd: long_description = fd.read() +def get_version(): + p = os.path.join(os.path.dirname( + os.path.abspath(__file__)), "_pytest", "__init__.py") + with open(p) as f: + for line in f.readlines(): + if "__version__" in line: + return line.strip().split("=")[-1].strip(" '") + raise ValueError("could not read version") + def main(): install_requires = ['py>=1.4.25'] @@ -28,7 +37,7 @@ name='pytest', description='pytest: simple powerful testing with Python', long_description=long_description, - version='2.7.1.dev', + version=get_version(), url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 2 10:57:58 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 02 Apr 2015 08:57:58 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: more streamlining of release checklist Message-ID: <20150402085758.7526.34440@app09.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/d78309fb516d/ Changeset: d78309fb516d Branch: release-checklist User: hpk42 Date: 2015-04-02 08:55:24+00:00 Summary: more streamlining of release checklist Affected #: 1 file diff -r d79d71cff698b3ff84f3591e66b9b13288d027cb -r d78309fb516d07086b8c29134977fbc510c49bab doc/en/release.txt --- a/doc/en/release.txt +++ b/doc/en/release.txt @@ -1,7 +1,7 @@ pytest release checklist ------------------------- -For doing a release of pytest (status April 2015) this checklist is used: +For doing a release of pytest (status April 2015) this rough checklist is used: 1. change version numbers in ``_pytest/__init__.py`` to the to-be-released version. (the version number in ``setup.py`` reads from that init file as well) @@ -33,8 +33,11 @@ 8. Once you have sufficiently passing tox tests you can do the actual release:: cd doc/en/ - make install - make install-pdf # a bit optional, if you have latex packages installed + make install # will install to 2.7, 2.8, ... according to _pytest/__init__.py + make install-pdf # optional, requires latex packages installed + ssh pytest-dev at pytest.org # MANUAL: symlink "pytest.org/latest" to the just + # installed release docs + # browse to pytest.org to see devpi push pytest-VERSION pypi:NAME hg ci -m "... finalized pytest-VERSION" @@ -44,9 +47,8 @@ 9. send out release announcement to pytest-dev at python.org, testing-in-python at lists.idyll.org and python-announce-list at python.org . -10. **after the release** bump the version numbers in ``setup.py``, - ``_pytest/__init__.py``, ``doc/en/conf.py`` to the next Minor release - version (i.e. if you released ``pytest-2.8.0``, set it to ``pytest-2.9.0.dev1``) - and set ``SITETARGET=dev`` in ``doc/en/makefile``. Commit. +10. **after the release** bump the version number in ``_pytest/__init__.py``, + to the next Minor release version (i.e. if you released ``pytest-2.8.0``, + set it to ``pytest-2.9.0.dev1``). 11. already done :) Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 3 21:29:40 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 03 Apr 2015 19:29:40 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Removed note about yield fixtures being experimental Message-ID: <20150403192940.25687.40565@app03.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/60008bd048a5/ Changeset: 60008bd048a5 Branch: yield-experimental-docs User: nicoddemus Date: 2015-04-03 19:28:20+00:00 Summary: Removed note about yield fixtures being experimental Affected #: 1 file diff -r 0de3f5c1a683a834d27b265d5e8781326d5dad04 -r 60008bd048a58852c513d129a0b8093cb9fc552b doc/en/yieldfixture.txt --- a/doc/en/yieldfixture.txt +++ b/doc/en/yieldfixture.txt @@ -9,15 +9,7 @@ pytest-2.4 allows fixture functions to seamlessly use a ``yield`` instead of a ``return`` statement to provide a fixture value while otherwise -fully supporting all other fixture features. - -.. note:: - - "yielding" fixture values is an experimental feature and its exact - declaration may change later but earliest in a 2.5 release. You can thus - safely use this feature in the 2.4 series but may need to adapt later. - Test functions themselves will not need to change (as a general - feature, they are ignorant of how fixtures are setup). +fully supporting all other fixture features. Let's look at a simple standalone-example using the new ``yield`` syntax:: Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 3 22:00:39 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 03 Apr 2015 20:00:39 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150403200039.13334.62800@app09.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/88a5fdc86c08/ Changeset: 88a5fdc86c08 Branch: yield-experimental-docs User: nicoddemus Date: 2015-04-03 19:55:10+00:00 Summary: Reviewed wording about yield being a "new" feature Affected #: 1 file diff -r 60008bd048a58852c513d129a0b8093cb9fc552b -r 88a5fdc86c0871498a95e33fa8c50e66416f3fef doc/en/yieldfixture.txt --- a/doc/en/yieldfixture.txt +++ b/doc/en/yieldfixture.txt @@ -11,7 +11,7 @@ of a ``return`` statement to provide a fixture value while otherwise fully supporting all other fixture features. -Let's look at a simple standalone-example using the new ``yield`` syntax:: +Let's look at a simple standalone-example using the ``yield`` syntax:: # content of test_yield.py @@ -64,9 +64,9 @@ because the Python ``file`` object supports finalization when the ``with`` statement ends. -Note that the new syntax is fully integrated with using ``scope``, -``params`` and other fixture features. Changing existing -fixture functions to use ``yield`` is thus straight forward. +Note that the yield fixture form supports all other fixture +features such as ``scope``, ``params``, etc., thus changing existing +fixture functions to use ``yield`` is straight forward. Discussion and future considerations / feedback ++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -106,7 +106,7 @@ - lastly ``yield`` introduces more than one way to write fixture functions, so what's the obvious way to a newcomer? Newcomers reading the docs will see feature examples using the - ``return`` style so should use that, if in doubt. + ``return`` style so should use that, if in doubt. Others can start experimenting with writing yield-style fixtures and possibly help evolving them further. https://bitbucket.org/pytest-dev/pytest/commits/619ad0c4b4dc/ Changeset: 619ad0c4b4dc Branch: yield-experimental-docs User: nicoddemus Date: 2015-04-03 19:59:33+00:00 Summary: Removed "discussion" session Kept a note about exceptions after yield not being reraised Affected #: 1 file diff -r 88a5fdc86c0871498a95e33fa8c50e66416f3fef -r 619ad0c4b4dc6e18a088c058f44aa00c7fc60f9f doc/en/yieldfixture.txt --- a/doc/en/yieldfixture.txt +++ b/doc/en/yieldfixture.txt @@ -68,48 +68,13 @@ features such as ``scope``, ``params``, etc., thus changing existing fixture functions to use ``yield`` is straight forward. -Discussion and future considerations / feedback -++++++++++++++++++++++++++++++++++++++++++++++++++++ +.. note:: -The yield-syntax has been discussed by pytest users extensively. -In general, the advantages of the using a ``yield`` fixture syntax are: + While the ``yield`` syntax is similar to what + :py:func:`contextlib.contextmanager` decorated functions + provide, with pytest fixture functions the part after the + "yield" will always be invoked, independently from the + exception status of the test function which uses the fixture. + This behaviour makes sense if you consider that many different + test functions might use a module or session scoped fixture. -- easy provision of fixtures in conjunction with context managers. - -- no need to register a callback, providing for more synchronous - control flow in the fixture function. Also there is no need to accept - the ``request`` object into the fixture function just for providing - finalization code. - -However, there are also limitations or foreseeable irritations: - -- usually ``yield`` is used for producing multiple values. - But fixture functions can only yield exactly one value. - Yielding a second fixture value will get you an error. - It's possible we can evolve pytest to allow for producing - multiple values as an alternative to current parametrization. - For now, you can just use the normal - :ref:`fixture parametrization ` - mechanisms together with ``yield``-style fixtures. - -- the ``yield`` syntax is similar to what - :py:func:`contextlib.contextmanager` decorated functions - provide. With pytest fixture functions, the "after yield" part will - always be invoked, independently from the exception status - of the test function which uses the fixture. The pytest - behaviour makes sense if you consider that many different - test functions might use a module or session scoped fixture. - Some test functions might raise exceptions and others not, - so how could pytest re-raise a single exception at the - ``yield`` point in the fixture function? - -- lastly ``yield`` introduces more than one way to write - fixture functions, so what's the obvious way to a newcomer? - Newcomers reading the docs will see feature examples using the - ``return`` style so should use that, if in doubt. - Others can start experimenting with writing yield-style fixtures - and possibly help evolving them further. - -If you want to feedback or participate in the ongoing -discussion, please join our :ref:`contact channels`. -you are most welcome. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 3 22:07:52 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 03 Apr 2015 20:07:52 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Fixed straightforward spelling Message-ID: <20150403200752.1882.69221@app01.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/7663e5239695/ Changeset: 7663e5239695 Branch: yield-experimental-docs User: nicoddemus Date: 2015-04-03 20:06:51+00:00 Summary: Fixed straightforward spelling Affected #: 1 file diff -r 619ad0c4b4dc6e18a088c058f44aa00c7fc60f9f -r 7663e5239695259c526f0a54c6e4fe22d200e7ec doc/en/yieldfixture.txt --- a/doc/en/yieldfixture.txt +++ b/doc/en/yieldfixture.txt @@ -66,7 +66,7 @@ Note that the yield fixture form supports all other fixture features such as ``scope``, ``params``, etc., thus changing existing -fixture functions to use ``yield`` is straight forward. +fixture functions to use ``yield`` is straightforward. .. note:: Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 00:46:07 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 03 Apr 2015 22:46:07 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Brought back discussion session Message-ID: <20150403224607.7991.745@app03.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/27b5ea7b6f5a/ Changeset: 27b5ea7b6f5a Branch: yield-experimental-docs User: nicoddemus Date: 2015-04-03 22:44:06+00:00 Summary: Brought back discussion session Reworded it a bit to bring it to par with the current status Affected #: 1 file diff -r 7663e5239695259c526f0a54c6e4fe22d200e7ec -r 27b5ea7b6f5a4466c356fa13ee49d0f944ca76e8 doc/en/yieldfixture.txt --- a/doc/en/yieldfixture.txt +++ b/doc/en/yieldfixture.txt @@ -78,3 +78,23 @@ This behaviour makes sense if you consider that many different test functions might use a module or session scoped fixture. + +Discussion and future considerations / feedback +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +There are some topics that are worth mentioning: + +- usually ``yield`` is used for producing multiple values. + But fixture functions can only yield exactly one value. + Yielding a second fixture value will get you an error. + It's possible we can evolve pytest to allow for producing + multiple values as an alternative to current parametrization. + For now, you can just use the normal + :ref:`fixture parametrization ` + mechanisms together with ``yield``-style fixtures. + +- lastly ``yield`` introduces more than one way to write + fixture functions, so what's the obvious way to a newcomer? + +If you want to feedback or participate in discussion of the above +topics, please join our :ref:`contact channels`, you are most welcome. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:25:29 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:25:29 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Close branch yield-experimental-docs Message-ID: <20150404142529.6501.35060@app04.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/b005f438a4bf/ Changeset: b005f438a4bf Branch: yield-experimental-docs User: hpk42 Date: 2015-04-04 14:25:24+00:00 Summary: Close branch yield-experimental-docs Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:25:29 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:25:29 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in yield-experimental-docs (pull request #267) Message-ID: <20150404142529.20027.73370@app04.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/1a0edfacf82e/ Changeset: 1a0edfacf82e User: hpk42 Date: 2015-04-04 14:25:24+00:00 Summary: Merged in yield-experimental-docs (pull request #267) Removed note about yield fixtures being experimental Affected #: 1 file diff -r 0de3f5c1a683a834d27b265d5e8781326d5dad04 -r 1a0edfacf82e465c026e0991b8954b43450e77bb doc/en/yieldfixture.txt --- a/doc/en/yieldfixture.txt +++ b/doc/en/yieldfixture.txt @@ -9,17 +9,9 @@ pytest-2.4 allows fixture functions to seamlessly use a ``yield`` instead of a ``return`` statement to provide a fixture value while otherwise -fully supporting all other fixture features. +fully supporting all other fixture features. -.. note:: - - "yielding" fixture values is an experimental feature and its exact - declaration may change later but earliest in a 2.5 release. You can thus - safely use this feature in the 2.4 series but may need to adapt later. - Test functions themselves will not need to change (as a general - feature, they are ignorant of how fixtures are setup). - -Let's look at a simple standalone-example using the new ``yield`` syntax:: +Let's look at a simple standalone-example using the ``yield`` syntax:: # content of test_yield.py @@ -72,24 +64,25 @@ because the Python ``file`` object supports finalization when the ``with`` statement ends. -Note that the new syntax is fully integrated with using ``scope``, -``params`` and other fixture features. Changing existing -fixture functions to use ``yield`` is thus straight forward. +Note that the yield fixture form supports all other fixture +features such as ``scope``, ``params``, etc., thus changing existing +fixture functions to use ``yield`` is straightforward. + +.. note:: + + While the ``yield`` syntax is similar to what + :py:func:`contextlib.contextmanager` decorated functions + provide, with pytest fixture functions the part after the + "yield" will always be invoked, independently from the + exception status of the test function which uses the fixture. + This behaviour makes sense if you consider that many different + test functions might use a module or session scoped fixture. + Discussion and future considerations / feedback ++++++++++++++++++++++++++++++++++++++++++++++++++++ -The yield-syntax has been discussed by pytest users extensively. -In general, the advantages of the using a ``yield`` fixture syntax are: - -- easy provision of fixtures in conjunction with context managers. - -- no need to register a callback, providing for more synchronous - control flow in the fixture function. Also there is no need to accept - the ``request`` object into the fixture function just for providing - finalization code. - -However, there are also limitations or foreseeable irritations: +There are some topics that are worth mentioning: - usually ``yield`` is used for producing multiple values. But fixture functions can only yield exactly one value. @@ -100,24 +93,8 @@ :ref:`fixture parametrization ` mechanisms together with ``yield``-style fixtures. -- the ``yield`` syntax is similar to what - :py:func:`contextlib.contextmanager` decorated functions - provide. With pytest fixture functions, the "after yield" part will - always be invoked, independently from the exception status - of the test function which uses the fixture. The pytest - behaviour makes sense if you consider that many different - test functions might use a module or session scoped fixture. - Some test functions might raise exceptions and others not, - so how could pytest re-raise a single exception at the - ``yield`` point in the fixture function? - - lastly ``yield`` introduces more than one way to write fixture functions, so what's the obvious way to a newcomer? - Newcomers reading the docs will see feature examples using the - ``return`` style so should use that, if in doubt. - Others can start experimenting with writing yield-style fixtures - and possibly help evolving them further. -If you want to feedback or participate in the ongoing -discussion, please join our :ref:`contact channels`. -you are most welcome. +If you want to feedback or participate in discussion of the above +topics, please join our :ref:`contact channels`, you are most welcome. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:27:10 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:27:10 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Close branch release-checklist Message-ID: <20150404142710.16636.46978@app07.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/b872d8499737/ Changeset: b872d8499737 Branch: release-checklist User: hpk42 Date: 2015-04-04 14:27:07+00:00 Summary: Close branch release-checklist Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:27:12 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:27:12 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in release-checklist (pull request #266) Message-ID: <20150404142712.8895.89570@app03.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/90703d1292db/ Changeset: 90703d1292db User: hpk42 Date: 2015-04-04 14:27:07+00:00 Summary: Merged in release-checklist (pull request #266) add a release checklist Affected #: 9 files diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- streamlined and documented release process. Also all versions + (in setup.py and documentation generation) are now read + from _pytest/__init__.py. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 CONTRIBUTING.rst --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -147,7 +147,9 @@ $ hg clone ssh://hg at bitbucket.org/YOUR_BITBUCKET_USERNAME/pytest $ cd pytest - $ hg branch your-branch-name + $ hg up pytest-2.7 # if you want to fix a bug for the pytest-2.7 series + $ hg up default # if you want to add a feature bound for the next minor release + $ hg branch your-branch-name # your feature/bugfix branch If you need some help with Mercurial, follow this quick start guide: http://mercurial.selenic.com/wiki/QuickStart @@ -197,7 +199,9 @@ branch: your-branch-name target: pytest-dev/pytest - branch: default + branch: default # if it's a feature + branch: pytest-VERSION # if it's a bugfix + .. _contribution-using-git: diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.7.1.dev' +__version__ = '2.7.1.dev1' diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 doc/en/Makefile --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -15,46 +15,39 @@ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest -regen: - PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" + @echo " showtarget to show the pytest.org target directory" + @echo " install to install docs to pytest.org/SITETARGET" + @echo " install-ldf to install the doc pdf to pytest.org/SITETARGET" + @echo " regen to regenerate pytest examples using the installed pytest" @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* -SITETARGET=dev +SITETARGET=$(shell ./_getdoctarget.py) + +showtarget: + @echo $(SITETARGET) install: html # for access talk to someone with login rights to # pytest-dev at pytest.org to add your ssh key - rsync -avz _build/html/ pytest-dev at pytest.org:/www/pytest.org/$(SITETARGET) + rsync -avz _build/html/ pytest-dev at pytest.org:pytest.org/$(SITETARGET) installpdf: latexpdf - @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev at pytest.org:/www/pytest.org/$(SITETARGET) + @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev at pytest.org:pytest.org/$(SITETARGET) installall: clean install installpdf @echo "done" +regen: + PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 doc/en/_getdoctarget.py --- /dev/null +++ b/doc/en/_getdoctarget.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import py + +def get_version_string(): + fn = py.path.local(__file__).join("..", "..", "..", + "_pytest", "__init__.py") + for line in fn.readlines(): + if "version" in line: + return eval(line.split("=")[-1]) + +def get_minor_version_string(): + return ".".join(get_version_string().split(".")[:2]) + +if __name__ == "__main__": + print (get_minor_version_string()) diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 doc/en/_themes/flask/static/flasky.css_t --- a/doc/en/_themes/flask/static/flasky.css_t +++ b/doc/en/_themes/flask/static/flasky.css_t @@ -12,7 +12,7 @@ {% set link_color = '#000' %} {% set link_hover_color = '#000' %} {% set base_font = 'sans-serif' %} -{% set header_font = 'sans-serif' %} +{% set header_font = 'serif' %} @import url("basic.css"); @@ -265,9 +265,10 @@ content: ":"; } -pre, tt { +pre, tt, code { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; + background: #eee; } img.screenshot { diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,10 +17,13 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = "2.7" -release = "2.7.1.dev" -import sys, os +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import _getdoctarget + +version = _getdoctarget.get_minor_version_string() +release = _getdoctarget.get_version_string() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -54,7 +57,7 @@ # General information about the project. project = u'pytest' -copyright = u'2014, holger krekel' +copyright = u'2015, holger krekel and pytest-dev team' diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 doc/en/release.txt --- /dev/null +++ b/doc/en/release.txt @@ -0,0 +1,54 @@ +pytest release checklist +------------------------- + +For doing a release of pytest (status April 2015) this rough checklist is used: + +1. change version numbers in ``_pytest/__init__.py`` to the to-be-released version. + (the version number in ``setup.py`` reads from that init file as well) + +2. finalize ``./CHANGELOG`` (don't forget the the header). + +3. write ``doc/en/announce/release-VERSION.txt`` + (usually copying from an earlier release version). + +4. regenerate doc examples with ``tox -e regen`` and check with ``hg diff`` + if the differences show regressions. It's a bit of a manual process because + there a large part of the diff is about pytest headers or differences in + speed ("tests took X.Y seconds"). (XXX automate doc/example diffing to ignore + such changes and integrate it into "tox -e regen"). + +5. ``devpi upload`` to `your developer devpi index `_. You can create your own user and index on https://devpi.net, + an inofficial service from the devpi authors. + +6. run ``devpi use INDEX`` and ``devpi test`` from linux and windows machines + and verify test results on the index. On linux typically all environments + pass (April 2015 there is a setup problem with a cx_freeze environment) + but on windows all involving ``pexpect`` fail because pexpect does not exist + on windows and tox does not allow to have platform-specific environments. + Also on windows ``py33-trial`` fails but should probably pass (March 2015). + In any case, py26,py27,py33,py34 are required to pass for all platforms. + +7. You can fix tests/code and repeat number 6. until everything passes. + +8. Once you have sufficiently passing tox tests you can do the actual release:: + + cd doc/en/ + make install # will install to 2.7, 2.8, ... according to _pytest/__init__.py + make install-pdf # optional, requires latex packages installed + ssh pytest-dev at pytest.org # MANUAL: symlink "pytest.org/latest" to the just + # installed release docs + # browse to pytest.org to see + + devpi push pytest-VERSION pypi:NAME + hg ci -m "... finalized pytest-VERSION" + hg tag VERSION + hg push + +9. send out release announcement to pytest-dev at python.org, + testing-in-python at lists.idyll.org and python-announce-list at python.org . + +10. **after the release** bump the version number in ``_pytest/__init__.py``, + to the next Minor release version (i.e. if you released ``pytest-2.8.0``, + set it to ``pytest-2.9.0.dev1``). + +11. already done :) diff -r 1a0edfacf82e465c026e0991b8954b43450e77bb -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 setup.py --- a/setup.py +++ b/setup.py @@ -16,6 +16,15 @@ with open('README.rst') as fd: long_description = fd.read() +def get_version(): + p = os.path.join(os.path.dirname( + os.path.abspath(__file__)), "_pytest", "__init__.py") + with open(p) as f: + for line in f.readlines(): + if "__version__" in line: + return line.strip().split("=")[-1].strip(" '") + raise ValueError("could not read version") + def main(): install_requires = ['py>=1.4.25'] @@ -28,7 +37,7 @@ name='pytest', description='pytest: simple powerful testing with Python', long_description=long_description, - version='2.7.1.dev', + version=get_version(), url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:29:49 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:29:49 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150404142949.25681.92497@app10.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/e419fe9ab7d4/ Changeset: e419fe9ab7d4 Branch: issue660 User: hpk42 Date: 2015-04-01 16:42:48+00:00 Summary: fix issue660: properly report fixture scope mismatches independent from fixture argument ordering. Affected #: 3 files diff -r 0de3f5c1a683a834d27b265d5e8781326d5dad04 -r e419fe9ab7d4217782bf92ca7a04af6825563696 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- fix issue660: properly report scope-mismatch-access errors + independently from ordering of fixture arguments. Also + avoid the pytest internal traceback which does not provide + information to the user. + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 0de3f5c1a683a834d27b265d5e8781326d5dad04 -r e419fe9ab7d4217782bf92ca7a04af6825563696 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1356,12 +1356,7 @@ try: val = cache[cachekey] except KeyError: - __tracebackhide__ = True - if scopemismatch(self.scope, scope): - raise ScopeMismatchError("You tried to access a %r scoped " - "resource with a %r scoped request object" %( - (scope, self.scope))) - __tracebackhide__ = False + self._check_scope(self.fixturename, self.scope, scope) val = setup() cache[cachekey] = val if teardown is not None: @@ -1392,6 +1387,7 @@ if argname == "request": class PseudoFixtureDef: cached_result = (self, [0], None) + scope = "function" return PseudoFixtureDef raise # remove indent to prevent the python3 exception @@ -1435,16 +1431,7 @@ subrequest = SubRequest(self, scope, param, param_index, fixturedef) # check if a higher-level scoped fixture accesses a lower level one - if scope is not None: - __tracebackhide__ = True - if scopemismatch(self.scope, scope): - # try to report something helpful - lines = subrequest._factorytraceback() - raise ScopeMismatchError("You tried to access the %r scoped " - "fixture %r with a %r scoped request object, " - "involved factories\n%s" %( - (scope, argname, self.scope, "\n".join(lines)))) - __tracebackhide__ = False + subrequest._check_scope(argname, self.scope, scope) # clear sys.exc_info before invoking the fixture (python bug?) # if its not explicitly cleared it will leak into the call @@ -1458,6 +1445,18 @@ subrequest.node) return val + def _check_scope(self, argname, invoking_scope, requested_scope): + if argname == "request": + return + if scopemismatch(invoking_scope, requested_scope): + # try to report something helpful + lines = self._factorytraceback() + pytest.fail("ScopeMismatch: you tried to access the %r scoped " + "fixture %r with a %r scoped request object, " + "involved factories\n%s" %( + (requested_scope, argname, invoking_scope, "\n".join(lines))), + pytrace=False) + def _factorytraceback(self): lines = [] for fixturedef in self._get_fixturestack(): @@ -1518,6 +1517,7 @@ def scopemismatch(currentscope, newscope): return scopes.index(newscope) > scopes.index(currentscope) + class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ def __init__(self, argname, request, msg=None): @@ -1867,6 +1867,7 @@ for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) result, arg_cache_key, exc = fixturedef.cached_result + request._check_scope(argname, request.scope, fixturedef.scope) kwargs[argname] = result if argname != "request": fixturedef.addfinalizer(self.finish) diff -r 0de3f5c1a683a834d27b265d5e8781326d5dad04 -r e419fe9ab7d4217782bf92ca7a04af6825563696 testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -906,6 +906,27 @@ "*1 error*" ]) + def test_receives_funcargs_scope_mismatch_issue660(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="function") + def arg1(): + return 1 + + @pytest.fixture(scope="module") + def arg2(arg1): + return arg1 + 1 + + def test_add(arg1, arg2): + assert arg2 == 2 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ScopeMismatch*involved factories*", + "* def arg2*", + "*1 error*" + ]) + def test_funcarg_parametrized_and_used_twice(self, testdir): testdir.makepyfile(""" import pytest https://bitbucket.org/pytest-dev/pytest/commits/0b48b3b5156a/ Changeset: 0b48b3b5156a Branch: pytest-2.7 User: hpk42 Date: 2015-04-04 14:29:38+00:00 Summary: add changelog entries Affected #: 1 file diff -r b872d84997379d9e140576d77516cf4d61f5347a -r 0b48b3b5156a55a09f3c27ce25beaeb156b56173 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,10 @@ - streamlined and documented release process. Also all versions (in setup.py and documentation generation) are now read - from _pytest/__init__.py. + from _pytest/__init__.py. Thanks Holger Krekel. + +- fixed docs to remove the notion that yield-fixtures are experimental. + They are here to stay :) Thanks Bruno Oliveira. 2.7.0 (compared to 2.6.4) ----------------------------- Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:35:26 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:35:26 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150404143526.19550.81179@app13.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/45921b2e6400/ Changeset: 45921b2e6400 User: hpk42 Date: 2015-04-04 14:32:25+00:00 Summary: shift default to 2.8.0.dev Affected #: 2 files diff -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 -r 45921b2e640011d8f169a7f13fd79218f88c7495 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,7 @@ -2.7.1.dev (compared to 2.7.0) +2.8.0.dev (compared to 2.7.X) ----------------------------- -- streamlined and documented release process. Also all versions - (in setup.py and documentation generation) are now read - from _pytest/__init__.py. + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 90703d1292db3ba2bda31b25baf74a7a14e5e568 -r 45921b2e640011d8f169a7f13fd79218f88c7495 _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.7.1.dev1' +__version__ = '2.8.0.dev1' https://bitbucket.org/pytest-dev/pytest/commits/0376bcf524d0/ Changeset: 0376bcf524d0 Branch: issue660 User: hpk42 Date: 2015-04-04 14:35:14+00:00 Summary: merge pytest-2.7 branch Affected #: 9 files diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,12 @@ avoid the pytest internal traceback which does not provide information to the user. +- streamlined and documented release process. Also all versions + (in setup.py and documentation generation) are now read + from _pytest/__init__.py. Thanks Holger Krekel. + +- fixed docs to remove the notion that yield-fixtures are experimental. + They are here to stay :) Thanks Bruno Oliveira. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d CONTRIBUTING.rst --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -147,7 +147,9 @@ $ hg clone ssh://hg at bitbucket.org/YOUR_BITBUCKET_USERNAME/pytest $ cd pytest - $ hg branch your-branch-name + $ hg up pytest-2.7 # if you want to fix a bug for the pytest-2.7 series + $ hg up default # if you want to add a feature bound for the next minor release + $ hg branch your-branch-name # your feature/bugfix branch If you need some help with Mercurial, follow this quick start guide: http://mercurial.selenic.com/wiki/QuickStart @@ -197,7 +199,9 @@ branch: your-branch-name target: pytest-dev/pytest - branch: default + branch: default # if it's a feature + branch: pytest-VERSION # if it's a bugfix + .. _contribution-using-git: diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.7.1.dev' +__version__ = '2.7.1.dev1' diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d doc/en/Makefile --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -15,46 +15,39 @@ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest -regen: - PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" + @echo " showtarget to show the pytest.org target directory" + @echo " install to install docs to pytest.org/SITETARGET" + @echo " install-ldf to install the doc pdf to pytest.org/SITETARGET" + @echo " regen to regenerate pytest examples using the installed pytest" @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* -SITETARGET=dev +SITETARGET=$(shell ./_getdoctarget.py) + +showtarget: + @echo $(SITETARGET) install: html # for access talk to someone with login rights to # pytest-dev at pytest.org to add your ssh key - rsync -avz _build/html/ pytest-dev at pytest.org:/www/pytest.org/$(SITETARGET) + rsync -avz _build/html/ pytest-dev at pytest.org:pytest.org/$(SITETARGET) installpdf: latexpdf - @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev at pytest.org:/www/pytest.org/$(SITETARGET) + @scp $(BUILDDIR)/latex/pytest.pdf pytest-dev at pytest.org:pytest.org/$(SITETARGET) installall: clean install installpdf @echo "done" +regen: + PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d doc/en/_getdoctarget.py --- /dev/null +++ b/doc/en/_getdoctarget.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import py + +def get_version_string(): + fn = py.path.local(__file__).join("..", "..", "..", + "_pytest", "__init__.py") + for line in fn.readlines(): + if "version" in line: + return eval(line.split("=")[-1]) + +def get_minor_version_string(): + return ".".join(get_version_string().split(".")[:2]) + +if __name__ == "__main__": + print (get_minor_version_string()) diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d doc/en/_themes/flask/static/flasky.css_t --- a/doc/en/_themes/flask/static/flasky.css_t +++ b/doc/en/_themes/flask/static/flasky.css_t @@ -12,7 +12,7 @@ {% set link_color = '#000' %} {% set link_hover_color = '#000' %} {% set base_font = 'sans-serif' %} -{% set header_font = 'sans-serif' %} +{% set header_font = 'serif' %} @import url("basic.css"); @@ -265,9 +265,10 @@ content: ":"; } -pre, tt { +pre, tt, code { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; + background: #eee; } img.screenshot { diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,10 +17,13 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = "2.7" -release = "2.7.1.dev" -import sys, os +import os, sys +sys.path.insert(0, os.path.dirname(__file__)) +import _getdoctarget + +version = _getdoctarget.get_minor_version_string() +release = _getdoctarget.get_version_string() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -54,7 +57,7 @@ # General information about the project. project = u'pytest' -copyright = u'2014, holger krekel' +copyright = u'2015, holger krekel and pytest-dev team' diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d doc/en/release.txt --- /dev/null +++ b/doc/en/release.txt @@ -0,0 +1,54 @@ +pytest release checklist +------------------------- + +For doing a release of pytest (status April 2015) this rough checklist is used: + +1. change version numbers in ``_pytest/__init__.py`` to the to-be-released version. + (the version number in ``setup.py`` reads from that init file as well) + +2. finalize ``./CHANGELOG`` (don't forget the the header). + +3. write ``doc/en/announce/release-VERSION.txt`` + (usually copying from an earlier release version). + +4. regenerate doc examples with ``tox -e regen`` and check with ``hg diff`` + if the differences show regressions. It's a bit of a manual process because + there a large part of the diff is about pytest headers or differences in + speed ("tests took X.Y seconds"). (XXX automate doc/example diffing to ignore + such changes and integrate it into "tox -e regen"). + +5. ``devpi upload`` to `your developer devpi index `_. You can create your own user and index on https://devpi.net, + an inofficial service from the devpi authors. + +6. run ``devpi use INDEX`` and ``devpi test`` from linux and windows machines + and verify test results on the index. On linux typically all environments + pass (April 2015 there is a setup problem with a cx_freeze environment) + but on windows all involving ``pexpect`` fail because pexpect does not exist + on windows and tox does not allow to have platform-specific environments. + Also on windows ``py33-trial`` fails but should probably pass (March 2015). + In any case, py26,py27,py33,py34 are required to pass for all platforms. + +7. You can fix tests/code and repeat number 6. until everything passes. + +8. Once you have sufficiently passing tox tests you can do the actual release:: + + cd doc/en/ + make install # will install to 2.7, 2.8, ... according to _pytest/__init__.py + make install-pdf # optional, requires latex packages installed + ssh pytest-dev at pytest.org # MANUAL: symlink "pytest.org/latest" to the just + # installed release docs + # browse to pytest.org to see + + devpi push pytest-VERSION pypi:NAME + hg ci -m "... finalized pytest-VERSION" + hg tag VERSION + hg push + +9. send out release announcement to pytest-dev at python.org, + testing-in-python at lists.idyll.org and python-announce-list at python.org . + +10. **after the release** bump the version number in ``_pytest/__init__.py``, + to the next Minor release version (i.e. if you released ``pytest-2.8.0``, + set it to ``pytest-2.9.0.dev1``). + +11. already done :) diff -r e419fe9ab7d4217782bf92ca7a04af6825563696 -r 0376bcf524d03f1c6605627bcbb45d2902c2157d setup.py --- a/setup.py +++ b/setup.py @@ -16,6 +16,15 @@ with open('README.rst') as fd: long_description = fd.read() +def get_version(): + p = os.path.join(os.path.dirname( + os.path.abspath(__file__)), "_pytest", "__init__.py") + with open(p) as f: + for line in f.readlines(): + if "__version__" in line: + return line.strip().split("=")[-1].strip(" '") + raise ValueError("could not read version") + def main(): install_requires = ['py>=1.4.25'] @@ -28,7 +37,7 @@ name='pytest', description='pytest: simple powerful testing with Python', long_description=long_description, - version='2.7.1.dev', + version=get_version(), url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Apr 4 16:36:40 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 04 Apr 2015 14:36:40 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: add me as author of PR Message-ID: <20150404143640.4399.70655@app04.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/2f5fe9b1e79b/ Changeset: 2f5fe9b1e79b Branch: issue660 User: hpk42 Date: 2015-04-04 14:36:32+00:00 Summary: add me as author of PR Affected #: 1 file diff -r 0376bcf524d03f1c6605627bcbb45d2902c2157d -r 2f5fe9b1e79b514014da0bff0e5a5a4791a0d863 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ - fix issue660: properly report scope-mismatch-access errors independently from ordering of fixture arguments. Also avoid the pytest internal traceback which does not provide - information to the user. + information to the user. Thanks Holger Krekel. - streamlined and documented release process. Also all versions (in setup.py and documentation generation) are now read Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Sat Apr 4 16:57:22 2015 From: builds at drone.io (Drone.io Build) Date: Sat, 04 Apr 2015 14:57:22 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 57 Message-ID: <20150404144715.16258.56801@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/57 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4102:2f5fe9b1e79b Author : holger krekel Branch : default Message: add me as author of PR -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Sat Apr 4 16:59:01 2015 From: builds at drone.io (Drone.io Build) Date: Sat, 04 Apr 2015 14:59:01 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 58 Message-ID: <20150404145901.16501.47728@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/58 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3942:b872d8499737 Author : holger krekel Branch : release-checklist Message: Close branch release-checklist -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Sat Apr 4 17:21:34 2015 From: builds at drone.io (Drone.io Build) Date: Sat, 04 Apr 2015 15:21:34 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 60 Message-ID: <20150404152132.98392.78636@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/60 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3946:2f5fe9b1e79b Author : holger krekel Branch : issue660 Message: add me as author of PR -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Sat Apr 4 23:40:24 2015 From: issues-reply at bitbucket.org (Jason R. Coombs) Date: Sat, 04 Apr 2015 21:40:24 -0000 Subject: [Pytest-commit] Issue #710: Add support for unicode literals to (pytest-dev/pytest) Message-ID: <20150404214024.2381.10541@app11.ash-private.bitbucket.org> New issue 710: Add support for unicode literals to https://bitbucket.org/pytest-dev/pytest/issue/710/add-support-for-unicode-literals-to Jason R. Coombs: I like to use doctests for some use cases, and pytest makes inclusion of them in tests suites quite elegant. However, doctests have a particularly hard problem with Python2/3 compatibility (because unicode strings render as u'' in Python 2 and '' in Python 3). The NLTK project has built a [nose plugin](https://github.com/nltk/nltk/blob/develop/nltk/test/doctest_nose_plugin.py) to better support the disparity. I'd like to see pytest support this functionality as well. From issues-reply at bitbucket.org Sun Apr 5 14:26:28 2015 From: issues-reply at bitbucket.org (simonb) Date: Sun, 05 Apr 2015 12:26:28 -0000 Subject: [Pytest-commit] Issue #233: setup.py example will hang forever (hpk42/tox) Message-ID: <20150405122628.28560.41694@app05.ash-private.bitbucket.org> New issue 233: setup.py example will hang forever https://bitbucket.org/hpk42/tox/issue/233/setuppy-example-will-hang-forever simonb: In the [setuptools integration example](https://testrun.org/tox/latest/example/basic.html#integration-with-setuptools-distribute-test-commands) in the documentation The script will hang forever if there are no arguments passed to tox. This is because the argument to shlex.split() is None so it waits to read from stdin. I suggest to change the example to something like the below. ``` #!python def run_tests(self): import tox import shlex args = self.tox_args if args: args = shlex.split(self.tox_args) errno = tox.cmdline(args=args) sys.exit(errno) ``` From issues-reply at bitbucket.org Mon Apr 6 12:45:58 2015 From: issues-reply at bitbucket.org (Matthew Schinckel) Date: Mon, 06 Apr 2015 10:45:58 -0000 Subject: [Pytest-commit] Issue #234: Have some level of non-concurrency. (hpk42/tox) Message-ID: <20150406104558.31693.94215@app09.ash-private.bitbucket.org> New issue 234: Have some level of non-concurrency. https://bitbucket.org/hpk42/tox/issue/234/have-some-level-of-non-concurrency Matthew Schinckel: I have a really nice workflow for managing coverage testing using tox: I have an envlist that looks something like: clean,{py27,py33,py34,pypy}-django{17,18},status The first listed environment simply removes the existing coverage data, and the final one runs the coverage status reports. The issue is that with detox, the env `status` runs before any of the other envs have had a chance to finish. I'd love some way of either marking that a specific env should _not_ run under detox (as it gives an error that no coverage data was found), or should run after everything else. The only workaround I have come up with is to remove the `status` env from the envlist, and run `tox -e status` after `detox`. From issues-reply at bitbucket.org Mon Apr 6 20:50:07 2015 From: issues-reply at bitbucket.org (Diogo Campos) Date: Mon, 06 Apr 2015 18:50:07 -0000 Subject: [Pytest-commit] Issue #711: Confusing exit code when pkg_resources fails (pytest-dev/pytest) Message-ID: <20150406185007.12924.57558@app13.ash-private.bitbucket.org> New issue 711: Confusing exit code when pkg_resources fails https://bitbucket.org/pytest-dev/pytest/issue/711/confusing-exit-code-when-pkg_resources Diogo Campos: When pytest fails to start (e.g., missing a dependency), the test command fails with something like this: ``` #!python Traceback (most recent call last): File "XXX/pytest-2.6.4/Scripts/py.test-script.py", line 5, in from pkg_resources import load_entry_point File "XXX\setuptools-12.2\lib\site-packages\pkg_resources\__init__.py", line 3020, in working_set = WorkingSet._build_master() File "XXX\setuptools-12.2\lib\site-packages\pkg_resources\__init__.py", line 614, in _build_master ws.require(__requires__) File "XXX\setuptools-12.2\lib\site-packages\pkg_resources\__init__.py", line 920, in require needed = self.resolve(parse_requirements(requirements)) File "XXX\setuptools-12.2\lib\site-packages\pkg_resources\__init__.py", line 807, in resolve raise DistributionNotFound(req) pkg_resources.DistributionNotFound: colorama ``` And the command returns **1**. This is confusing because pytest states that exit code **1** means "tests failed", while "internal error" or "usage error" should return **2**/**3**. I'm not sure if there is much that can be done to fix this, since that **1** is given by setuptools, and not pytest. Overall, using such a common error exit code **1** to indicate anything other than errors might be a bad idea. From issues-reply at bitbucket.org Wed Apr 8 15:35:50 2015 From: issues-reply at bitbucket.org (tibor_arpas) Date: Wed, 08 Apr 2015 13:35:50 -0000 Subject: [Pytest-commit] Issue #712: some issue with setup and teardown in pytest suite (pytest-dev/pytest) Message-ID: <20150408133550.17406.91449@app11.ash-private.bitbucket.org> New issue 712: some issue with setup and teardown in pytest suite https://bitbucket.org/pytest-dev/pytest/issue/712/some-issue-with-setup-and-teardown-in tibor_arpas: I'm on platform 'darwin'. I select only one test, which by chance is skipped. I get an instead of a success. py.test testing/test_argcomplete.py::TestArgComplete::test_compare_with_compgen ============================= test session starts ============================== platform darwin -- Python 2.7.8 -- py-1.4.26 -- pytest-2.8.0.dev1 rootdir: /Users/tibor/tmonworkspace/pytest, inifile: tox.ini collected 3 items testing/test_argcomplete.py sE ==================================== ERRORS ==================================== ________ ERROR at teardown of TestArgComplete.test_compare_with_compgen ________ item = __multicall__ = , '__multicall__': , 'nextitem': None}> def pytest_runtest_teardown(item, __multicall__): > item.config._basedir.chdir() E AttributeError: 'Config' object has no attribute '_basedir' testing/conftest.py:70: AttributeError =========================== short test summary info ============================ SKIP [1] /Users/tibor/tmonworkspace/pytest/_pytest/skipping.py:140: condition: sys.platform in ('win32', 'darwin') From issues-reply at bitbucket.org Wed Apr 8 16:56:20 2015 From: issues-reply at bitbucket.org (Richard Barrell) Date: Wed, 08 Apr 2015 14:56:20 -0000 Subject: [Pytest-commit] Issue #713: AttributeError: ReprFailDoctest instance has no attribute 'reprcrash' with --doctest-modules, --junit-xml and at a failing doctest. (pytest-dev/pytest) Message-ID: <20150408145620.14238.83343@app06.ash-private.bitbucket.org> New issue 713: AttributeError: ReprFailDoctest instance has no attribute 'reprcrash' with --doctest-modules, --junit-xml and at a failing doctest. https://bitbucket.org/pytest-dev/pytest/issue/713/attributeerror-reprfaildoctest-instance Richard Barrell: Reproduction: in `a/__init__.py`: ``` #!python def foo(): """ >>> 1 + 1 3 """ pass ``` Without --junit-xml: ``` #!text $ py.test a --doctest-modules ============================= test session starts ============================== platform linux2 -- Python 2.7.5 -- py-1.4.26 -- pytest-2.7.0 rootdir: /home/RichardB/a, inifile: plugins: cov collected 1 items a/__init__.py F =================================== FAILURES =================================== _______________________________ [doctest] a.foo ________________________________ 002 """ 003 >>> 1 + 1 Expected: 3 Got: 2 /home/RichardB/a/__init__.py:3: DocTestFailure =========================== 1 failed in 0.01 seconds =========================== ``` And that's fine. However, when I try with --junit-xml: ``` #!text $ py.test a --doctest-modules --junit-xml=junit.xml ============================= test session starts ============================== platform linux2 -- Python 2.7.5 -- py-1.4.26 -- pytest-2.7.0 rootdir: /home/RichardB/a, inifile: plugins: cov collected 1 items a/__init__.py F INTERNALERROR> Traceback (most recent call last): INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/main.py", line 84, in wrap_session INTERNALERROR> doit(config, session) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/main.py", line 122, in _main INTERNALERROR> config.hook.pytest_runtestloop(session=session) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 521, in __call__ INTERNALERROR> return self._docall(self.methods, kwargs) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 528, in _docall INTERNALERROR> firstresult=self.firstresult).execute() INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 394, in execute INTERNALERROR> res = method(*args) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/main.py", line 142, in pytest_runtestloop INTERNALERROR> item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 521, in __call__ INTERNALERROR> return self._docall(self.methods, kwargs) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 528, in _docall INTERNALERROR> firstresult=self.firstresult).execute() INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 393, in execute INTERNALERROR> return wrapped_call(method(*args), self.execute) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 113, in wrapped_call INTERNALERROR> return call_outcome.get_result() INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 138, in get_result INTERNALERROR> py.builtin._reraise(*ex) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 123, in __init__ INTERNALERROR> self.result = func() INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 394, in execute INTERNALERROR> res = method(*args) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/runner.py", line 65, in pytest_runtest_protocol INTERNALERROR> runtestprotocol(item, nextitem=nextitem) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/runner.py", line 75, in runtestprotocol INTERNALERROR> reports.append(call_and_report(item, "call", log)) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/runner.py", line 123, in call_and_report INTERNALERROR> hook.pytest_runtest_logreport(report=report) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 521, in __call__ INTERNALERROR> return self._docall(self.methods, kwargs) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 528, in _docall INTERNALERROR> firstresult=self.firstresult).execute() INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/core.py", line 394, in execute INTERNALERROR> res = method(*args) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/junitxml.py", line 193, in pytest_runtest_logreport INTERNALERROR> self.append_failure(report) INTERNALERROR> File "/home/RichardB/buildouts/pytest/eggs/pytest-2.7.0-py2.7.egg/_pytest/junitxml.py", line 132, in append_failure INTERNALERROR> message = report.longrepr.reprcrash.message INTERNALERROR> AttributeError: ReprFailDoctest instance has no attribute 'reprcrash' =========================== 1 failed in 0.02 seconds =========================== ``` From issues-reply at bitbucket.org Wed Apr 8 17:00:06 2015 From: issues-reply at bitbucket.org (Alina Berdnikova) Date: Wed, 08 Apr 2015 15:00:06 -0000 Subject: [Pytest-commit] Issue #714: Request the feature to apply indirect=True on particular argnames (pytest-dev/pytest) Message-ID: <20150408150006.19247.91305@app14.ash-private.bitbucket.org> New issue 714: Request the feature to apply indirect=True on particular argnames https://bitbucket.org/pytest-dev/pytest/issue/714/request-the-feature-to-apply-indirect-true Alina Berdnikova: According to http://pytest.org/latest/parametrize.html#the-metafunc-object, if indirect=True it'll pass each argvalue to its corresponding fixture function. It would be great if there was a way to specify which arguments are to be passed as params to corresponding fixtures and which are to be passed directly to test -- for example, via explicitly listing names of those indirectly-parametrized fixtures. If argument name is listed in indirect= list, but corresponding fixture is nowhere to be found, an error should be raised. And if the argument not listed in the indirect= clause, it should be passed directly as a test parameter despite corresponding fixture being defined or not. I'm expecting this code ``` #!python @pytest.fixture(scope='function') def bar(request): return 'little %s' % request.param @pytest.fixture(scope='function') def foo(request): return 'great %s' % request.param @pytest.mark.parametrize('foo, bar, spam', [('ololol', 'lalala', 'cococo'), ('pewpew', 'pawpaw', 'pafpaf')], indirect=('bar', 'foo')) def test_one(foo, bar, spam): print foo, bar, spam ``` to output this: ``` #!python ============================= test session starts ============================== platform darwin -- Python 2.7.5 -- py-1.4.26 -- pytest-2.6.4 -- /Users/freakbelka/.venv/bin/python plugins: yadt collecting ... collected 2 items test_1.py::test_one[ololol-lalala-cococo] great ololol little lalala cococo PASSED test_1.py::test_one[pewpew-pawpaw-pafpaf] great pewpew little pawpaw pafpaf PASSED =========================== 2 passed in 2.59 seconds =========================== ``` Responsible: hpk42 From issues-reply at bitbucket.org Thu Apr 9 10:42:43 2015 From: issues-reply at bitbucket.org (pombredanne) Date: Thu, 09 Apr 2015 08:42:43 -0000 Subject: [Pytest-commit] Issue #715: Full diff of sequences should be displayed on assertion failures when verbose (pytest-dev/pytest) Message-ID: <20150409084243.17445.32666@app08.ash-private.bitbucket.org> New issue 715: Full diff of sequences should be displayed on assertion failures when verbose https://bitbucket.org/pytest-dev/pytest/issue/715/full-diff-of-sequences-should-be-displayed pombredanne: Today when tow sequences are compared, only the first different element is display with an ellipsis for the rest and there is no way to display a full sequence diff. This means that other logging/print mechanisms must be used to display these on failure. The proposed fix (pull request coming) is to enable a full sequence diff. Note that while `_pytest.assertion.util._compare_eq_sequence` does not allow for these full diffs, `_pytest.assertion.util._compare_eq_iterable` does. And that `_pytest.assertion.util._compare_eq_sequence` has some commented out difflib calling code meaning that someone though about this at some point of time in the past... From issues-reply at bitbucket.org Fri Apr 10 01:46:21 2015 From: issues-reply at bitbucket.org (dariuspbom) Date: Thu, 09 Apr 2015 23:46:21 -0000 Subject: [Pytest-commit] Issue #716: xfail should expect the test to fail (pytest-dev/pytest) Message-ID: <20150409234621.10772.25924@app08.ash-private.bitbucket.org> New issue 716: xfail should expect the test to fail https://bitbucket.org/pytest-dev/pytest/issue/716/xfail-should-expect-the-test-to-fail dariuspbom: xfail used to expect the test to fail and if it unexpectedly passed then it was considered a failure and would mark as xpass and fail the test job with an exit code. This changed it appears to be used for flakely tests that can pass or fail. I can see that this might be useful but it is also useful to have an expected result for a test and to mark the test as a failure and the exit code as a failure if it doesn't match the expected outcome. I suggest that there should be a marker that expects the test to fail and if it doesn't match that expectation then the it should be marked xpass and fail with non zero exit code. Then there should be another marker for flakely tests where the test result can be pass or fail and the test is counted as either xfail or xpass but either was expected and allowed so the exit code is zero. Perhaps use xfail marker for tests that are expected to fail and use xfailorxpass marker for tests that may pass or fail. Given that this is a testing framework I think it is highly desirable to be able to specify the expected result and for that expected result to be verified. If the expected result is not the actual result then there should be an exit code to flag this to other tools like jenkins. Responsible: hpk42 From commits-noreply at bitbucket.org Sat Apr 11 21:41:44 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 11 Apr 2015 19:41:44 -0000 Subject: [Pytest-commit] commit/pytest: flub: Some docstrings for the pytester plugin Message-ID: <20150411194144.24725.84795@app05.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/65a7f821cb6e/ Changeset: 65a7f821cb6e User: flub Date: 2015-04-11 16:07:37+00:00 Summary: Some docstrings for the pytester plugin These aren't quite complete but are a jolly good start anyway. It seems better to commit this now then leave it lingering until it gets lost. Affected #: 1 file diff -r 45921b2e640011d8f169a7f13fd79218f88c7495 -r 65a7f821cb6e582ef5b295e458dae90508e7a4c8 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -50,6 +50,13 @@ class HookRecorder: + """Record all hooks called in a plugin manager. + + This wraps all the hook calls in the plugin manager, recording + each call before propagating the normal calls. + + """ + def __init__(self, pluginmanager): self._pluginmanager = pluginmanager self.calls = [] @@ -180,6 +187,20 @@ rex_outcome = re.compile("(\d+) (\w+)") class RunResult: + """The result of running a command. + + Attributes: + + :ret: The return value. + :outlines: List of lines captured from stdout. + :errlines: List of lines captures from stderr. + :stdout: LineMatcher of stdout, use ``stdout.str()`` to + reconstruct stdout or the commonly used + ``stdout.fnmatch_lines()`` method. + :stderrr: LineMatcher of stderr. + :duration: Duration in seconds. + + """ def __init__(self, ret, outlines, errlines, duration): self.ret = ret self.outlines = outlines @@ -199,6 +220,26 @@ return d class TmpTestdir: + """Temporary test directory with tools to test/run py.test itself. + + This is based on the ``tmpdir`` fixture but provides a number of + methods which aid with testing py.test itself. Unless + :py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as + current working directory. + + Attributes: + + :tmpdir: The :py:class:`py.path.local` instance of the temporary + directory. + + :plugins: A list of plugins to use with :py:meth:`parseconfig` and + :py:meth:`runpytest`. Initially this is an empty list but + plugins can be added to the list. The type of items to add to + the list depend on the method which uses them so refer to them + for details. + + """ + def __init__(self, request): self.request = request self.Config = request.config.__class__ @@ -221,6 +262,14 @@ return "" % (self.tmpdir,) def finalize(self): + """Clean up global state artifacts. + + Some methods modify the global interpreter state and this + tries to clean this up. It does not remove the temporary + directlry however so it can be looked at after the test run + has finished. + + """ for p in self._syspathremove: sys.path.remove(p) if hasattr(self, '_olddir'): @@ -233,12 +282,18 @@ del sys.modules[name] def make_hook_recorder(self, pluginmanager): + """Create a new :py:class:`HookRecorder` for a PluginManager.""" assert not hasattr(pluginmanager, "reprec") pluginmanager.reprec = reprec = HookRecorder(pluginmanager) self.request.addfinalizer(reprec.finish_recording) return reprec def chdir(self): + """Cd into the temporary directory. + + This is done automatically upon instantiation. + + """ old = self.tmpdir.chdir() if not hasattr(self, '_olddir'): self._olddir = old @@ -267,42 +322,82 @@ ret = p return ret + def makefile(self, ext, *args, **kwargs): + """Create a new file in the testdir. - def makefile(self, ext, *args, **kwargs): + ext: The extension the file should use, including the dot. + E.g. ".py". + + args: All args will be treated as strings and joined using + newlines. The result will be written as contents to the + file. The name of the file will be based on the test + function requesting this fixture. + E.g. "testdir.makefile('.txt', 'line1', 'line2')" + + kwargs: Each keyword is the name of a file, while the value of + it will be written as contents of the file. + E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')" + + """ return self._makefile(ext, args, kwargs) def makeconftest(self, source): + """Write a contest.py file with 'source' as contents.""" return self.makepyfile(conftest=source) def makeini(self, source): + """Write a tox.ini file with 'source' as contents.""" return self.makefile('.ini', tox=source) def getinicfg(self, source): + """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) return py.iniconfig.IniConfig(p)['pytest'] def makepyfile(self, *args, **kwargs): + """Shortcut for .makefile() with a .py extension.""" return self._makefile('.py', args, kwargs) def maketxtfile(self, *args, **kwargs): + """Shortcut for .makefile() with a .txt extension.""" return self._makefile('.txt', args, kwargs) def syspathinsert(self, path=None): + """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. + + This is undone automatically after the test. + """ if path is None: path = self.tmpdir sys.path.insert(0, str(path)) self._syspathremove.append(str(path)) def mkdir(self, name): + """Create a new (sub)directory.""" return self.tmpdir.mkdir(name) def mkpydir(self, name): + """Create a new python package. + + This creates a (sub)direcotry with an empty ``__init__.py`` + file so that is recognised as a python package. + + """ p = self.mkdir(name) p.ensure("__init__.py") return p Session = Session def getnode(self, config, arg): + """Return the collection node of a file. + + :param config: :py:class:`_pytest.config.Config` instance, see + :py:meth:`parseconfig` and :py:meth:`parseconfigure` to + create the configuration. + + :param arg: A :py:class:`py.path.local` instance of the file. + + """ session = Session(config) assert '::' not in str(arg) p = py.path.local(arg) @@ -312,6 +407,15 @@ return res def getpathnode(self, path): + """Return the collection node of a file. + + This is like :py:meth:`getnode` but uses + :py:meth:`parseconfigure` to create the (configured) py.test + Config instance. + + :param path: A :py:class:`py.path.local` instance of the file. + + """ config = self.parseconfigure(path) session = Session(config) x = session.fspath.bestrelpath(path) @@ -321,6 +425,12 @@ return res def genitems(self, colitems): + """Generate all test items from a collection node. + + This recurses into the collection node and returns a list of + all the test items contained within. + + """ session = colitems[0].session result = [] for colitem in colitems: @@ -328,6 +438,14 @@ return result def runitem(self, source): + """Run the "test_func" Item. + + The calling test instance (the class which contains the test + method) must provide a ``.getrunner()`` method which should + return a runner which can run the test protocol for a single + item, like e.g. :py:func:`_pytest.runner.runtestprotocol`. + + """ # used from runner functional tests item = self.getitem(source) # the test class where we are called from wants to provide the runner @@ -336,11 +454,32 @@ return runner(item) def inline_runsource(self, source, *cmdlineargs): + """Run a test module in process using ``pytest.main()``. + + This run writes "source" into a temporary file and runs + ``pytest.main()`` on it, returning a :py:class:`HookRecorder` + instance for the result. + + :param source: The source code of the test module. + + :param cmdlineargs: Any extra command line arguments to use. + + :return: :py:class:`HookRecorder` instance of the result. + + """ p = self.makepyfile(source) l = list(cmdlineargs) + [p] return self.inline_run(*l) def inline_runsource1(self, *args): + """Run a test module in process using ``pytest.main()``. + + This behaves exactly like :py:meth:`inline_runsource` and + takes identical arguments. However the return value is a list + of the reports created by the pytest_runtest_logreport hook + during the run. + + """ args = list(args) source = args.pop() p = self.makepyfile(source) @@ -351,14 +490,45 @@ return reports[1] def inline_genitems(self, *args): + """Run ``pytest.main(['--collectonly'])`` in-process. + + Retuns a tuple of the collected items and a + :py:class:`HookRecorder` instance. + + """ return self.inprocess_run(list(args) + ['--collectonly']) def inprocess_run(self, args, plugins=()): + """Run ``pytest.main()`` in-process, return Items and a HookRecorder. + + This runs the :py:func:`pytest.main` function to run all of + py.test inside the test process itself like + :py:meth:`inline_run`. However the return value is a tuple of + the collection items and a :py:class:`HookRecorder` instance. + + """ rec = self.inline_run(*args, plugins=plugins) items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec def inline_run(self, *args, **kwargs): + """Run ``pytest.main()`` in-process, returning a HookRecorder. + + This runs the :py:func:`pytest.main` function to run all of + py.test inside the test process itself. This means it can + return a :py:class:`HookRecorder` instance which gives more + detailed results from then run then can be done by matching + stdout/stderr from :py:meth:`runpytest`. + + :param args: Any command line arguments to pass to + :py:func:`pytest.main`. + + :param plugin: (keyword-only) Extra plugin instances the + ``pytest.main()`` instance should use. + + :return: A :py:class:`HookRecorder` instance. + + """ rec = [] class Collect: def pytest_configure(x, config): @@ -372,6 +542,17 @@ return reprec def parseconfig(self, *args): + """Return a new py.test Config instance from given commandline args. + + This invokes the py.test bootstrapping code in _pytest.config + to create a new :py:class:`_pytest.core.PluginManager` and + call the pytest_cmdline_parse hook to create new + :py:class:`_pytest.config.Config` instance. + + If :py:attr:`plugins` has been populated they should be plugin + modules which will be registered with the PluginManager. + + """ args = [str(x) for x in args] for x in args: if str(x).startswith('--basetemp'): @@ -392,12 +573,31 @@ return config def parseconfigure(self, *args): + """Return a new py.test configured Config instance. + + This returns a new :py:class:`_pytest.config.Config` instance + like :py:meth:`parseconfig`, but also calls the + pytest_configure hook. + + """ config = self.parseconfig(*args) config.do_configure() self.request.addfinalizer(config.do_unconfigure) return config def getitem(self, source, funcname="test_func"): + """Return the test item for a test function. + + This writes the source to a python file and runs py.test's + collection on the resulting module, returning the test item + for the requested function name. + + :param source: The module source. + + :param funcname: The name of the test function for which the + Item must be returned. + + """ items = self.getitems(source) for item in items: if item.name == funcname: @@ -406,10 +606,32 @@ funcname, source, items) def getitems(self, source): + """Return all test items collected from the module. + + This writes the source to a python file and runs py.test's + collection on the resulting module, returning all test items + contained within. + + """ modcol = self.getmodulecol(source) return self.genitems([modcol]) def getmodulecol(self, source, configargs=(), withinit=False): + """Return the module collection node for ``source``. + + This writes ``source`` to a file using :py:meth:`makepyfile` + and then runs the py.test collection on it, returning the + collection node for the test module. + + :param source: The source code of the module to collect. + + :param configargs: Any extra arguments to pass to + :py:meth:`parseconfigure`. + + :param withinit: Whether to also write a ``__init__.py`` file + to the temporarly directory to ensure it is a package. + + """ kw = {self.request.function.__name__: py.code.Source(source).strip()} path = self.makepyfile(**kw) if withinit: @@ -419,11 +641,30 @@ return node def collect_by_name(self, modcol, name): + """Return the collection node for name from the module collection. + + This will search a module collection node for a collection + node matching the given name. + + :param modcol: A module collection node, see + :py:meth:`getmodulecol`. + + :param name: The name of the node to return. + + """ for colitem in modcol._memocollect(): if colitem.name == name: return colitem def popen(self, cmdargs, stdout, stderr, **kw): + """Invoke subprocess.Popen. + + This calls subprocess.Popen making sure the current working + directory is the PYTHONPATH. + + You probably want to use :py:meth:`run` instead. + + """ env = os.environ.copy() env['PYTHONPATH'] = os.pathsep.join(filter(None, [ str(os.getcwd()), env.get('PYTHONPATH', '')])) @@ -432,6 +673,14 @@ stdout=stdout, stderr=stderr, **kw) def run(self, *cmdargs): + """Run a command with arguments. + + Run a process using subprocess.Popen saving the stdout and + stderr. + + Returns a :py:class:`RunResult`. + + """ return self._run(*cmdargs) def _run(self, *cmdargs): @@ -469,6 +718,14 @@ print("couldn't print to %s because of encoding" % (fp,)) def runpybin(self, scriptname, *args): + """Run a py.* tool with arguments. + + This can realy only be used to run py.test, you probably want + :py:meth:`runpytest` instead. + + Returns a :py:class:`RunResult`. + + """ fullargs = self._getpybinargs(scriptname) + args return self.run(*fullargs) @@ -482,6 +739,16 @@ pytest.skip("cannot run %r with --no-tools-on-path" % scriptname) def runpython(self, script, prepend=True): + """Run a python script. + + If ``prepend`` is True then the directory from which the py + package has been imported will be prepended to sys.path. + + Returns a :py:class:`RunResult`. + + """ + # XXX The prepend feature is probably not very useful since the + # split of py and pytest. if prepend: s = self._getsysprepend() if s: @@ -496,10 +763,23 @@ return s def runpython_c(self, command): + """Run python -c "command", return a :py:class:`RunResult`.""" command = self._getsysprepend() + command return self.run(sys.executable, "-c", command) def runpytest(self, *args): + """Run py.test as a subprocess with given arguments. + + Any plugins added to the :py:attr:`plugins` list will added + using the ``-p`` command line option. Addtionally + ``--basetemp`` is used put any temporary files and directories + in a numbered directory prefixed with "runpytest-" so they do + not conflict with the normal numberd pytest location for + temporary files and directories. + + Returns a :py:class:`RunResult`. + + """ p = py.path.local.make_numbered_dir(prefix="runpytest-", keep=None, rootdir=self.tmpdir) args = ('--basetemp=%s' % p, ) + args @@ -515,6 +795,14 @@ return self.runpybin("py.test", *args) def spawn_pytest(self, string, expect_timeout=10.0): + """Run py.test using pexpect. + + This makes sure to use the right py.test and sets up the + temporary directory locations. + + The pexpect child is returned. + + """ if self.request.config.getvalue("notoolsonpath"): pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") @@ -523,6 +811,10 @@ return self.spawn(cmd, expect_timeout=expect_timeout) def spawn(self, cmd, expect_timeout=10.0): + """Run a command using pexpect. + + The pexpect child is returned. + """ pexpect = pytest.importorskip("pexpect", "3.0") if hasattr(sys, 'pypy_version_info') and '64' in platform.machine(): pytest.skip("pypy-64 bit not supported") @@ -560,10 +852,21 @@ return LineMatcher(lines1).fnmatch_lines(lines2) class LineMatcher: + """Flexible matching of text. + + This is a convenience class to test large texts like the output of + commands. + + The constructor takes a list of lines without their trailing + newlines, i.e. ``text.splitlines()``. + + """ + def __init__(self, lines): self.lines = lines def str(self): + """Return the entire original text.""" return "\n".join(self.lines) def _getlines(self, lines2): @@ -574,6 +877,12 @@ return lines2 def fnmatch_lines_random(self, lines2): + """Check lines exist in the output. + + The argument is a list of lines which have to occur in the + output, in any order. Each line can contain glob whildcards. + + """ lines2 = self._getlines(lines2) for line in lines2: for x in self.lines: @@ -584,12 +893,24 @@ raise ValueError("line %r not found in output" % line) def get_lines_after(self, fnline): + """Return all lines following the given line in the text. + + The given line can contain glob wildcards. + """ for i, line in enumerate(self.lines): if fnline == line or fnmatch(line, fnline): return self.lines[i+1:] raise ValueError("line %r not found in output" % fnline) def fnmatch_lines(self, lines2): + """Search the text for matching lines. + + The argument is a list of lines which have to match and can + use glob wildcards. If they do not match an pytest.fail() is + called. The matches and non-matches are also printed on + stdout. + + """ def show(arg1, arg2): py.builtin.print_(arg1, arg2, file=sys.stderr) lines2 = self._getlines(lines2) Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From issues-reply at bitbucket.org Sun Apr 12 01:33:32 2015 From: issues-reply at bitbucket.org (Daniel Hahler) Date: Sat, 11 Apr 2015 23:33:32 -0000 Subject: [Pytest-commit] Issue #717: Improve error format with errors during setup (pytest-dev/pytest) Message-ID: <20150411233332.30109.12935@app12.ash-private.bitbucket.org> New issue 717: Improve error format with errors during setup https://bitbucket.org/pytest-dev/pytest/issue/717/improve-error-format-with-errors-during Daniel Hahler: When there is an error during setup (in case of a missing fixture), the output is not easily parsable: _____________ ERROR at setup of test_foo ______________ file ?/project/app/test_models.py, line 334 def test_foo(db2): fixture 'db2' not found available fixtures: _django_clear_outbox, ... use 'py.test --fixtures [testpath]' for help on them. ?/project/app/test_models.py:334 It would be nice if the line containing the error ("fixture 'db2' not found") was prefixed with "E", and maybe the additional information with ">". I am in the process of improving the errorformat string for Vim, defined/based on the [pytest-vim-compiler](https://github.com/5long/pytest-vim-compiler) plugin. From commits-noreply at bitbucket.org Mon Apr 13 00:49:48 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 12 Apr 2015 22:49:48 -0000 Subject: [Pytest-commit] commit/pytest: 8 new changesets Message-ID: <20150412224948.17078.91091@app11.ash-private.bitbucket.org> 8 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/8e1d89a270a7/ Changeset: 8e1d89a270a7 Branch: pytest-2.7 User: ionelmc Date: 2015-04-10 18:08:50+00:00 Summary: Add support for building proper wheels (universal and proper dependency evnironment markers for argparse/colorama if setuptools is new-ish). Affected #: 2 files diff -r 0b48b3b5156a55a09f3c27ce25beaeb156b56173 -r 8e1d89a270a72fae6b7915afdd6f0cc5c9759fd5 setup.cfg --- a/setup.cfg +++ b/setup.cfg @@ -6,3 +6,5 @@ [upload_sphinx] upload-dir = doc/en/build/html +[bdist_wheel] +universal = 1 diff -r 0b48b3b5156a55a09f3c27ce25beaeb156b56173 -r 8e1d89a270a72fae6b7915afdd6f0cc5c9759fd5 setup.py --- a/setup.py +++ b/setup.py @@ -26,12 +26,25 @@ raise ValueError("could not read version") +def has_newish_setuptools(): + try: + import setuptools + return tuple(int(i) for i in str(setuptools.__version__).split('.')) > (0, 7) + except Exception: + return False + + def main(): install_requires = ['py>=1.4.25'] - if sys.version_info < (2, 7) or (3,) <= sys.version_info < (3, 2): - install_requires.append('argparse') - if sys.platform == 'win32': - install_requires.append('colorama') + extras_require = {} + if has_newish_setuptools(): + extras_require[':python_version=="2.6"'] = ['argparse'] + extras_require[':sys_platform=="win32"'] = ['colorama'] + else: + if sys.version_info < (2, 7) or (3,) <= sys.version_info < (3, 2): + install_requires.append('argparse') + if sys.platform == 'win32': + install_requires.append('colorama') setup( name='pytest', @@ -48,6 +61,7 @@ cmdclass={'test': PyTest}, # the following should be enabled for release install_requires=install_requires, + extras_require=extras_require, packages=['_pytest', '_pytest.assertion'], py_modules=['pytest'], zip_safe=False, https://bitbucket.org/pytest-dev/pytest/commits/2880968b7204/ Changeset: 2880968b7204 Branch: pytest-2.7 User: ionelmc Date: 2015-04-10 18:44:27+00:00 Summary: Improve version test (use pkg_resources to compare versions). Also log failures to stderr. Affected #: 1 file diff -r 8e1d89a270a72fae6b7915afdd6f0cc5c9759fd5 -r 2880968b7204b390f858f9a7129d34614bacd8d3 setup.py --- a/setup.py +++ b/setup.py @@ -29,8 +29,10 @@ def has_newish_setuptools(): try: import setuptools - return tuple(int(i) for i in str(setuptools.__version__).split('.')) > (0, 7) - except Exception: + import pkg_resources + return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7') + except Exception as exc: + sys.stderr.write("Could not test setuptool's version: %s\n" % exc) return False https://bitbucket.org/pytest-dev/pytest/commits/20cf57bd6df5/ Changeset: 20cf57bd6df5 Branch: pytest-2.7 User: ionelmc Date: 2015-04-10 18:58:59+00:00 Summary: Test for setuptools 0.7.2, turns out there's no 0.7 release on pypi. Affected #: 1 file diff -r 2880968b7204b390f858f9a7129d34614bacd8d3 -r 20cf57bd6df5dd62f156f60c43eb2478ea38b316 setup.py --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ try: import setuptools import pkg_resources - return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7') + return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7.2') except Exception as exc: sys.stderr.write("Could not test setuptool's version: %s\n" % exc) return False https://bitbucket.org/pytest-dev/pytest/commits/119223095bfa/ Changeset: 119223095bfa Branch: pytest-2.7 User: ionelmc Date: 2015-04-10 18:59:47+00:00 Summary: Make argpase a dependency for python 3.0 and 3.1 too. Affected #: 1 file diff -r 20cf57bd6df5dd62f156f60c43eb2478ea38b316 -r 119223095bfaba7d5b7be672b4797fcc6f113536 setup.py --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ install_requires = ['py>=1.4.25'] extras_require = {} if has_newish_setuptools(): - extras_require[':python_version=="2.6"'] = ['argparse'] + extras_require[':python_version=="2.6" or python_version=="3.0" or python_version=="3.1"'] = ['argparse'] extras_require[':sys_platform=="win32"'] = ['colorama'] else: if sys.version_info < (2, 7) or (3,) <= sys.version_info < (3, 2): https://bitbucket.org/pytest-dev/pytest/commits/4851f41d56ee/ Changeset: 4851f41d56ee Branch: pytest-2.7 User: ionelmc Date: 2015-04-10 19:11:17+00:00 Summary: Rename function and add nice docstring. Affected #: 1 file diff -r 119223095bfaba7d5b7be672b4797fcc6f113536 -r 4851f41d56ee2e29da807c22154cf7f9342fa52c setup.py --- a/setup.py +++ b/setup.py @@ -26,7 +26,18 @@ raise ValueError("could not read version") -def has_newish_setuptools(): +def has_environment_marker_support(): + """ + Tests that setuptools has support for PEP-426 environment marker support. + + The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 + so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142 + + References: + + * https://wheel.readthedocs.org/en/latest/index.html#defining-conditional-dependencies + * https://www.python.org/dev/peps/pep-0426/#environment-markers + """ try: import setuptools import pkg_resources @@ -39,7 +50,7 @@ def main(): install_requires = ['py>=1.4.25'] extras_require = {} - if has_newish_setuptools(): + if has_environment_marker_support(): extras_require[':python_version=="2.6" or python_version=="3.0" or python_version=="3.1"'] = ['argparse'] extras_require[':sys_platform=="win32"'] = ['colorama'] else: https://bitbucket.org/pytest-dev/pytest/commits/c8996a674862/ Changeset: c8996a674862 Branch: pytest-2.7 User: flub Date: 2015-04-12 22:36:28+00:00 Summary: Do imports at the head of the file Affected #: 1 file diff -r 4851f41d56ee2e29da807c22154cf7f9342fa52c -r c8996a674862ac6ebfe04e89949ee2131454cacc setup.py --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ import os, sys +import setuptools +import pkg_resources from setuptools import setup, Command classifiers = ['Development Status :: 6 - Mature', @@ -39,8 +41,6 @@ * https://www.python.org/dev/peps/pep-0426/#environment-markers """ try: - import setuptools - import pkg_resources return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7.2') except Exception as exc: sys.stderr.write("Could not test setuptool's version: %s\n" % exc) https://bitbucket.org/pytest-dev/pytest/commits/88a88d3d42b0/ Changeset: 88a88d3d42b0 Branch: pytest-2.7 User: flub Date: 2015-04-12 22:36:43+00:00 Summary: Add wheel support in the changelog Affected #: 1 file diff -r c8996a674862ac6ebfe04e89949ee2131454cacc -r 88a88d3d42b01da075dc8e4fcf140e15d723d880 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,9 @@ - fixed docs to remove the notion that yield-fixtures are experimental. They are here to stay :) Thanks Bruno Oliveira. +- Support building wheels by using environment markers for the + requirements. Thanks Ionel Maries Cristian. + 2.7.0 (compared to 2.6.4) ----------------------------- https://bitbucket.org/pytest-dev/pytest/commits/988ffd1be9ea/ Changeset: 988ffd1be9ea User: flub Date: 2015-04-12 22:47:10+00:00 Summary: Merge wheel support from pytest-2.7 branch Merged in pytest-2.7 (pull request #269). Affected #: 3 files diff -r 65a7f821cb6e582ef5b295e458dae90508e7a4c8 -r 988ffd1be9ea48576af7ae42048ab372b7af26db CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,9 @@ +- Support building wheels by using environment markers for the + requirements. Thanks Ionel Maries Cristian. + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 65a7f821cb6e582ef5b295e458dae90508e7a4c8 -r 988ffd1be9ea48576af7ae42048ab372b7af26db setup.cfg --- a/setup.cfg +++ b/setup.cfg @@ -6,3 +6,5 @@ [upload_sphinx] upload-dir = doc/en/build/html +[bdist_wheel] +universal = 1 diff -r 65a7f821cb6e582ef5b295e458dae90508e7a4c8 -r 988ffd1be9ea48576af7ae42048ab372b7af26db setup.py --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ import os, sys +import setuptools +import pkg_resources from setuptools import setup, Command classifiers = ['Development Status :: 6 - Mature', @@ -26,12 +28,36 @@ raise ValueError("could not read version") +def has_environment_marker_support(): + """ + Tests that setuptools has support for PEP-426 environment marker support. + + The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 + so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142 + + References: + + * https://wheel.readthedocs.org/en/latest/index.html#defining-conditional-dependencies + * https://www.python.org/dev/peps/pep-0426/#environment-markers + """ + try: + return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7.2') + except Exception as exc: + sys.stderr.write("Could not test setuptool's version: %s\n" % exc) + return False + + def main(): install_requires = ['py>=1.4.25'] - if sys.version_info < (2, 7) or (3,) <= sys.version_info < (3, 2): - install_requires.append('argparse') - if sys.platform == 'win32': - install_requires.append('colorama') + extras_require = {} + if has_environment_marker_support(): + extras_require[':python_version=="2.6" or python_version=="3.0" or python_version=="3.1"'] = ['argparse'] + extras_require[':sys_platform=="win32"'] = ['colorama'] + else: + if sys.version_info < (2, 7) or (3,) <= sys.version_info < (3, 2): + install_requires.append('argparse') + if sys.platform == 'win32': + install_requires.append('colorama') setup( name='pytest', @@ -48,6 +74,7 @@ cmdclass={'test': PyTest}, # the following should be enabled for release install_requires=install_requires, + extras_require=extras_require, packages=['_pytest', '_pytest.assertion'], py_modules=['pytest'], zip_safe=False, Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Mon Apr 13 01:00:43 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 12 Apr 2015 23:00:43 -0000 Subject: [Pytest-commit] commit/pytest: flub: Close branch issue660 Message-ID: <20150412230043.20203.44800@app14.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/a67a37982453/ Changeset: a67a37982453 Branch: issue660 User: flub Date: 2015-04-12 23:00:40+00:00 Summary: Close branch issue660 Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Mon Apr 13 01:00:44 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 12 Apr 2015 23:00:44 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merged in issue660 (pull request #268) Message-ID: <20150412230044.19854.89560@app12.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/e42f106823e9/ Changeset: e42f106823e9 Branch: pytest-2.7 User: flub Date: 2015-04-12 23:00:40+00:00 Summary: Merged in issue660 (pull request #268) fix issue660 Affected #: 3 files diff -r 88a88d3d42b01da075dc8e4fcf140e15d723d880 -r e42f106823e9f453005a626744d7ac234a05938a CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- fix issue660: properly report scope-mismatch-access errors + independently from ordering of fixture arguments. Also + avoid the pytest internal traceback which does not provide + information to the user. Thanks Holger Krekel. + - streamlined and documented release process. Also all versions (in setup.py and documentation generation) are now read from _pytest/__init__.py. Thanks Holger Krekel. diff -r 88a88d3d42b01da075dc8e4fcf140e15d723d880 -r e42f106823e9f453005a626744d7ac234a05938a _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1356,12 +1356,7 @@ try: val = cache[cachekey] except KeyError: - __tracebackhide__ = True - if scopemismatch(self.scope, scope): - raise ScopeMismatchError("You tried to access a %r scoped " - "resource with a %r scoped request object" %( - (scope, self.scope))) - __tracebackhide__ = False + self._check_scope(self.fixturename, self.scope, scope) val = setup() cache[cachekey] = val if teardown is not None: @@ -1392,6 +1387,7 @@ if argname == "request": class PseudoFixtureDef: cached_result = (self, [0], None) + scope = "function" return PseudoFixtureDef raise # remove indent to prevent the python3 exception @@ -1435,16 +1431,7 @@ subrequest = SubRequest(self, scope, param, param_index, fixturedef) # check if a higher-level scoped fixture accesses a lower level one - if scope is not None: - __tracebackhide__ = True - if scopemismatch(self.scope, scope): - # try to report something helpful - lines = subrequest._factorytraceback() - raise ScopeMismatchError("You tried to access the %r scoped " - "fixture %r with a %r scoped request object, " - "involved factories\n%s" %( - (scope, argname, self.scope, "\n".join(lines)))) - __tracebackhide__ = False + subrequest._check_scope(argname, self.scope, scope) # clear sys.exc_info before invoking the fixture (python bug?) # if its not explicitly cleared it will leak into the call @@ -1458,6 +1445,18 @@ subrequest.node) return val + def _check_scope(self, argname, invoking_scope, requested_scope): + if argname == "request": + return + if scopemismatch(invoking_scope, requested_scope): + # try to report something helpful + lines = self._factorytraceback() + pytest.fail("ScopeMismatch: you tried to access the %r scoped " + "fixture %r with a %r scoped request object, " + "involved factories\n%s" %( + (requested_scope, argname, invoking_scope, "\n".join(lines))), + pytrace=False) + def _factorytraceback(self): lines = [] for fixturedef in self._get_fixturestack(): @@ -1518,6 +1517,7 @@ def scopemismatch(currentscope, newscope): return scopes.index(newscope) > scopes.index(currentscope) + class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ def __init__(self, argname, request, msg=None): @@ -1867,6 +1867,7 @@ for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) result, arg_cache_key, exc = fixturedef.cached_result + request._check_scope(argname, request.scope, fixturedef.scope) kwargs[argname] = result if argname != "request": fixturedef.addfinalizer(self.finish) diff -r 88a88d3d42b01da075dc8e4fcf140e15d723d880 -r e42f106823e9f453005a626744d7ac234a05938a testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -906,6 +906,27 @@ "*1 error*" ]) + def test_receives_funcargs_scope_mismatch_issue660(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="function") + def arg1(): + return 1 + + @pytest.fixture(scope="module") + def arg2(arg1): + return arg1 + 1 + + def test_add(arg1, arg2): + assert arg2 == 2 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ScopeMismatch*involved factories*", + "* def arg2*", + "*1 error*" + ]) + def test_funcarg_parametrized_and_used_twice(self, testdir): testdir.makepyfile(""" import pytest Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Mon Apr 13 01:05:10 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 12 Apr 2015 23:05:10 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merge pytest-2.7 for issue660 fix Message-ID: <20150412230510.2701.78448@app13.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/508d2b3cb60e/ Changeset: 508d2b3cb60e User: flub Date: 2015-04-12 23:04:53+00:00 Summary: Merge pytest-2.7 for issue660 fix Affected #: 3 files diff -r 988ffd1be9ea48576af7ae42048ab372b7af26db -r 508d2b3cb60efa1d38d86f83021cedfcd582742d CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,20 @@ ----------------------------- +2.7.1.dev (compared to 2.7.0) +----------------------------- + +- fix issue660: properly report scope-mismatch-access errors + independently from ordering of fixture arguments. Also + avoid the pytest internal traceback which does not provide + information to the user. Thanks Holger Krekel. + +- streamlined and documented release process. Also all versions + (in setup.py and documentation generation) are now read + from _pytest/__init__.py. Thanks Holger Krekel. + +- fixed docs to remove the notion that yield-fixtures are experimental. + They are here to stay :) Thanks Bruno Oliveira. - Support building wheels by using environment markers for the requirements. Thanks Ionel Maries Cristian. diff -r 988ffd1be9ea48576af7ae42048ab372b7af26db -r 508d2b3cb60efa1d38d86f83021cedfcd582742d _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1356,12 +1356,7 @@ try: val = cache[cachekey] except KeyError: - __tracebackhide__ = True - if scopemismatch(self.scope, scope): - raise ScopeMismatchError("You tried to access a %r scoped " - "resource with a %r scoped request object" %( - (scope, self.scope))) - __tracebackhide__ = False + self._check_scope(self.fixturename, self.scope, scope) val = setup() cache[cachekey] = val if teardown is not None: @@ -1392,6 +1387,7 @@ if argname == "request": class PseudoFixtureDef: cached_result = (self, [0], None) + scope = "function" return PseudoFixtureDef raise # remove indent to prevent the python3 exception @@ -1435,16 +1431,7 @@ subrequest = SubRequest(self, scope, param, param_index, fixturedef) # check if a higher-level scoped fixture accesses a lower level one - if scope is not None: - __tracebackhide__ = True - if scopemismatch(self.scope, scope): - # try to report something helpful - lines = subrequest._factorytraceback() - raise ScopeMismatchError("You tried to access the %r scoped " - "fixture %r with a %r scoped request object, " - "involved factories\n%s" %( - (scope, argname, self.scope, "\n".join(lines)))) - __tracebackhide__ = False + subrequest._check_scope(argname, self.scope, scope) # clear sys.exc_info before invoking the fixture (python bug?) # if its not explicitly cleared it will leak into the call @@ -1458,6 +1445,18 @@ subrequest.node) return val + def _check_scope(self, argname, invoking_scope, requested_scope): + if argname == "request": + return + if scopemismatch(invoking_scope, requested_scope): + # try to report something helpful + lines = self._factorytraceback() + pytest.fail("ScopeMismatch: you tried to access the %r scoped " + "fixture %r with a %r scoped request object, " + "involved factories\n%s" %( + (requested_scope, argname, invoking_scope, "\n".join(lines))), + pytrace=False) + def _factorytraceback(self): lines = [] for fixturedef in self._get_fixturestack(): @@ -1518,6 +1517,7 @@ def scopemismatch(currentscope, newscope): return scopes.index(newscope) > scopes.index(currentscope) + class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ def __init__(self, argname, request, msg=None): @@ -1867,6 +1867,7 @@ for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) result, arg_cache_key, exc = fixturedef.cached_result + request._check_scope(argname, request.scope, fixturedef.scope) kwargs[argname] = result if argname != "request": fixturedef.addfinalizer(self.finish) diff -r 988ffd1be9ea48576af7ae42048ab372b7af26db -r 508d2b3cb60efa1d38d86f83021cedfcd582742d testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -906,6 +906,27 @@ "*1 error*" ]) + def test_receives_funcargs_scope_mismatch_issue660(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="function") + def arg1(): + return 1 + + @pytest.fixture(scope="module") + def arg2(arg1): + return arg1 + 1 + + def test_add(arg1, arg2): + assert arg2 == 2 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ScopeMismatch*involved factories*", + "* def arg2*", + "*1 error*" + ]) + def test_funcarg_parametrized_and_used_twice(self, testdir): testdir.makepyfile(""" import pytest Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Mon Apr 13 01:11:34 2015 From: builds at drone.io (Drone.io Build) Date: Sun, 12 Apr 2015 23:11:34 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 62 Message-ID: <20150412230102.113187.13399@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/62 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4111:988ffd1be9ea Author : Floris Bruynooghe Branch : default Message: Merge wheel support from pytest-2.7 branch -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Mon Apr 13 01:11:45 2015 From: builds at drone.io (Drone.io Build) Date: Sun, 12 Apr 2015 23:11:45 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 63 Message-ID: <20150412231145.26884.63433@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/63 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3955:e42f106823e9 Author : Floris Bruynooghe Branch : pytest-2.7 Message: Merged in issue660 (pull request #268) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Mon Apr 13 01:23:25 2015 From: builds at drone.io (Drone.io Build) Date: Sun, 12 Apr 2015 23:23:25 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 64 Message-ID: <20150412232325.41548.39493@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/64 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3947:a67a37982453 Author : Floris Bruynooghe Branch : issue660 Message: Close branch issue660 -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Mon Apr 13 01:34:14 2015 From: builds at drone.io (Drone.io Build) Date: Sun, 12 Apr 2015 23:34:14 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 65 Message-ID: <20150412233414.37466.4636@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/65 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4114:508d2b3cb60e Author : Floris Bruynooghe Branch : default Message: Merge pytest-2.7 for issue660 fix -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Mon Apr 13 10:08:25 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 13 Apr 2015 08:08:25 -0000 Subject: [Pytest-commit] commit/pytest: flub: Use capital Y as the tests look for that Message-ID: <20150413080825.23214.22570@app14.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/7d32b89da36e/ Changeset: 7d32b89da36e User: flub Date: 2015-04-13 08:08:10+00:00 Summary: Use capital Y as the tests look for that Affected #: 1 file diff -r 508d2b3cb60efa1d38d86f83021cedfcd582742d -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1451,7 +1451,7 @@ if scopemismatch(invoking_scope, requested_scope): # try to report something helpful lines = self._factorytraceback() - pytest.fail("ScopeMismatch: you tried to access the %r scoped " + pytest.fail("ScopeMismatch: You tried to access the %r scoped " "fixture %r with a %r scoped request object, " "involved factories\n%s" %( (requested_scope, argname, invoking_scope, "\n".join(lines))), Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From issues-reply at bitbucket.org Mon Apr 13 13:56:07 2015 From: issues-reply at bitbucket.org (Michael van Tellingen) Date: Mon, 13 Apr 2015 11:56:07 -0000 Subject: [Pytest-commit] Issue #235: tox --installpkg raises an AttributeError (hpk42/tox) Message-ID: <20150413115607.3286.71688@app09.ash-private.bitbucket.org> New issue 235: tox --installpkg raises an AttributeError https://bitbucket.org/hpk42/tox/issue/235/tox-installpkg-raises-an-attributeerror Michael van Tellingen: tox --installpkg dist/something.tgz raises an attributeerror: ``` #!python Traceback (most recent call last): File "/Users/mvantellingen/virtualenvs/something/bin/tox", line 9, in load_entry_point('tox==1.8.0', 'console_scripts', 'tox')() File "/Users/mvantellingen/virtualenvs/something/lib/python2.7/site-packages/tox/_cmdline.py", line 26, in main retcode = Session(config).runcommand() File "/Users/mvantellingen/virtualenvs/something/lib/python2.7/site-packages/tox/_cmdline.py", line 310, in runcommand return self.subcommand_test() File "/Users/mvantellingen/virtualenvs/something/lib/python2.7/site-packages/tox/_cmdline.py", line 454, in subcommand_test self.installpkg(venv, sdist_path) File "/Users/mvantellingen/virtualenvs/something/lib/python2.7/site-packages/tox/_cmdline.py", line 397, in installpkg self.resultlog.set_header(installpkg=sdist_path) File "/Users/mvantellingen/virtualenvs/something/lib/python2.7/site-packages/tox/result.py", line 21, in set_header md5=installpkg.computehash("md5"), AttributeError: 'str' object has no attribute 'computehash' ``` From issues-reply at bitbucket.org Mon Apr 13 14:22:32 2015 From: issues-reply at bitbucket.org (Edison Gustavo Muenz) Date: Mon, 13 Apr 2015 12:22:32 -0000 Subject: [Pytest-commit] Issue #718: Failure to create representation with sets having "unsortable" elements (pytest-dev/pytest) Message-ID: <20150413122232.17752.4909@app05.ash-private.bitbucket.org> New issue 718: Failure to create representation with sets having "unsortable" elements https://bitbucket.org/pytest-dev/pytest/issue/718/failure-to-create-representation-with-sets Edison Gustavo Muenz: I?m using Python 2.7.7 (The error also happens on Python 2.7.9) The following code fails: ```python def test_pretty_printer_screws_up_representation(): class UnsortableKey(object): def __init__(self, name): self.name = name def __lt__(self, other): raise RuntimeError() def __repr__(self): return self.name def __hash__(self): return hash(self.name) def __eq__(self, other): return self.name == other.name assert {UnsortableKey('1'), UnsortableKey('2')} == {UnsortableKey('2')} > assert {UnsortableKey('1'), UnsortableKey('2')} == {UnsortableKey('2')} E assert set([1, 2]) == set([2]) E (pytest_assertion plugin: representation of details failed. Probably an object has a faulty __repr__.) E X:\etk\coilib50\source\python\coilib50\_pytest\_tests\pytest_almost_equal.py:766: RuntimeError ``` While debugging, the problem happens in this [line](https://github.com/python/cpython/blob/2.7/Lib/pprint.py#L199) of pprint.py (The [PrettyPrinter module](https://docs.python.org/2/library/pprint.html)). It assumes that the ?object? can have the method sorted() called on it, which is not always true. The `repr.py` module handles this correctly, but `pprint.py` doesn?t. This is the full [traceback](http://pastebin.com/t9a9amcR) (I printed it from the the `__lt__()` method). I think that this should be handled by `pprint.py`, however, it is tied to the python version being used. Maybe pytest could handle this as a workaround this limitation of `pprint.py`? Fixing it in `pprint.py` looks straightforward: just handle it like `repr.py` [does](https://hg.python.org/cpython/file/2.7/Lib/repr.py#l122), by using the `_possiblysorted()` method. From commits-noreply at bitbucket.org Mon Apr 13 21:00:00 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 13 Apr 2015 19:00:00 -0000 Subject: [Pytest-commit] commit/pytest: RonnyPfannschmidt: move evaluation and caching of mark expressions to own function Message-ID: <20150413190000.32745.51470@app08.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/551a54d002fd/ Changeset: 551a54d002fd Branch: markexpr-parser User: RonnyPfannschmidt Date: 2015-04-13 17:37:34+00:00 Summary: move evaluation and caching of mark expressions to own function Affected #: 1 file diff -r bf6f8a626afb870b19a91ff3993203e264fc2e9c -r 551a54d002fda8db9a947063ce37c14ed5edcea0 _pytest/mark.py --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -113,9 +113,15 @@ return False +def _match_expr(expr, globals_, __cache={}): + if expr not in __cache: + __cache[expr] = compile(expr, expr, 'eval') + return eval(__cache[expr], {}, globals_) + + def matchmark(colitem, markexpr): """Tries to match on any marker names, attached to the given colitem.""" - return eval(markexpr, {}, MarkMapping(colitem.keywords)) + return _match_expr(markexpr, MarkMapping(colitem.keywords)) def matchkeyword(colitem, keywordexpr): @@ -150,7 +156,7 @@ return mapping[keywordexpr] elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: return not mapping[keywordexpr[4:]] - return eval(keywordexpr, {}, mapping) + return _match_expr(keywordexpr, mapping) def pytest_configure(config): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Mon Apr 13 21:12:34 2015 From: builds at drone.io (Drone.io Build) Date: Mon, 13 Apr 2015 19:12:34 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 67 Message-ID: <20150413191233.56383.28812@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/67 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3928:551a54d002fd Author : Ronny Pfannschmidt Branch : markexpr-parser Message: move evaluation and caching of mark expressions to own function -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Tue Apr 14 01:16:15 2015 From: issues-reply at bitbucket.org (David Haney) Date: Mon, 13 Apr 2015 23:16:15 -0000 Subject: [Pytest-commit] Issue #719: pytest.mark.parametrize string-based parameter list doesn't handle single element tuples (pytest-dev/pytest) Message-ID: <20150413231615.14793.97155@app03.ash-private.bitbucket.org> New issue 719: pytest.mark.parametrize string-based parameter list doesn't handle single element tuples https://bitbucket.org/pytest-dev/pytest/issue/719/pytestmarkparametrize-string-based David Haney: When specifying a `@pytest.mark.parametrize` element composed of a one-element tuple, I'm unable to use the string-based argument list, for example: @pytest.mark.parametrize("arg", scenarios) def test(arg): print(arg) assert(arg == "a") # this assert always fails arg ends up being the tuple instead of the first item in the tuple (as I would expected based on tuples with more than one item. I also tried: @pytest.mark.parametrize("arg,", scenarios) but that also associated the entire tuple with arg instead of the first element. I reverted back to the older model of specifying the arguments as a tuple: @pytest.mark.parametrize("arg,", scenarios) Finally I switched back to the older style of using a tuple to specify the parameter list: @pytest.mark.parametrize(("arg",), scenarios) This version worked. This seems to imply that there is either a bug/limitation in the new string-based parameter specification, or that there is still a use-case for the tuple-based parameter specification. It would be helpful if either the string-based implementation could be updated to handle this situation, or if the documentation could be updated to note when the tuple-based parameter specification is still needed. From issues-reply at bitbucket.org Tue Apr 14 21:37:34 2015 From: issues-reply at bitbucket.org (=?utf-8?q?Mike_M=C3=BCller?=) Date: Tue, 14 Apr 2015 19:37:34 -0000 Subject: [Pytest-commit] Issue #720: Allow testing of bytecode-only modules in Python 3 (pytest-dev/pytest) Message-ID: <20150414193734.2083.81446@app07.ash-private.bitbucket.org> New issue 720: Allow testing of bytecode-only modules in Python 3 https://bitbucket.org/pytest-dev/pytest/issue/720/allow-testing-of-bytecode-only-modules-in Mike M?ller: I am testing another programming language that eventually emits Python 3 (or PyPyp 3) bytecode. I would like to use `pytest` for testing. When I import `*.pyc` file somewhere in my test I get this error message: ``` #!shell from factorial import factorial :2237: in _find_and_load ??? :2222: in _find_and_load_unlocked ??? :2160: in _find_spec ??? :2141: in _find_spec_legacy ??? .../site-packages/_pytest/assertion/rewrite.py:75: in find_module fn = imp.source_from_cache(fn) ..../lib/python3.4/imp.py:100: in source_from_cache return util.source_from_cache(path) :479: in source_from_cache ??? E ValueError: __pycache__ not bottom-level directory in '.../tests/factorial.pyc' ``` I pinned down the problem in the file `assertion/rewrite.py`. This is the code that causes the exception (starting form line 73): ``` #!python if tp == imp.PY_COMPILED: if hasattr(imp, "source_from_cache"): fn = imp.source_from_cache(fn) else: fn = fn[:-1] elif tp != imp.PY_SOURCE: # Don't know what this is. return None ``` My very programmatic, and probably wrong, solution ist to return `None` if this exception occurs: ``` #!python if tp == imp.PY_COMPILED: if hasattr(imp, "source_from_cache"): try: fn = imp.source_from_cache(fn) except ValueError: return None else: fn = fn[:-1] ``` Is there another way to make bytecode-only files work? Could my solution above serve as a basis for a real solution? Maybe a command line option that switches on and off the behavior I want can be useful here. These are my versions: ``` platform darwin -- Python 3.4.3 -- py-1.4.26 -- pytest-2.7.0 plugins: xdist ``` From issues-reply at bitbucket.org Wed Apr 15 11:34:22 2015 From: issues-reply at bitbucket.org (JocelynDelalande) Date: Wed, 15 Apr 2015 09:34:22 -0000 Subject: [Pytest-commit] Issue #721: Add release date to changelog (pytest-dev/pytest) Message-ID: <20150415093422.536.46805@app11.ash-private.bitbucket.org> New issue 721: Add release date to changelog https://bitbucket.org/pytest-dev/pytest/issue/721/add-release-date-to-changelog JocelynDelalande: Hello, Would be usefull in project evaluation and following to add release date to the project documentation, changelog section. From issues-reply at bitbucket.org Wed Apr 15 15:36:39 2015 From: issues-reply at bitbucket.org (Victor Stinner) Date: Wed, 15 Apr 2015 13:36:39 -0000 Subject: [Pytest-commit] Issue #236: tox must create the source distribution with the Python of the virtual environment (hpk42/tox) Message-ID: <20150415133639.454.20508@app09.ash-private.bitbucket.org> New issue 236: tox must create the source distribution with the Python of the virtual environment https://bitbucket.org/hpk42/tox/issue/236/tox-must-create-the-source-distribution Victor Stinner: Hi, I just got a bug in the Oslo Messaging project on requirements: a unit test ensures that requirements are installed, and this test fails. tox currently starts by building a source distribution using "python setup.py sdist". The problem is that in Oslo Messaging, requirements are different on Python 2 and Python 3: oslo.messaging.egg-info/requires.txt is different if you run "python2 setup.py sdist" or "python3 setup.py sdist". I wrote a patch to always rebuild the source distribution with the Python of the virtual environment in subcommand_test(). I kept the first call to sdist() which may be redundant in some cases, but I don't know tox enough to decide when the first call can be skipped (I don't want to introduce a regression). Victor From issues-reply at bitbucket.org Wed Apr 15 21:05:46 2015 From: issues-reply at bitbucket.org (Bruno Oliveira) Date: Wed, 15 Apr 2015 19:05:46 -0000 Subject: [Pytest-commit] Issue #722: Missing tag for release 1.11 (pytest-dev/pytest) Message-ID: <20150415190546.28086.67571@app04.ash-private.bitbucket.org> New issue 722: Missing tag for release 1.11 https://bitbucket.org/pytest-dev/pytest/issue/722/missing-tag-for-release-111 Bruno Oliveira: Hi Holger, The tag to version `1.11` is missing from the repository. Looking at the date of the release, the tag should be at either commit [220f6e4](https://bitbucket.org/pytest-dev/pytest-xdist/commits/220f6e46eb71a6212ccbe6b67b9e6edcf8ee4fa5?at=default) or [a796c2c](https://bitbucket.org/pytest-dev/pytest-xdist/commits/a796c2c3042f4fd8e09c026fe80cdda08e574df2?at=default), because they were made on the same date as the PyPI release (18-09-2014). Responsible: hpk42 From commits-noreply at bitbucket.org Thu Apr 16 00:20:16 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 15 Apr 2015 22:20:16 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Handle inspect.getsourcelines failures in FixtureLookupError Message-ID: <20150415222016.5268.29186@app01.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/e0c4d91ebba2/ Changeset: e0c4d91ebba2 Branch: getsourcelines-error-issue-553 User: nicoddemus Date: 2015-04-15 22:19:49+00:00 Summary: Handle inspect.getsourcelines failures in FixtureLookupError Fixes #553 Affected #: 3 files diff -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 -r e0c4d91ebba24d224a747a749dfa600a3bc5b485 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,11 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- fix issue553: properly handling inspect.getsourcelines failures in + FixtureLookupError which would lead to to an internal error, + obfuscating the original problem. Thanks talljosh for initial + diagnose/patch and Bruno Oliveira for final patch. + - fix issue660: properly report scope-mismatch-access errors independently from ordering of fixture arguments. Also avoid the pytest internal traceback which does not provide diff -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 -r e0c4d91ebba24d224a747a749dfa600a3bc5b485 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1537,13 +1537,18 @@ # it at the requesting side for function in stack: fspath, lineno = getfslineno(function) - lines, _ = inspect.getsourcelines(function) - addline("file %s, line %s" % (fspath, lineno+1)) - for i, line in enumerate(lines): - line = line.rstrip() - addline(" " + line) - if line.lstrip().startswith('def'): - break + try: + lines, _ = inspect.getsourcelines(function) + except IOError: + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno+1)) + else: + addline("file %s, line %s" % (fspath, lineno+1)) + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith('def'): + break if msg is None: fm = self.request._fixturemanager diff -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 -r e0c4d91ebba24d224a747a749dfa600a3bc5b485 testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -353,6 +353,23 @@ *unrecognized* """) + def test_getsourcelines_error_issue553(self, testdir): + p = testdir.makepyfile(""" + def raise_error(obj): + raise IOError('source code not available') + + import inspect + inspect.getsourcelines = raise_error + + def test_foo(invalid_fixture): + pass + """) + res = testdir.runpytest(p) + res.stdout.fnmatch_lines([ + "*source code not available*", + "*fixture 'invalid_fixture' not found", + ]) + class TestInvocationVariants: def test_earlyinit(self, testdir): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 16 00:32:16 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 15 Apr 2015 22:32:16 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Handle inspect.getsourcelines failures in FixtureLookupError Message-ID: <20150415223216.5429.8404@app08.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/b820fe775929/ Changeset: b820fe775929 Branch: getsourcelines-error-issue-553-pytest2.7 User: nicoddemus Date: 2015-04-15 22:31:22+00:00 Summary: Handle inspect.getsourcelines failures in FixtureLookupError Fixes #553 Affected #: 3 files diff -r e42f106823e9f453005a626744d7ac234a05938a -r b820fe7759295097225ee633c488df4fdbc2e239 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- fix issue553: properly handling inspect.getsourcelines failures in + FixtureLookupError which would lead to to an internal error, + obfuscating the original problem. Thanks talljosh for initial + diagnose/patch and Bruno Oliveira for final patch. + - fix issue660: properly report scope-mismatch-access errors independently from ordering of fixture arguments. Also avoid the pytest internal traceback which does not provide diff -r e42f106823e9f453005a626744d7ac234a05938a -r b820fe7759295097225ee633c488df4fdbc2e239 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1537,13 +1537,18 @@ # it at the requesting side for function in stack: fspath, lineno = getfslineno(function) - lines, _ = inspect.getsourcelines(function) - addline("file %s, line %s" % (fspath, lineno+1)) - for i, line in enumerate(lines): - line = line.rstrip() - addline(" " + line) - if line.lstrip().startswith('def'): - break + try: + lines, _ = inspect.getsourcelines(function) + except IOError: + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno+1)) + else: + addline("file %s, line %s" % (fspath, lineno+1)) + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith('def'): + break if msg is None: fm = self.request._fixturemanager diff -r e42f106823e9f453005a626744d7ac234a05938a -r b820fe7759295097225ee633c488df4fdbc2e239 testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -353,6 +353,23 @@ *unrecognized* """) + def test_getsourcelines_error_issue553(self, testdir): + p = testdir.makepyfile(""" + def raise_error(obj): + raise IOError('source code not available') + + import inspect + inspect.getsourcelines = raise_error + + def test_foo(invalid_fixture): + pass + """) + res = testdir.runpytest(p) + res.stdout.fnmatch_lines([ + "*source code not available*", + "*fixture 'invalid_fixture' not found", + ]) + class TestInvocationVariants: def test_earlyinit(self, testdir): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 16 00:37:14 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 15 Apr 2015 22:37:14 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Branch created from default by accident (meant to use pytest-2.7 as base) Message-ID: <20150415223714.22479.47905@app07.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/1dcd0a914a3b/ Changeset: 1dcd0a914a3b Branch: getsourcelines-error-issue-553 User: nicoddemus Date: 2015-04-15 22:36:13+00:00 Summary: Branch created from default by accident (meant to use pytest-2.7 as base) Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Thu Apr 16 00:42:01 2015 From: builds at drone.io (Drone.io Build) Date: Wed, 15 Apr 2015 22:42:01 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 69 Message-ID: <20150415224200.100979.73620@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/69 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3956:b820fe775929 Author : Bruno Oliveira Branch : getsourcelines-error-issue-553-pytest2.7 Message: Handle inspect.getsourcelines failures in FixtureLookupError -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Thu Apr 16 08:50:27 2015 From: builds at drone.io (Drone.io Build) Date: Thu, 16 Apr 2015 06:50:27 +0000 Subject: [Pytest-commit] [FAIL] pytest-xdist - # 10 Message-ID: <20150416065026.6605.50443@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest-xdist/10 Project : https://drone.io/bitbucket.org/pytest-dev/pytest-xdist Repository : https://bitbucket.org/pytest-dev/pytest-xdist Version : 204:4ffc63d4bef2 Author : holger krekel Branch : default Message: Added tag 1.11 for changeset 220f6e46eb71 -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Thu Apr 16 23:28:16 2015 From: issues-reply at bitbucket.org (Sandeep Kasargod) Date: Thu, 16 Apr 2015 21:28:16 -0000 Subject: [Pytest-commit] Issue #723: module scope fixture invoked multiple times if using test node specifiers for same module (pytest-dev/pytest) Message-ID: <20150416212816.27019.39808@app05.ash-private.bitbucket.org> New issue 723: module scope fixture invoked multiple times if using test node specifiers for same module https://bitbucket.org/pytest-dev/pytest/issue/723/module-scope-fixture-invoked-multiple Sandeep Kasargod: Using Version 2.6.4 The attachment mytest.py defines a module scoped fixture used by two tests. Running the tests with method 1 or 2 results in the fixture setup once only as expected. However if I use method 3, where I specify the two test nodes explicitly, looks like they are always treated as two separate modules. Is this expected behavior or should pytest do module coalescing when test collection takes place ? The reason I want to use method 3 is to be able to use a text file that specifies the tests using the test node syntax as in method 3 1) py.test mytest.py -v -s 2) py.test mytest.py -v -s -k 'test_1 or test_2' ``` #!python mytest.py::test_1 > module_fixture_start TEST1 PASSED mytest.py::test_2 TEST2 PASSED < module_fixture_cleanup ``` 3) py.test mytest.py::test_1 mytest.py::test_2 -v -s ``` #!python platform linux2 -- Python 2.7.5 -- py-1.4.26 -- pytest-2.6.4 -- /home/vxbuild/python_virtualenv/bin/python collected 6 items mytest.py::test_1 > module_fixture_start TEST1 PASSED < module_fixture_cleanup mytest.py::test_2 > module_fixture_start TEST2 PASSED < module_fixture_cleanup ``` From commits-noreply at bitbucket.org Fri Apr 17 11:57:23 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 09:57:23 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: backport Y->y fix from floris Message-ID: <20150417095723.30889.32090@app06.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/5d56a7e92fe5/ Changeset: 5d56a7e92fe5 Branch: pytest-2.7 User: hpk42 Date: 2015-04-17 09:57:09+00:00 Summary: backport Y->y fix from floris Affected #: 1 file diff -r e42f106823e9f453005a626744d7ac234a05938a -r 5d56a7e92fe524d72c8788916bc574ce09f575df _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1451,7 +1451,7 @@ if scopemismatch(invoking_scope, requested_scope): # try to report something helpful lines = self._factorytraceback() - pytest.fail("ScopeMismatch: you tried to access the %r scoped " + pytest.fail("ScopeMismatch: You tried to access the %r scoped " "fixture %r with a %r scoped request object, " "involved factories\n%s" %( (requested_scope, argname, invoking_scope, "\n".join(lines))), Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 12:07:28 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 10:07:28 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150417100728.20278.92349@app12.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/69642e30e3c6/ Changeset: 69642e30e3c6 Branch: systemexit User: hpk42 Date: 2015-04-17 09:47:29+00:00 Summary: fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing when tests raised SystemExit. Affected #: 3 files diff -r e42f106823e9f453005a626744d7ac234a05938a -r 69642e30e3c63f506ec7ca00336417752f282211 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,10 @@ - Support building wheels by using environment markers for the requirements. Thanks Ionel Maries Cristian. +- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing + when tests raised SystemExit. Thanks Holger Krekel. + + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r e42f106823e9f453005a626744d7ac234a05938a -r 69642e30e3c63f506ec7ca00336417752f282211 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -121,7 +121,7 @@ def __init__(self, func): try: self.result = func() - except Exception: + except BaseException: self.excinfo = sys.exc_info() def force_result(self, result): diff -r e42f106823e9f453005a626744d7ac234a05938a -r 69642e30e3c63f506ec7ca00336417752f282211 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -607,6 +607,21 @@ assert "m1" in str(ex.value) assert "test_core.py:" in str(ex.value) + @pytest.mark.parametrize("exc", [ValueError, SystemExit]) + def test_hookwrapper_exception(self, exc): + l = [] + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + m1.hookwrapper = True + + def m2(): + raise exc + with pytest.raises(exc): + MultiCall([m2, m1], {}).execute() + assert l == ["m1 init", "m1 finish"] + class TestHookRelay: def test_happypath(self): https://bitbucket.org/pytest-dev/pytest/commits/f61e0f6a9f49/ Changeset: f61e0f6a9f49 Branch: pytest-2.7 User: flub Date: 2015-04-17 10:07:24+00:00 Summary: Merged in hpk42/pytest-patches/systemexit (pull request #274) fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing Affected #: 3 files diff -r 5d56a7e92fe524d72c8788916bc574ce09f575df -r f61e0f6a9f49f12278020f2007a1931e01821df3 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,10 @@ - Support building wheels by using environment markers for the requirements. Thanks Ionel Maries Cristian. +- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing + when tests raised SystemExit. Thanks Holger Krekel. + + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 5d56a7e92fe524d72c8788916bc574ce09f575df -r f61e0f6a9f49f12278020f2007a1931e01821df3 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -121,7 +121,7 @@ def __init__(self, func): try: self.result = func() - except Exception: + except BaseException: self.excinfo = sys.exc_info() def force_result(self, result): diff -r 5d56a7e92fe524d72c8788916bc574ce09f575df -r f61e0f6a9f49f12278020f2007a1931e01821df3 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -607,6 +607,21 @@ assert "m1" in str(ex.value) assert "test_core.py:" in str(ex.value) + @pytest.mark.parametrize("exc", [ValueError, SystemExit]) + def test_hookwrapper_exception(self, exc): + l = [] + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + m1.hookwrapper = True + + def m2(): + raise exc + with pytest.raises(exc): + MultiCall([m2, m1], {}).execute() + assert l == ["m1 init", "m1 finish"] + class TestHookRelay: def test_happypath(self): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 12:07:28 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 10:07:28 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merged in hpk42/pytest-patches/systemexit (pull request #274) Message-ID: <20150417100728.31875.73452@app13.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/f61e0f6a9f49/ Changeset: f61e0f6a9f49 Branch: pytest-2.7 User: flub Date: 2015-04-17 10:07:24+00:00 Summary: Merged in hpk42/pytest-patches/systemexit (pull request #274) fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing Affected #: 3 files diff -r 5d56a7e92fe524d72c8788916bc574ce09f575df -r f61e0f6a9f49f12278020f2007a1931e01821df3 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,10 @@ - Support building wheels by using environment markers for the requirements. Thanks Ionel Maries Cristian. +- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing + when tests raised SystemExit. Thanks Holger Krekel. + + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 5d56a7e92fe524d72c8788916bc574ce09f575df -r f61e0f6a9f49f12278020f2007a1931e01821df3 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -121,7 +121,7 @@ def __init__(self, func): try: self.result = func() - except Exception: + except BaseException: self.excinfo = sys.exc_info() def force_result(self, result): diff -r 5d56a7e92fe524d72c8788916bc574ce09f575df -r f61e0f6a9f49f12278020f2007a1931e01821df3 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -607,6 +607,21 @@ assert "m1" in str(ex.value) assert "test_core.py:" in str(ex.value) + @pytest.mark.parametrize("exc", [ValueError, SystemExit]) + def test_hookwrapper_exception(self, exc): + l = [] + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + m1.hookwrapper = True + + def m2(): + raise exc + with pytest.raises(exc): + MultiCall([m2, m1], {}).execute() + assert l == ["m1 init", "m1 finish"] + class TestHookRelay: def test_happypath(self): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 12:08:24 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 10:08:24 -0000 Subject: [Pytest-commit] commit/pytest: 7 new changesets Message-ID: <20150417100824.20923.95179@app02.ash-private.bitbucket.org> 7 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/79a3c663605d/ Changeset: 79a3c663605d Branch: systemexit User: hpk42 Date: 2015-04-17 10:08:02+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/00623de64963/ Changeset: 00623de64963 Branch: issue463 User: hpk42 Date: 2015-04-17 10:08:03+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/2850ee1e29da/ Changeset: 2850ee1e29da Branch: fix-reload User: hpk42 Date: 2015-04-17 10:08:03+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/c0388ad997ec/ Changeset: c0388ad997ec Branch: merge-cache User: hpk42 Date: 2015-04-17 10:08:04+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/c35e4fd32e01/ Changeset: c35e4fd32e01 Branch: strip-docstrings-from-fixtures User: hpk42 Date: 2015-04-17 10:08:04+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/ede11185fc82/ Changeset: ede11185fc82 Branch: issue616 User: hpk42 Date: 2015-04-17 10:08:05+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/f8a1f5f5f6c8/ Changeset: f8a1f5f5f6c8 Branch: ignore-doctest-import-errors User: hpk42 Date: 2015-04-17 10:08:05+00:00 Summary: close branch Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 12:11:26 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 10:11:26 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merge pull request #274 from pytest-2.7 Message-ID: <20150417101126.17555.24885@app03.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/e966701003e4/ Changeset: e966701003e4 User: flub Date: 2015-04-17 10:10:47+00:00 Summary: Merge pull request #274 from pytest-2.7 fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing Affected #: 3 files diff -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 -r e966701003e436b00e8e517d08d16cb2b1ee9d1c CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,10 @@ - Support building wheels by using environment markers for the requirements. Thanks Ionel Maries Cristian. +- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing + when tests raised SystemExit. Thanks Holger Krekel. + + 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 -r e966701003e436b00e8e517d08d16cb2b1ee9d1c _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -121,7 +121,7 @@ def __init__(self, func): try: self.result = func() - except Exception: + except BaseException: self.excinfo = sys.exc_info() def force_result(self, result): diff -r 7d32b89da36e8714554fae12483a1fbb2bf7c1c7 -r e966701003e436b00e8e517d08d16cb2b1ee9d1c testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -607,6 +607,21 @@ assert "m1" in str(ex.value) assert "test_core.py:" in str(ex.value) + @pytest.mark.parametrize("exc", [ValueError, SystemExit]) + def test_hookwrapper_exception(self, exc): + l = [] + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + m1.hookwrapper = True + + def m2(): + raise exc + with pytest.raises(exc): + MultiCall([m2, m1], {}).execute() + assert l == ["m1 init", "m1 finish"] + class TestHookRelay: def test_happypath(self): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 12:15:55 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 10:15:55 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: pytest-2.6 dev development is finished Message-ID: <20150417101555.14734.92393@app14.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/8300e8120546/ Changeset: 8300e8120546 Branch: pytest-2.6 User: hpk42 Date: 2015-04-17 10:15:34+00:00 Summary: pytest-2.6 dev development is finished Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 12:20:33 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 10:20:33 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150417102033.9422.78442@app14.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/3ea835c697d5/ Changeset: 3ea835c697d5 Branch: pytest-2.6 User: flub Date: 2015-04-17 10:19:19+00:00 Summary: Merge pytest-2.6 heads Affected #: 1 file diff -r 773b7c62aaa11236aaf658fe4f1ddefc2efa7213 -r 3ea835c697d54ab760aedfd7dbfe89ab59b14e7e README.rst --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -.. image:: https://drone.io/bitbucket.org/hpk42/pytest/status.png - :target: https://drone.io/bitbucket.org/hpk42/pytest/latest +.. image:: https://drone.io/bitbucket.org/pytest-dev/pytest/status.png + :target: https://drone.io/bitbucket.org/pytest-dev/pytest/latest .. image:: https://pypip.in/v/pytest/badge.png :target: https://crate.io/packages/pytest/ @@ -7,9 +7,9 @@ Changelog: http://pytest.org/latest/changelog.html -Issues: https://bitbucket.org/hpk42/pytest/issues?status=open +Issues: https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open -CI: https://drone.io/bitbucket.org/hpk42/pytest +CI: https://drone.io/bitbucket.org/pytest-dev/pytest The ``pytest`` testing tool makes it easy to write small tests, yet scales to support complex functional testing. It provides @@ -44,11 +44,11 @@ and report bugs at: - http://bitbucket.org/hpk42/pytest/issues/ + http://bitbucket.org/pytest-dev/pytest/issues/ and checkout or fork repo at: - http://bitbucket.org/hpk42/pytest/ + http://bitbucket.org/pytest-dev/pytest/ Copyright Holger Krekel and others, 2004-2014 https://bitbucket.org/pytest-dev/pytest/commits/9591ae3efe55/ Changeset: 9591ae3efe55 Branch: pytest-2.6 User: flub Date: 2015-04-17 10:19:53+00:00 Summary: close pytest-2.6 (again) Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Fri Apr 17 12:27:59 2015 From: builds at drone.io (Drone.io Build) Date: Fri, 17 Apr 2015 10:27:59 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 73 Message-ID: <20150417102758.30333.40687@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/73 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3958:f61e0f6a9f49 Author : Floris Bruynooghe Branch : pytest-2.7 Message: Merged in hpk42/pytest-patches/systemexit (pull request #274) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Fri Apr 17 13:08:07 2015 From: builds at drone.io (Drone.io Build) Date: Fri, 17 Apr 2015 11:08:07 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 76 Message-ID: <20150417110712.15797.84571@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/76 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3737:9591ae3efe55 Author : Floris Bruynooghe Branch : pytest-2.6 Message: close pytest-2.6 (again) -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Fri Apr 17 16:04:14 2015 From: issues-reply at bitbucket.org (Evgeny Dontsov) Date: Fri, 17 Apr 2015 14:04:14 -0000 Subject: [Pytest-commit] Issue #724: Unreadable stdout, stderr in Teamcity Build Log (pytest-dev/pytest) Message-ID: <20150417140414.1118.69331@app10.ash-private.bitbucket.org> New issue 724: Unreadable stdout, stderr in Teamcity Build Log https://bitbucket.org/pytest-dev/pytest/issue/724/unreadable-stdout-stderr-in-teamcity-build Evgeny Dontsov: We use Teamcity for run tests. I see in stdout and stderr unreadable unicode text: ---------------------------- Captured stdout setup ----------------------------- [17:11:37][Step 1/2] \u0412\u044b\u0431\u0440\u0430\u043d \u043f\u0430\u043a\u0435\u0442 \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0430: /home/teamcity/BuildAgent/work/8b6a84b329dfc16d/var/build/linux64-gcc-release/packages/ubuntu13.zip\n\u041d\u0430\u0439\u0434\u0435\u043d \u043f\u0430\u043a\u0435\u0442 \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u043 I think, the problem in capture.py file. I offer to add encoding of captured streams: def suspendcapture_item(self, item, when): out, err = self.suspendcapture() item.add_report_section(when, "out", **out.encode("utf8")**) item.add_report_section(when, "err", **err.encode("utf8")**) From issues-reply at bitbucket.org Fri Apr 17 20:28:51 2015 From: issues-reply at bitbucket.org (foobarbazquux) Date: Fri, 17 Apr 2015 18:28:51 -0000 Subject: [Pytest-commit] Issue #725: Markers are transferred from subclasses to base class methods (pytest-dev/pytest) Message-ID: <20150417182851.9213.22204@app08.ash-private.bitbucket.org> New issue 725: Markers are transferred from subclasses to base class methods https://bitbucket.org/pytest-dev/pytest/issue/725/markers-are-transferred-from-subclasses-to foobarbazquux: pytest transfers markers from subclasses to base class methods, which can result in incorrect test selection. Example code: ``` #!python import pytest class BaseClass(): def test_foo(self): pass class Test1(BaseClass): @pytest.mark.test1 def test_test1(self): pass class Test2(BaseClass): @pytest.mark.test2 def test_test2(self): pass ``` Example run showing the `test1` marker also being applied to the `Test2` base class: ``` $ py.test -v --collect-only -m test1 ================================================================================ test session starts ================================================================================ platform darwin -- Python 2.7.8 -- py-1.4.26 -- pytest-2.6.4 -- /usr/local/opt/python/bin/python2.7 collected 4 items ======================================================================== 1 tests deselected by "-m 'test1'" ========================================================================= =========================================================================== 1 deselected in 0.01 seconds =========================================================================== ``` More detail from `ronny` via IRC: ``` 1:52 ronny joesmith: ah, i figured the bug 1:52 ronny hpk: marker transfer to base class test methods fails 1:52 ronny joesmith: we have a mechanism that transfers test markers from the classes to the methods 1:53 ronny joesmith: due to a misstake they trasnfer subclass markers to base class methods 1:53 ronny can you report a issue, i think we can issue a fix this weekend 1:55 ronny hpk: markers on a subclass transfer to a base class method 1:57 ronny https://bitbucket.org/pytest-dev/pytest/src/tip/_pytest/python.py#cl-351 is the "bad" call site 1:58 ronny and https://bitbucket.org/pytest-dev/pytest/src/tip/_pytest/python.py#cl-437 is the implementation 2:01 ronny hpk: its not an easy fix ^^ 2:02 ronny hpk: as far as i can tell marker transfer should be part of the marker plugin and the item marker set should be detached from the per object markers 2:03 ronny its a minor restructuring but it has external facing api implications ``` From commits-noreply at bitbucket.org Fri Apr 17 22:32:04 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 20:32:04 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Close branch getsourcelines-error-issue-553-pytest2.7 Message-ID: <20150417203204.24727.41027@app10.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/cccb84e7c60e/ Changeset: cccb84e7c60e Branch: getsourcelines-error-issue-553-pytest2.7 User: hpk42 Date: 2015-04-17 20:31:55+00:00 Summary: Close branch getsourcelines-error-issue-553-pytest2.7 Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 22:32:04 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 20:32:04 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in getsourcelines-error-issue-553-pytest2.7 (pull request #273) Message-ID: <20150417203204.1265.6919@app07.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/a7d1219faa25/ Changeset: a7d1219faa25 Branch: pytest-2.7 User: hpk42 Date: 2015-04-17 20:31:55+00:00 Summary: Merged in getsourcelines-error-issue-553-pytest2.7 (pull request #273) Handle inspect.getsourcelines failures in FixtureLookupError Affected #: 3 files diff -r f61e0f6a9f49f12278020f2007a1931e01821df3 -r a7d1219faa250eef78d6c2ced47f2199ca01f7e1 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- fix issue553: properly handling inspect.getsourcelines failures in + FixtureLookupError which would lead to to an internal error, + obfuscating the original problem. Thanks talljosh for initial + diagnose/patch and Bruno Oliveira for final patch. + - fix issue660: properly report scope-mismatch-access errors independently from ordering of fixture arguments. Also avoid the pytest internal traceback which does not provide diff -r f61e0f6a9f49f12278020f2007a1931e01821df3 -r a7d1219faa250eef78d6c2ced47f2199ca01f7e1 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1537,13 +1537,18 @@ # it at the requesting side for function in stack: fspath, lineno = getfslineno(function) - lines, _ = inspect.getsourcelines(function) - addline("file %s, line %s" % (fspath, lineno+1)) - for i, line in enumerate(lines): - line = line.rstrip() - addline(" " + line) - if line.lstrip().startswith('def'): - break + try: + lines, _ = inspect.getsourcelines(function) + except IOError: + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno+1)) + else: + addline("file %s, line %s" % (fspath, lineno+1)) + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith('def'): + break if msg is None: fm = self.request._fixturemanager diff -r f61e0f6a9f49f12278020f2007a1931e01821df3 -r a7d1219faa250eef78d6c2ced47f2199ca01f7e1 testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -353,6 +353,23 @@ *unrecognized* """) + def test_getsourcelines_error_issue553(self, testdir): + p = testdir.makepyfile(""" + def raise_error(obj): + raise IOError('source code not available') + + import inspect + inspect.getsourcelines = raise_error + + def test_foo(invalid_fixture): + pass + """) + res = testdir.runpytest(p) + res.stdout.fnmatch_lines([ + "*source code not available*", + "*fixture 'invalid_fixture' not found", + ]) + class TestInvocationVariants: def test_earlyinit(self, testdir): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Apr 17 22:33:44 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 17 Apr 2015 20:33:44 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: merge fix of issue553 on pytest-2.7 Message-ID: <20150417203344.15257.42637@app13.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/eb60281e3952/ Changeset: eb60281e3952 User: hpk42 Date: 2015-04-17 20:32:49+00:00 Summary: merge fix of issue553 on pytest-2.7 Affected #: 3 files diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r eb60281e3952e5959005669a16c60a9979983de2 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,11 @@ 2.7.1.dev (compared to 2.7.0) ----------------------------- +- fix issue553: properly handling inspect.getsourcelines failures in + FixtureLookupError which would lead to to an internal error, + obfuscating the original problem. Thanks talljosh for initial + diagnose/patch and Bruno Oliveira for final patch. + - fix issue660: properly report scope-mismatch-access errors independently from ordering of fixture arguments. Also avoid the pytest internal traceback which does not provide diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r eb60281e3952e5959005669a16c60a9979983de2 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1537,13 +1537,18 @@ # it at the requesting side for function in stack: fspath, lineno = getfslineno(function) - lines, _ = inspect.getsourcelines(function) - addline("file %s, line %s" % (fspath, lineno+1)) - for i, line in enumerate(lines): - line = line.rstrip() - addline(" " + line) - if line.lstrip().startswith('def'): - break + try: + lines, _ = inspect.getsourcelines(function) + except IOError: + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno+1)) + else: + addline("file %s, line %s" % (fspath, lineno+1)) + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith('def'): + break if msg is None: fm = self.request._fixturemanager diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r eb60281e3952e5959005669a16c60a9979983de2 testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -353,6 +353,23 @@ *unrecognized* """) + def test_getsourcelines_error_issue553(self, testdir): + p = testdir.makepyfile(""" + def raise_error(obj): + raise IOError('source code not available') + + import inspect + inspect.getsourcelines = raise_error + + def test_foo(invalid_fixture): + pass + """) + res = testdir.runpytest(p) + res.stdout.fnmatch_lines([ + "*source code not available*", + "*fixture 'invalid_fixture' not found", + ]) + class TestInvocationVariants: def test_earlyinit(self, testdir): Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Fri Apr 17 22:42:30 2015 From: builds at drone.io (Drone.io Build) Date: Fri, 17 Apr 2015 20:42:30 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 77 Message-ID: <20150417204229.120214.96561@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/77 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3957:cccb84e7c60e Author : holger krekel Branch : getsourcelines-error-issue-553-pytest2.7 Message: Close branch getsourcelines-error-issue-553-pytest2.7 -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Sat Apr 18 22:54:31 2015 From: issues-reply at bitbucket.org (Nikita Grishko) Date: Sat, 18 Apr 2015 20:54:31 -0000 Subject: [Pytest-commit] Issue #237: Add option to update packages without virtualenv recreation (hpk42/tox) Message-ID: <20150418205431.11607.71165@app12.ash-private.bitbucket.org> New issue 237: Add option to update packages without virtualenv recreation https://bitbucket.org/hpk42/tox/issue/237/add-option-to-update-packages-without Nikita Grishko: Hi! It would be great if you added the ability to add option to update packages without virtualenv recreation. From issues-reply at bitbucket.org Mon Apr 20 07:51:01 2015 From: issues-reply at bitbucket.org (Michael Merickel) Date: Mon, 20 Apr 2015 05:51:01 -0000 Subject: [Pytest-commit] Issue #238: group envs into an alias (hpk42/tox) Message-ID: <20150420055101.8721.76861@app12.ash-private.bitbucket.org> New issue 238: group envs into an alias https://bitbucket.org/hpk42/tox/issue/238/group-envs-into-an-alias Michael Merickel: We have always used tox to run our coverage commands. However recently we started running coverage on py2 and py3 and combining it into a single coverage report that should be 100%. Anyway, this is done using 3 environments. One for py2, one for py3 and one to aggregate the results. This is all fine but the interface has changed from `tox -e cover` to `tox -e py2-cover,py3-cover,cover`. Ideally tox would possibly support some sort of grouping instead of requiring us to wrap this invocation in something else. But wait, we found a scary solution! ```ini [testenv:py2-cover] commands = coverage run ... setenv = COVERAGE_FILE=.coverage.py2 [testenv:py3-cover] commands = coverage run ... setenv = COVERAGE_FILE=.coverage.py3 [textenv:cover] commands = coverage erase tox -e py2-cover tox -e py3-cover coverage combine coverage xml setenv = COVERAGE_FILE=.coverage deps = tox ``` A recursive tox file! Anyway this has some downsides like `tox -r` is not propagated downward to the sub-toxes. However this solves our issues with the CLI, and even gives us the opportunity to do things prior to the sub-toxes like erase without introducing yet another tox env. I don't have an actual proposal but I wanted to open an issue and get some thoughts on possible solutions inside or outside of tox. From issues-reply at bitbucket.org Mon Apr 20 16:23:38 2015 From: issues-reply at bitbucket.org (Przemek Hejman) Date: Mon, 20 Apr 2015 14:23:38 -0000 Subject: [Pytest-commit] Issue #239: Passing several commands from other section invoked as one-liner (hpk42/tox) Message-ID: <20150420142338.5630.18081@app08.ash-private.bitbucket.org> New issue 239: Passing several commands from other section invoked as one-liner https://bitbucket.org/hpk42/tox/issue/239/passing-several-commands-from-other Przemek Hejman: it seems like passing commands from other section with: ``` [testenv:smoketests] setenv= DJANGO_CONFIGURATION=ProductTestsSqlite commands = python manage.py reset_db --noinput py.cleanup -p python manage.py syncdb --no-initial-data --noinput python manage.py migrate --noinput python manage.py load_all_fixtures py.cleanup -p python manage.py reset_db --noinput [testenv:smoketests-without-bug-in-tox] setenv= DJANGO_CONFIGURATION=ProductSDKTestsSqlite commands = {[smoketests]commands} ``` inovkes all commands separated with newline **as a one, single-lined command**, because I get following errors: ``` ERROR: InvocationError: '/.toxenv/bin/python manage.py reset_db --noinput py.cleanup -p python manage.py syncdb --no-initial-data --noinput python manage.py migrate --noinput python manage.py load_all_fixtures py.cleanup -p python manage.py reset_db --noinput' ``` Responsible: RonnyPfannschmidt From commits-noreply at bitbucket.org Tue Apr 21 10:59:56 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 08:59:56 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150421085956.23656.30222@app04.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/eed4702a2a91/ Changeset: eed4702a2a91 Branch: markexpr-parser User: flub Date: 2015-04-21 08:58:13+00:00 Summary: Close feature branch We now keep feature branches in separate repositories until they are merged. Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/8abe854a64ae/ Changeset: 8abe854a64ae Branch: yield-test-run-inline User: flub Date: 2015-04-21 08:59:09+00:00 Summary: Close feature branch We now keep feature branches in separate repositories and only merge them in the main repo when they are done. Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Tue Apr 21 11:11:24 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 21 Apr 2015 09:11:24 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 80 Message-ID: <20150421091123.22470.85481@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/80 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3850:8abe854a64ae Author : Floris Bruynooghe Branch : yield-test-run-inline Message: Close feature branch -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Tue Apr 21 11:47:38 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 09:47:38 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merged in hpk42/pytest-patches/prefer_installed (pull request #275) Message-ID: <20150421094738.13652.95989@app08.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/0f7f18eb7bb7/ Changeset: 0f7f18eb7bb7 User: flub Date: 2015-04-21 09:47:33+00:00 Summary: Merged in hpk42/pytest-patches/prefer_installed (pull request #275) change test module importing behaviour to append to sys.path Affected #: 8 files diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- change test module importing behaviour to append to sys.path + instead of prepending. This better allows to run test modules + against installated versions of a package even if the package + under test has the same import root. In this example:: + + testing/__init__.py + testing/test_pkg_under_test.py + pkg_under_test/ + + the tests will preferrably run against the installed version + of pkg_under_test whereas before they would always pick + up the local version. Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.8.0.dev1' +__version__ = '2.8.0.dev2' diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -254,7 +254,7 @@ break self.tmpdir = tmpdir self.plugins = [] - self._syspathremove = [] + self._savesyspath = list(sys.path) self.chdir() # always chdir self.request.addfinalizer(self.finalize) @@ -270,8 +270,7 @@ has finished. """ - for p in self._syspathremove: - sys.path.remove(p) + sys.path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() # delete modules that have been loaded from tmpdir @@ -370,7 +369,6 @@ if path is None: path = self.tmpdir sys.path.insert(0, str(path)) - self._syspathremove.append(str(path)) def mkdir(self, name): """Create a new (sub)directory.""" diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -485,7 +485,7 @@ def _importtestmodule(self): # we assume we are only called once per module try: - mod = self.fspath.pyimport(ensuresyspath=True) + mod = self.fspath.pyimport(ensuresyspath="append") except SyntaxError: raise self.CollectError( py.code.ExceptionInfo().getrepr(style="short")) @@ -2062,3 +2062,4 @@ return node.session raise ValueError("unknown scope") return node.getparent(cls) + diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab setup.py --- a/setup.py +++ b/setup.py @@ -31,12 +31,12 @@ def has_environment_marker_support(): """ Tests that setuptools has support for PEP-426 environment marker support. - - The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 + + The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142 - + References: - + * https://wheel.readthedocs.org/en/latest/index.html#defining-conditional-dependencies * https://www.python.org/dev/peps/pep-0426/#environment-markers """ @@ -48,7 +48,7 @@ def main(): - install_requires = ['py>=1.4.25'] + install_requires = ['py>=1.4.27.dev2'] extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6" or python_version=="3.0" or python_version=="3.1"'] = ['argparse'] diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,3 +1,5 @@ +import sys +from textwrap import dedent import pytest, py class TestModule: @@ -23,6 +25,24 @@ "*HINT*", ]) + def test_import_appends_for_import(self, testdir, monkeypatch): + syspath = list(sys.path) + monkeypatch.setattr(sys, "path", syspath) + root1 = testdir.mkdir("root1") + root2 = testdir.mkdir("root2") + root1.ensure("x456.py") + root2.ensure("x456.py") + p = root2.join("test_x456.py") + p.write(dedent("""\ + import x456 + def test(): + assert x456.__file__.startswith(%r) + """ % str(root1))) + syspath.insert(0, str(root1)) + with root2.as_cwd(): + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_syntax_error_in_module(self, testdir): modcol = testdir.getmodulecol("this is a syntax error") pytest.raises(modcol.CollectError, modcol.collect) diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab testing/python/integration.py --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -238,7 +238,7 @@ class TestNoselikeTestAttribute: - def test_module(self, testdir): + def test_module_with_global_test(self, testdir): testdir.makepyfile(""" __test__ = False def test_hello(): @@ -248,7 +248,7 @@ assert not reprec.getfailedcollections() calls = reprec.getreports("pytest_runtest_logreport") assert not calls - + def test_class_and_method(self, testdir): testdir.makepyfile(""" __test__ = True diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -772,6 +772,7 @@ ]) def test_importplugin_issue375(testdir): + testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe="import aaaa") excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) assert "qwe" not in str(excinfo.value) Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Apr 21 11:47:38 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 09:47:38 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150421094738.26324.94068@app12.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/67658403f2aa/ Changeset: 67658403f2aa Branch: prefer_installed User: hpk42 Date: 2015-04-17 20:25:35+00:00 Summary: change test module importing behaviour to append to sys.path instead of prepending. This better allows to run test modules against installated versions of a package even if the package under test has the same import root. In this example:: testing/__init__.py testing/test_pkg_under_test.py pkg_under_test/ the tests will preferrably run against the installed version of pkg_under_test whereas before they would always pick up the local version. Affected #: 8 files diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- change test module importing behaviour to append to sys.path + instead of prepending. This better allows to run test modules + against installated versions of a package even if the package + under test has the same import root. In this example:: + + testing/__init__.py + testing/test_pkg_under_test.py + pkg_under_test/ + + the tests will preferrably run against the installed version + of pkg_under_test whereas before they would always pick + up the local version. Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.8.0.dev1' +__version__ = '2.8.0.dev2' diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -254,7 +254,7 @@ break self.tmpdir = tmpdir self.plugins = [] - self._syspathremove = [] + self._savesyspath = list(sys.path) self.chdir() # always chdir self.request.addfinalizer(self.finalize) @@ -270,8 +270,7 @@ has finished. """ - for p in self._syspathremove: - sys.path.remove(p) + sys.path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() # delete modules that have been loaded from tmpdir @@ -370,7 +369,6 @@ if path is None: path = self.tmpdir sys.path.insert(0, str(path)) - self._syspathremove.append(str(path)) def mkdir(self, name): """Create a new (sub)directory.""" diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -485,7 +485,7 @@ def _importtestmodule(self): # we assume we are only called once per module try: - mod = self.fspath.pyimport(ensuresyspath=True) + mod = self.fspath.pyimport(ensuresyspath="append") except SyntaxError: raise self.CollectError( py.code.ExceptionInfo().getrepr(style="short")) @@ -2057,3 +2057,4 @@ return node.session raise ValueError("unknown scope") return node.getparent(cls) + diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e setup.py --- a/setup.py +++ b/setup.py @@ -31,12 +31,12 @@ def has_environment_marker_support(): """ Tests that setuptools has support for PEP-426 environment marker support. - - The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 + + The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142 - + References: - + * https://wheel.readthedocs.org/en/latest/index.html#defining-conditional-dependencies * https://www.python.org/dev/peps/pep-0426/#environment-markers """ @@ -48,7 +48,7 @@ def main(): - install_requires = ['py>=1.4.25'] + install_requires = ['py>=1.4.27.dev2'] extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6" or python_version=="3.0" or python_version=="3.1"'] = ['argparse'] diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,3 +1,5 @@ +import sys +from textwrap import dedent import pytest, py class TestModule: @@ -23,6 +25,24 @@ "*HINT*", ]) + def test_import_appends_for_import(self, testdir, monkeypatch): + syspath = list(sys.path) + monkeypatch.setattr(sys, "path", syspath) + root1 = testdir.mkdir("root1") + root2 = testdir.mkdir("root2") + root1.ensure("x456.py") + root2.ensure("x456.py") + p = root2.join("test_x456.py") + p.write(dedent("""\ + import x456 + def test(): + assert x456.__file__.startswith(%r) + """ % str(root1))) + syspath.insert(0, str(root1)) + with root2.as_cwd(): + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_syntax_error_in_module(self, testdir): modcol = testdir.getmodulecol("this is a syntax error") pytest.raises(modcol.CollectError, modcol.collect) diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e testing/python/integration.py --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -238,7 +238,7 @@ class TestNoselikeTestAttribute: - def test_module(self, testdir): + def test_module_with_global_test(self, testdir): testdir.makepyfile(""" __test__ = False def test_hello(): @@ -248,7 +248,7 @@ assert not reprec.getfailedcollections() calls = reprec.getreports("pytest_runtest_logreport") assert not calls - + def test_class_and_method(self, testdir): testdir.makepyfile(""" __test__ = True diff -r e966701003e436b00e8e517d08d16cb2b1ee9d1c -r 67658403f2aa6344e2e53ed72c4efb487dc1529e testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -772,6 +772,7 @@ ]) def test_importplugin_issue375(testdir): + testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe="import aaaa") excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) assert "qwe" not in str(excinfo.value) https://bitbucket.org/pytest-dev/pytest/commits/0f7f18eb7bb7/ Changeset: 0f7f18eb7bb7 User: flub Date: 2015-04-21 09:47:33+00:00 Summary: Merged in hpk42/pytest-patches/prefer_installed (pull request #275) change test module importing behaviour to append to sys.path Affected #: 8 files diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- change test module importing behaviour to append to sys.path + instead of prepending. This better allows to run test modules + against installated versions of a package even if the package + under test has the same import root. In this example:: + + testing/__init__.py + testing/test_pkg_under_test.py + pkg_under_test/ + + the tests will preferrably run against the installed version + of pkg_under_test whereas before they would always pick + up the local version. Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.8.0.dev1' +__version__ = '2.8.0.dev2' diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -254,7 +254,7 @@ break self.tmpdir = tmpdir self.plugins = [] - self._syspathremove = [] + self._savesyspath = list(sys.path) self.chdir() # always chdir self.request.addfinalizer(self.finalize) @@ -270,8 +270,7 @@ has finished. """ - for p in self._syspathremove: - sys.path.remove(p) + sys.path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() # delete modules that have been loaded from tmpdir @@ -370,7 +369,6 @@ if path is None: path = self.tmpdir sys.path.insert(0, str(path)) - self._syspathremove.append(str(path)) def mkdir(self, name): """Create a new (sub)directory.""" diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -485,7 +485,7 @@ def _importtestmodule(self): # we assume we are only called once per module try: - mod = self.fspath.pyimport(ensuresyspath=True) + mod = self.fspath.pyimport(ensuresyspath="append") except SyntaxError: raise self.CollectError( py.code.ExceptionInfo().getrepr(style="short")) @@ -2062,3 +2062,4 @@ return node.session raise ValueError("unknown scope") return node.getparent(cls) + diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab setup.py --- a/setup.py +++ b/setup.py @@ -31,12 +31,12 @@ def has_environment_marker_support(): """ Tests that setuptools has support for PEP-426 environment marker support. - - The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 + + The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142 - + References: - + * https://wheel.readthedocs.org/en/latest/index.html#defining-conditional-dependencies * https://www.python.org/dev/peps/pep-0426/#environment-markers """ @@ -48,7 +48,7 @@ def main(): - install_requires = ['py>=1.4.25'] + install_requires = ['py>=1.4.27.dev2'] extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6" or python_version=="3.0" or python_version=="3.1"'] = ['argparse'] diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,3 +1,5 @@ +import sys +from textwrap import dedent import pytest, py class TestModule: @@ -23,6 +25,24 @@ "*HINT*", ]) + def test_import_appends_for_import(self, testdir, monkeypatch): + syspath = list(sys.path) + monkeypatch.setattr(sys, "path", syspath) + root1 = testdir.mkdir("root1") + root2 = testdir.mkdir("root2") + root1.ensure("x456.py") + root2.ensure("x456.py") + p = root2.join("test_x456.py") + p.write(dedent("""\ + import x456 + def test(): + assert x456.__file__.startswith(%r) + """ % str(root1))) + syspath.insert(0, str(root1)) + with root2.as_cwd(): + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_syntax_error_in_module(self, testdir): modcol = testdir.getmodulecol("this is a syntax error") pytest.raises(modcol.CollectError, modcol.collect) diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab testing/python/integration.py --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -238,7 +238,7 @@ class TestNoselikeTestAttribute: - def test_module(self, testdir): + def test_module_with_global_test(self, testdir): testdir.makepyfile(""" __test__ = False def test_hello(): @@ -248,7 +248,7 @@ assert not reprec.getfailedcollections() calls = reprec.getreports("pytest_runtest_logreport") assert not calls - + def test_class_and_method(self, testdir): testdir.makepyfile(""" __test__ = True diff -r eb60281e3952e5959005669a16c60a9979983de2 -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -772,6 +772,7 @@ ]) def test_importplugin_issue375(testdir): + testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe="import aaaa") excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) assert "qwe" not in str(excinfo.value) Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Tue Apr 21 11:48:51 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 21 Apr 2015 09:48:51 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 81 Message-ID: <20150421094851.30744.34675@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/81 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4140:0f7f18eb7bb7 Author : Floris Bruynooghe Branch : default Message: Merged in hpk42/pytest-patches/prefer_installed (pull request #275) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Tue Apr 21 11:50:01 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 21 Apr 2015 09:50:01 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 82 Message-ID: <20150421095000.98622.23831@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/82 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4140:0f7f18eb7bb7 Author : Floris Bruynooghe Branch : default Message: Merged in hpk42/pytest-patches/prefer_installed (pull request #275) -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Tue Apr 21 12:02:45 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 10:02:45 -0000 Subject: [Pytest-commit] commit/pytest: 5 new changesets Message-ID: <20150421100245.2225.31275@app08.ash-private.bitbucket.org> 5 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/a80f7dc7a9e5/ Changeset: a80f7dc7a9e5 Branch: pytester-inline-run-clean-sys-modules User: schettino72 Date: 2015-04-21 02:16:04+00:00 Summary: fix regendoc repository location on requirements-docs.txt. Affected #: 1 file diff -r eb60281e3952e5959005669a16c60a9979983de2 -r a80f7dc7a9e587b2e1804bf2fa96435472493a6b requirements-docs.txt --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,2 @@ sphinx==1.2.3 -hg+ssh://hg at bitbucket.org/RonnyPfannschmidt/regendoc#egg=regendoc +hg+ssh://hg at bitbucket.org/pytest-dev/regendoc#egg=regendoc https://bitbucket.org/pytest-dev/pytest/commits/92c20dc049bd/ Changeset: 92c20dc049bd Branch: pytester-inline-run-clean-sys-modules User: schettino72 Date: 2015-04-21 02:18:04+00:00 Summary: pytester: add method ``TmpTestdir.delete_loaded_modules()`` , and call it from ``inline_run()`` to allow temporary modules to be reloaded. Affected #: 4 files diff -r a80f7dc7a9e587b2e1804bf2fa96435472493a6b -r 92c20dc049bd1a954ddb2a7669466baa46189962 AUTHORS --- a/AUTHORS +++ b/AUTHORS @@ -48,3 +48,4 @@ Tom Viner Dave Hunt Charles Cloud +schettino72 diff -r a80f7dc7a9e587b2e1804bf2fa96435472493a6b -r 92c20dc049bd1a954ddb2a7669466baa46189962 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- pytester: add method ``TmpTestdir.delete_loaded_modules()``, and call it + from ``inline_run()`` to allow temporary modules to be reloaded. 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r a80f7dc7a9e587b2e1804bf2fa96435472493a6b -r 92c20dc049bd1a954ddb2a7669466baa46189962 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -266,7 +266,7 @@ Some methods modify the global interpreter state and this tries to clean this up. It does not remove the temporary - directlry however so it can be looked at after the test run + directory however so it can be looked at after the test run has finished. """ @@ -274,7 +274,15 @@ sys.path.remove(p) if hasattr(self, '_olddir'): self._olddir.chdir() - # delete modules that have been loaded from tmpdir + self.delete_loaded_modules() + + def delete_loaded_modules(self): + """Delete modules that have been loaded from tmpdir. + + This allows the interpreter to catch module changes in case + the module is re-imported. + + """ for name, mod in list(sys.modules.items()): if mod: fn = getattr(mod, '__file__', None) @@ -539,6 +547,7 @@ assert len(rec) == 1 reprec = rec[0] reprec.ret = ret + self.delete_loaded_modules() return reprec def parseconfig(self, *args): diff -r a80f7dc7a9e587b2e1804bf2fa96435472493a6b -r 92c20dc049bd1a954ddb2a7669466baa46189962 testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -2,6 +2,8 @@ import os from _pytest.pytester import HookRecorder from _pytest.core import PluginManager +from _pytest.main import EXIT_OK, EXIT_TESTSFAILED + def test_make_hook_recorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -121,3 +123,12 @@ testdir.inprocess_run([], [plugin]) assert plugin.configured + +def test_inline_run_clean_modules(testdir): + test_mod = testdir.makepyfile("def test_foo(): assert True") + result = testdir.inline_run(str(test_mod)) + assert result.ret == EXIT_OK + # rewrite module, now test should fail if module was re-imported + test_mod.write("def test_foo(): assert False") + result2 = testdir.inline_run(str(test_mod)) + assert result2.ret == EXIT_TESTSFAILED https://bitbucket.org/pytest-dev/pytest/commits/2361a9322d2f/ Changeset: 2361a9322d2f User: flub Date: 2015-04-21 10:00:32+00:00 Summary: Merge cleaning of sys.modules after pytester.inline_run() Merged in schettino72/pytest/pytester-inline-run-clean-sys-modules (pull request #278). Affected #: 5 files diff -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 AUTHORS --- a/AUTHORS +++ b/AUTHORS @@ -48,3 +48,4 @@ Tom Viner Dave Hunt Charles Cloud +Eduardo Schettino diff -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,9 @@ of pkg_under_test whereas before they would always pick up the local version. Thanks Holger Krekel. +- pytester: add method ``TmpTestdir.delete_loaded_modules()``, and call it + from ``inline_run()`` to allow temporary modules to be reloaded. + Thanks Eduardo Schettino. 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -266,14 +266,22 @@ Some methods modify the global interpreter state and this tries to clean this up. It does not remove the temporary - directlry however so it can be looked at after the test run + directory however so it can be looked at after the test run has finished. """ sys.path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() - # delete modules that have been loaded from tmpdir + self.delete_loaded_modules() + + def delete_loaded_modules(self): + """Delete modules that have been loaded from tmpdir. + + This allows the interpreter to catch module changes in case + the module is re-imported. + + """ for name, mod in list(sys.modules.items()): if mod: fn = getattr(mod, '__file__', None) @@ -537,6 +545,7 @@ assert len(rec) == 1 reprec = rec[0] reprec.ret = ret + self.delete_loaded_modules() return reprec def parseconfig(self, *args): diff -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 requirements-docs.txt --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,2 @@ sphinx==1.2.3 -hg+ssh://hg at bitbucket.org/RonnyPfannschmidt/regendoc#egg=regendoc +hg+ssh://hg at bitbucket.org/pytest-dev/regendoc#egg=regendoc diff -r 0f7f18eb7bb74ffeebf9bb9dad87cf26bd7cb8ab -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -2,6 +2,8 @@ import os from _pytest.pytester import HookRecorder from _pytest.core import PluginManager +from _pytest.main import EXIT_OK, EXIT_TESTSFAILED + def test_make_hook_recorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -121,3 +123,12 @@ testdir.inprocess_run([], [plugin]) assert plugin.configured + +def test_inline_run_clean_modules(testdir): + test_mod = testdir.makepyfile("def test_foo(): assert True") + result = testdir.inline_run(str(test_mod)) + assert result.ret == EXIT_OK + # rewrite module, now test should fail if module was re-imported + test_mod.write("def test_foo(): assert False") + result2 = testdir.inline_run(str(test_mod)) + assert result2.ret == EXIT_TESTSFAILED https://bitbucket.org/pytest-dev/pytest/commits/8186dfdc5250/ Changeset: 8186dfdc5250 Branch: pytester-inline-run-clean-sys-modules User: flub Date: 2015-04-21 10:01:03+00:00 Summary: Close merged feature branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/8ab27356e61e/ Changeset: 8ab27356e61e Branch: prefer_installed User: flub Date: 2015-04-21 10:01:21+00:00 Summary: Close merged feature branch Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Tue Apr 21 12:04:03 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 21 Apr 2015 10:04:03 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 83 Message-ID: <20150421100403.27904.2972@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/83 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3974:8ab27356e61e Author : Floris Bruynooghe Branch : prefer_installed Message: Close merged feature branch -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Tue Apr 21 13:27:54 2015 From: issues-reply at bitbucket.org (Floris Bruynooghe) Date: Tue, 21 Apr 2015 11:27:54 -0000 Subject: [Pytest-commit] Issue #726: Remove badges from cheeseshop homepage (pytest-dev/pytest) Message-ID: <20150421112754.4666.48923@app11.ash-private.bitbucket.org> New issue 726: Remove badges from cheeseshop homepage https://bitbucket.org/pytest-dev/pytest/issue/726/remove-badges-from-cheeseshop-homepage Floris Bruynooghe: Currently the cheeseshop homepage shows a drone.io badge with the status of the last build. This is not very useful as the trunk might occasionally break but this has no effect on the published release. A published release is always well tested and pass all it's test. Responsible: flub From commits-noreply at bitbucket.org Tue Apr 21 16:01:48 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 14:01:48 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150421140148.21855.46463@app11.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/1af652d61fa2/ Changeset: 1af652d61fa2 Branch: nodrone User: hpk42 Date: 2015-04-21 13:55:48+00:00 Summary: strike drone badge as it doesn't make sense on PYPI (where the README is rendered) Affected #: 1 file diff -r a7d1219faa250eef78d6c2ced47f2199ca01f7e1 -r 1af652d61fa21aca3557286b3196d0f9e09b00e8 README.rst --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -.. image:: https://drone.io/bitbucket.org/pytest-dev/pytest/status.png - :target: https://drone.io/bitbucket.org/pytest-dev/pytest/latest .. image:: https://pypip.in/v/pytest/badge.png :target: https://pypi.python.org/pypi/pytest https://bitbucket.org/pytest-dev/pytest/commits/b89271e45121/ Changeset: b89271e45121 Branch: pytest-2.7 User: flub Date: 2015-04-21 14:01:42+00:00 Summary: Merged in hpk42/pytest-patches/nodrone (pull request #277) strike drone badge as it doesn't make sense on PYPI (where the README is rendered) Affected #: 1 file diff -r a7d1219faa250eef78d6c2ced47f2199ca01f7e1 -r b89271e4512145c7a723a49ac760ee39240531f5 README.rst --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -.. image:: https://drone.io/bitbucket.org/pytest-dev/pytest/status.png - :target: https://drone.io/bitbucket.org/pytest-dev/pytest/latest .. image:: https://pypip.in/v/pytest/badge.png :target: https://pypi.python.org/pypi/pytest Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Apr 21 16:01:49 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 14:01:49 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merged in hpk42/pytest-patches/nodrone (pull request #277) Message-ID: <20150421140149.900.22934@app14.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/b89271e45121/ Changeset: b89271e45121 Branch: pytest-2.7 User: flub Date: 2015-04-21 14:01:42+00:00 Summary: Merged in hpk42/pytest-patches/nodrone (pull request #277) strike drone badge as it doesn't make sense on PYPI (where the README is rendered) Affected #: 1 file diff -r a7d1219faa250eef78d6c2ced47f2199ca01f7e1 -r b89271e4512145c7a723a49ac760ee39240531f5 README.rst --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -.. image:: https://drone.io/bitbucket.org/pytest-dev/pytest/status.png - :target: https://drone.io/bitbucket.org/pytest-dev/pytest/latest .. image:: https://pypip.in/v/pytest/badge.png :target: https://pypi.python.org/pypi/pytest Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Apr 21 16:05:43 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 14:05:43 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150421140543.956.90644@app13.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/321454cd5615/ Changeset: 321454cd5615 Branch: nodrone User: flub Date: 2015-04-21 14:02:34+00:00 Summary: Close merged feature branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/f54e97fa4105/ Changeset: f54e97fa4105 User: flub Date: 2015-04-21 14:04:08+00:00 Summary: Merge pull request #277 from pytest-2.7 branch This removes the drone.io badge from the README as it doesn't make sense to have it on the cheeseshop status page. Fixes issue #726. Affected #: 1 file diff -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 -r f54e97fa410507a24f2533ce62fc6d7cbb038247 README.rst --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -.. image:: https://drone.io/bitbucket.org/pytest-dev/pytest/status.png - :target: https://drone.io/bitbucket.org/pytest-dev/pytest/latest .. image:: https://pypip.in/v/pytest/badge.png :target: https://pypi.python.org/pypi/pytest Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Tue Apr 21 16:12:02 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 21 Apr 2015 14:12:02 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 84 Message-ID: <20150421141202.27930.1694@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/84 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3963:b89271e45121 Author : Floris Bruynooghe Branch : pytest-2.7 Message: Merged in hpk42/pytest-patches/nodrone (pull request #277) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Tue Apr 21 16:22:57 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 21 Apr 2015 14:22:57 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 86 Message-ID: <20150421142254.27902.50522@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/86 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4149:f54e97fa4105 Author : Floris Bruynooghe Branch : default Message: Merge pull request #277 from pytest-2.7 branch -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Tue Apr 21 16:39:22 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 21 Apr 2015 14:39:22 -0000 Subject: [Pytest-commit] commit/tox: 5 new changesets Message-ID: <20150421143922.2174.31888@app11.ash-private.bitbucket.org> 5 new commits in tox: https://bitbucket.org/hpk42/tox/commits/92d5e08065fc/ Changeset: 92d5e08065fc User: hpk42 Date: 2015-04-20 18:40:23+00:00 Summary: remove empty vendor directory Affected #: 2 files diff -r 45733e7d58f2617938fd2e9dd0c64860785b3974 -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 setup.py --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel', author_email='holger at merlinux.eu', - packages=['tox', 'tox.vendor'], + packages=['tox'], entry_points={'console_scripts': 'tox=tox:cmdline\ntox-quickstart=tox._quickstart:main'}, # we use a public tox version to test, see tox.ini's testenv # "deps" definition for the required dependencies diff -r 45733e7d58f2617938fd2e9dd0c64860785b3974 -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 tox/vendor/__init__.py --- a/tox/vendor/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# https://bitbucket.org/hpk42/tox/commits/1877e74ab9b8/ Changeset: 1877e74ab9b8 User: hpk42 Date: 2015-04-20 19:51:46+00:00 Summary: introduce new "platform" setting for tox (XXX) consider using environment marker syntax Affected #: 9 files diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ -dev +2.0.dev1 ----------- -- +- introduce a way to specify on which platform a testenvironment is to + execute: the new per-environment "platform" setting allows to specify + a regular expression which is matched against sys.platform. + If platform is set and doesn't match the test environment the + test environment is ignored, no setup or tests are attempted. 1.9.2 diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 doc/config.txt --- a/doc/config.txt +++ b/doc/config.txt @@ -155,6 +155,12 @@ (Experimentally introduced in 1.6.1) all installer commands are executed using the ``{toxinidir}`` as the current working directory. +.. confval:: platform=REGEX + + A testenv can define a new ``platform`` setting as a regular expression. + If a non-empty expression is defined and does not match against the + ``sys.platform`` string the test environment will be skipped. + .. confval:: setenv=MULTI-LINE-LIST .. versionadded:: 0.9 diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 doc/example/basic.txt --- a/doc/example/basic.txt +++ b/doc/example/basic.txt @@ -46,6 +46,19 @@ However, you can also create your own test environment names, see some of the examples in :doc:`examples <../examples>`. +specifying a platform +----------------------------------------------- + +.. versionadded:: 2.0 + +If you want to specify which platform(s) your test environment +runs on you can set a platform regular expression like this:: + + platform = linux2|darwin + +If the expression does not match against ``sys.platform`` +the test environment will be skipped. + whitelisting non-virtualenv commands ----------------------------------------------- diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,6 +7,7 @@ import tox._config from tox._config import * # noqa from tox._config import _split_env +from tox._venv import VirtualEnv class TestVenvConfig: @@ -18,6 +19,7 @@ assert config.toxworkdir.realpath() == tmpdir.join(".tox").realpath() assert config.envconfigs['py1'].basepython == sys.executable assert config.envconfigs['py1'].deps == [] + assert not config.envconfigs['py1'].platform def test_config_parsing_multienv(self, tmpdir, newconfig): config = newconfig([], """ @@ -98,6 +100,50 @@ assert parseini._is_same_dep('pkg_hello-world3==1.0', 'pkg_hello-world3<=2.0') assert not parseini._is_same_dep('pkg_hello-world3==1.0', 'otherpkg>=2.0') + +class TestConfigPlatform: + def test_config_parse_platform(self, newconfig): + config = newconfig([], """ + [testenv:py1] + platform = linux2 + """) + assert len(config.envconfigs) == 1 + assert config.envconfigs['py1'].platform == "linux2" + + def test_config_parse_platform_rex(self, newconfig, mocksession, monkeypatch): + config = newconfig([], """ + [testenv:py1] + platform = a123|b123 + """) + assert len(config.envconfigs) == 1 + envconfig = config.envconfigs['py1'] + venv = VirtualEnv(envconfig, session=mocksession) + assert not venv.matching_platform() + monkeypatch.setattr(sys, "platform", "a123") + assert venv.matching_platform() + monkeypatch.setattr(sys, "platform", "b123") + assert venv.matching_platform() + monkeypatch.undo() + assert not venv.matching_platform() + + + @pytest.mark.parametrize("plat", ["win", "lin", ]) + def test_config_parse_platform_with_factors(self, newconfig, plat, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + config = newconfig([], """ + [tox] + envlist = py27-{win,lin,osx} + [testenv] + platform = + win: win32 + lin: linux2 + """) + assert len(config.envconfigs) == 3 + platform = config.envconfigs['py27-' + plat].platform + expected = {"win": "win32", "lin": "linux2"}.get(plat) + assert platform == expected + + class TestConfigPackage: def test_defaults(self, tmpdir, newconfig): config = newconfig([], "") diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 tests/test_z_cmdline.py --- a/tests/test_z_cmdline.py +++ b/tests/test_z_cmdline.py @@ -242,6 +242,24 @@ "*ERROR*InterpreterNotFound*xyz_unknown_interpreter*", ]) +def test_skip_platform_mismatch(cmd, initproj): + initproj("interp123-0.5", filedefs={ + 'tests': {'test_hello.py': "def test_hello(): pass"}, + 'tox.ini': ''' + [testenv] + changedir=tests + platform=x123 + ''' + }) + result = cmd.run("tox") + assert not result.ret + assert "platform mismatch" not in result.stdout.str() + result = cmd.run("tox", "-v") + assert not result.ret + result.stdout.fnmatch_lines([ + "*python*platform mismatch*" + ]) + def test_skip_unknown_interpreter(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 tox/_cmdline.py --- a/tox/_cmdline.py +++ b/tox/_cmdline.py @@ -474,6 +474,9 @@ if self.config.option.sdistonly: return for venv in self.venvlist: + if not venv.matching_platform(): + venv.status = "platform mismatch" + continue # we simply omit non-matching platforms if self.setupenv(venv): if venv.envconfig.develop: self.developpkg(venv, self.config.setupdir) @@ -505,6 +508,9 @@ else: retcode = 1 self.report.error(msg) + elif status == "platform mismatch": + msg = " %s: %s" %(venv.envconfig.envname, str(status)) + self.report.verbosity1(msg) elif status and status != "skipped tests": msg = " %s: %s" %(venv.envconfig.envname, str(status)) self.report.error(msg) diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -151,9 +151,11 @@ homedir = self.toxinidir # XXX good idea? return homedir + class VenvConfig: - def __init__(self, **kw): - self.__dict__.update(kw) + def __init__(self, envname, config): + self.envname = envname + self.config = config @property def envbindir(self): @@ -195,6 +197,8 @@ "python2.5 is not supported anymore, sorry") return info.executable + + testenvprefix = "testenv:" def get_homedir(): @@ -321,8 +325,7 @@ return factors def _makeenvconfig(self, name, section, subs, config): - vc = VenvConfig(envname=name) - vc.config = config + vc = VenvConfig(config=config, envname=name) factors = set(name.split('-')) reader = IniReader(self._cfg, fallbacksections=["testenv"], factors=factors) @@ -381,6 +384,13 @@ ixserver = None name = self._replace_forced_dep(name, config) vc.deps.append(DepConfig(name, ixserver)) + + platform = "" + for platform in reader.getlist(section, "platform"): + if platform.strip(): + break + vc.platform = platform + vc.distribute = reader.getbool(section, "distribute", False) vc.sitepackages = self.config.option.sitepackages or \ reader.getbool(section, "sitepackages", False) diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 tox/_venv.py --- a/tox/_venv.py +++ b/tox/_venv.py @@ -1,5 +1,6 @@ from __future__ import with_statement import sys, os +import re import codecs import py import tox @@ -171,6 +172,9 @@ def getsupportedinterpreter(self): return self.envconfig.getsupportedinterpreter() + def matching_platform(self): + return re.match(self.envconfig.platform, sys.platform) + def create(self, action=None): #if self.getcommandpath("activate").dirpath().check(): # return diff -r 92d5e08065fc927d14ecb7e4c8081a219bce9cf5 -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 tox/interpreters.py --- a/tox/interpreters.py +++ b/tox/interpreters.py @@ -90,11 +90,12 @@ class InterpreterInfo: runnable = True - def __init__(self, name, executable, version_info): + def __init__(self, name, executable, version_info, sysplatform): assert executable and version_info self.name = name self.executable = executable self.version_info = version_info + self.sysplatform = sysplatform def __str__(self): return "" % ( @@ -163,7 +164,8 @@ def pyinfo(): import sys - return dict(version_info=tuple(sys.version_info)) + return dict(version_info=tuple(sys.version_info), + sysplatform=sys.platform) def sitepackagesdir(envdir): from distutils.sysconfig import get_python_lib https://bitbucket.org/hpk42/tox/commits/49f0a9981117/ Changeset: 49f0a9981117 User: hpk42 Date: 2015-04-21 09:51:10+00:00 Summary: trying out isolating env variables Affected #: 10 files diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,21 @@ -2.0.dev1 +2.0.0.dev1 ----------- -- introduce a way to specify on which platform a testenvironment is to - execute: the new per-environment "platform" setting allows to specify +- (new) introduce environment variable isolation: + tox now only passes the PATH variable from the tox + invocation environment to the test environment and on Windows + also ``SYSTEMROOT`` and ``PATHEXT``. If you need to pass through further + environment variables you can use the new ``passenv`` setting, + a space-separated list of environment variable names. Each name + can make use of fnmatch-style glob patterns. All environment + variables which exist in the tox-invocation environment will be copied + to the test environment. + +- (new) introduce a way to specify on which platform a testenvironment is to + execute: the new per-venv "platform" setting allows to specify a regular expression which is matched against sys.platform. - If platform is set and doesn't match the test environment the - test environment is ignored, no setup or tests are attempted. + If platform is set and doesn't match the platform spec in the test + environment the test environment is ignored, no setup or tests are attempted. 1.9.2 diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 doc/config.txt --- a/doc/config.txt +++ b/doc/config.txt @@ -169,6 +169,20 @@ will be used for all test command invocations as well as for installing the sdist package into a virtual environment. +.. confval:: passenv=SPACE-SEPARATED-GLOBNAMES + + .. versionadded:: 2.0 + + A list of wildcard environment variable names which + shall be copied from the tox invocation environment to the test + environment. If a specified environment variable doesn't exist in the tox + invocation environment it is ignored. You can use ``*`` and ``?`` to + match multiple environment variables with one name. + + Note that the ``PATH`` variable is unconditionally passed down and on + Windows ``SYSTEMROOT`` and ``PATHEXT`` will be passed down as well. + You can override these variables with the ``setenv`` option. + .. confval:: recreate=True|False(default) Always recreate virtual environment if this option is True. diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 doc/example/basic.txt --- a/doc/example/basic.txt +++ b/doc/example/basic.txt @@ -173,6 +173,22 @@ would trigger a complete reinstallation of the existing py27 environment (or create it afresh if it doesn't exist). +passing down environment variables +------------------------------------------- + +.. versionadded:: 2.0 + +By default tox will only pass the ``PATH`` environment variable (and on +windows ``SYSTEMROOT`` and ``PATHEXT``) from the tox invocation to the +test environments. If you want to pass down additional environment +variables you can use the ``passenv`` option:: + + [testenv] + passenv = LANG + +When your test commands execute they will execute with +the same LANG setting as the one with which tox was invoked. + setting environment variables ------------------------------------------- diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 setup.cfg --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 setup.py --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ description='virtualenv-based automation of test activities', long_description=open("README.rst").read(), url='http://tox.testrun.org/', - version='1.9.3.dev1', + version='2.0.0.dev1', license='http://opensource.org/licenses/MIT', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel', diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -642,6 +642,43 @@ assert envconfig.setenv['PYTHONPATH'] == 'something' assert envconfig.setenv['ANOTHER_VAL'] == 'else' + @pytest.mark.parametrize("plat", ["win32", "linux2"]) + def test_passenv(self, tmpdir, newconfig, monkeypatch, plat): + monkeypatch.setattr(sys, "platform", plat) + monkeypatch.setenv("A123A", "a") + monkeypatch.setenv("A123B", "b") + monkeypatch.setenv("BX23", "0") + config = newconfig(""" + [testenv] + passenv = A123* B?23 + """) + assert len(config.envconfigs) == 1 + envconfig = config.envconfigs['python'] + if plat == "win32": + assert "PATHEXT" in envconfig.passenv + assert "SYSTEMROOT" in envconfig.passenv + assert "PATH" in envconfig.passenv + assert "A123A" in envconfig.passenv + assert "A123B" in envconfig.passenv + + def test_passenv_with_factor(self, tmpdir, newconfig, monkeypatch): + monkeypatch.setenv("A123A", "a") + monkeypatch.setenv("A123B", "b") + monkeypatch.setenv("BX23", "0") + config = newconfig(""" + [tox] + envlist = {x1,x2} + [testenv] + passenv = + x1: A123A + x2: A123B + """) + assert len(config.envconfigs) == 2 + assert "A123A" in config.envconfigs["x1"].passenv + assert "A123B" not in config.envconfigs["x1"].passenv + assert "A123B" in config.envconfigs["x2"].passenv + assert "A123A" not in config.envconfigs["x2"].passenv + def test_changedir_override(self, tmpdir, newconfig): config = newconfig(""" [testenv] diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 tests/test_venv.py --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -517,11 +517,13 @@ assert 'PIP_REQUIRE_VIRTUALENV' not in os.environ assert '__PYVENV_LAUNCHER__' not in os.environ -def test_setenv_added_to_pcall(tmpdir, mocksession, newconfig): +def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch): pkg = tmpdir.ensure("package.tar.gz") + monkeypatch.setenv("X123", "123") config = newconfig([], """ [testenv:python] commands=python -V + passenv = X123 setenv = ENV_VAR = value """) @@ -540,9 +542,12 @@ assert 'ENV_VAR' in env assert env['ENV_VAR'] == 'value' assert env['VIRTUAL_ENV'] == str(venv.path) + assert env['X123'] == "123" - for e in os.environ: - assert e in env + assert set(env) == set(["ENV_VAR", "VIRTUAL_ENV", "PYTHONHASHSEED", + "X123", "PATH"]) + #for e in os.environ: + # assert e in env def test_installpkg_no_upgrade(tmpdir, newmocksession): pkg = tmpdir.ensure("package.tar.gz") diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 tox/__init__.py --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,5 +1,5 @@ # -__version__ = '1.9.3.dev1' +__version__ = '2.0.0.dev1' class exception: class Error(Exception): diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -1,6 +1,7 @@ import argparse import os import random +from fnmatch import fnmatchcase import sys import re import shlex @@ -366,6 +367,17 @@ if config.hashseed is not None: setenv['PYTHONHASHSEED'] = config.hashseed setenv.update(reader.getdict(section, 'setenv')) + + # read passenv + vc.passenv = set(["PATH"]) + if sys.platform == "win32": + vc.passenv.add("SYSTEMROOT") # needed for python's crypto module + vc.passenv.add("PATHEXT") # needed for discovering executables + for spec in reader.getlist(section, "passenv", sep=" "): + for name in os.environ: + if fnmatchcase(name, spec): + vc.passenv.add(name) + vc.setenv = setenv if not vc.setenv: vc.setenv = None diff -r 1877e74ab9b82b8a7d1818dd623edf2f6f2ea822 -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 tox/_venv.py --- a/tox/_venv.py +++ b/tox/_venv.py @@ -326,7 +326,10 @@ action=action, extraenv=extraenv) def _getenv(self, extraenv={}): - env = os.environ.copy() + env = {} + for envname in self.envconfig.passenv: + if envname in os.environ: + env[envname] = os.environ[envname] setenv = self.envconfig.setenv if setenv: env.update(setenv) https://bitbucket.org/hpk42/tox/commits/385a38e6afca/ Changeset: 385a38e6afca User: hpk42 Date: 2015-04-21 09:59:54+00:00 Summary: remove the long-deprecated "distribute" option as it has no effect these days. Affected #: 8 files diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ If platform is set and doesn't match the platform spec in the test environment the test environment is ignored, no setup or tests are attempted. +- remove the long-deprecated "distribute" option as it has no effect these days. 1.9.2 ----------- diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc doc/Makefile --- a/doc/Makefile +++ b/doc/Makefile @@ -37,7 +37,7 @@ -rm -rf $(BUILDDIR)/* install: clean html - @rsync -avz $(BUILDDIR)/html/ testrun.org:/www/testrun.org/tox/latest + @rsync -avz $(BUILDDIR)/html/ testrun.org:/www/testrun.org/tox/dev #latexpdf #@scp $(BUILDDIR)/latex/*.pdf testrun.org:www-tox/latest diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc doc/config.txt --- a/doc/config.txt +++ b/doc/config.txt @@ -200,15 +200,6 @@ `--download-cache` command-line option. **default**: no download cache will be used. -.. confval:: distribute=True|False - - **DEPRECATED** -- as of August 2013 you should use setuptools - which has merged most of distribute_ 's changes. Just use - the default, Luke! In future versions of tox this option might - be ignored and setuptools always chosen. - - **default:** False. - .. confval:: sitepackages=True|False Set to ``True`` if you want to create virtual environments that also diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc doc/links.txt --- a/doc/links.txt +++ b/doc/links.txt @@ -13,7 +13,6 @@ .. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall .. _pip: https://pypi.python.org/pypi/pip .. _setuptools: https://pypi.python.org/pypi/setuptools -.. _distribute: https://pypi.python.org/pypi/distribute .. _`jenkins`: http://jenkins-ci.org/ .. _sphinx: https://pypi.python.org/pypi/Sphinx .. _discover: https://pypi.python.org/pypi/discover diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -543,7 +543,6 @@ envconfig = config.envconfigs['python'] assert envconfig.commands == [["xyz", "--abc"]] assert envconfig.changedir == config.setupdir - assert envconfig.distribute == False assert envconfig.sitepackages == False assert envconfig.develop == False assert envconfig.envlogdir == envconfig.envdir.join("log") diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc tests/test_venv.py --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -80,22 +80,6 @@ p = venv.getcommandpath("easy_install") assert py.path.local(p).relto(envconfig.envbindir), p -def test_create_distribute(monkeypatch, mocksession, newconfig): - config = newconfig([], """ - [testenv:py123] - distribute=False - """) - envconfig = config.envconfigs['py123'] - venv = VirtualEnv(envconfig, session=mocksession) - assert venv.path == envconfig.envdir - assert not venv.path.check() - venv.create() - l = mocksession._pcalls - assert len(l) >= 1 - args = l[0].args - assert "--distribute" not in map(str, args) - assert "--setuptools" in map(str, args) - def test_create_sitepackages(monkeypatch, mocksession, newconfig): config = newconfig([], """ [testenv:site] @@ -158,7 +142,6 @@ monkeypatch.delenv("PIP_DOWNLOAD_CACHE", raising=False) mocksession = newmocksession([], """ [testenv:py123] - distribute=True deps= dep1 dep2 @@ -458,18 +441,6 @@ venv.update() mocksession.report.expect("*", "*recreate*") - def test_distribute_recreation(self, newconfig, mocksession): - config = newconfig([], "") - envconfig = config.envconfigs['python'] - venv = VirtualEnv(envconfig, session=mocksession) - venv.update() - cconfig = venv._getliveconfig() - cconfig.distribute = True - cconfig.writeconfig(venv.path_config) - mocksession._clearmocks() - venv.update() - mocksession.report.expect("verbosity0", "*recreate*") - def test_develop_recreation(self, newconfig, mocksession): config = newconfig([], "") envconfig = config.envconfigs['python'] @@ -523,7 +494,7 @@ config = newconfig([], """ [testenv:python] commands=python -V - passenv = X123 + passenv = x123 setenv = ENV_VAR = value """) @@ -544,8 +515,9 @@ assert env['VIRTUAL_ENV'] == str(venv.path) assert env['X123'] == "123" - assert set(env) == set(["ENV_VAR", "VIRTUAL_ENV", "PYTHONHASHSEED", - "X123", "PATH"]) + assert set(["ENV_VAR", "VIRTUAL_ENV", "PYTHONHASHSEED", "X123", "PATH"])\ + .issubset(env) + #for e in os.environ: # assert e in env diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -375,7 +375,7 @@ vc.passenv.add("PATHEXT") # needed for discovering executables for spec in reader.getlist(section, "passenv", sep=" "): for name in os.environ: - if fnmatchcase(name, spec): + if fnmatchcase(name.lower(), spec.lower()): vc.passenv.add(name) vc.setenv = setenv @@ -403,7 +403,6 @@ break vc.platform = platform - vc.distribute = reader.getbool(section, "distribute", False) vc.sitepackages = self.config.option.sitepackages or \ reader.getbool(section, "sitepackages", False) diff -r 49f0a9981117a5ca9b598dd52b2f831b86df57f2 -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc tox/_venv.py --- a/tox/_venv.py +++ b/tox/_venv.py @@ -7,20 +7,18 @@ from tox._config import DepConfig class CreationConfig: - def __init__(self, md5, python, version, distribute, sitepackages, + def __init__(self, md5, python, version, sitepackages, develop, deps): self.md5 = md5 self.python = python self.version = version - self.distribute = distribute self.sitepackages = sitepackages self.develop = develop self.deps = deps def writeconfig(self, path): lines = ["%s %s" % (self.md5, self.python)] - lines.append("%s %d %d %d" % (self.version, self.distribute, - self.sitepackages, self.develop)) + lines.append("%s %d %d" % (self.version, self.sitepackages, self.develop)) for dep in self.deps: lines.append("%s %s" % dep) path.ensure() @@ -32,27 +30,21 @@ lines = path.readlines(cr=0) value = lines.pop(0).split(None, 1) md5, python = value - version, distribute, sitepackages, develop = lines.pop(0).split( - None, 3) - distribute = bool(int(distribute)) + version, sitepackages, develop = lines.pop(0).split(None, 3) sitepackages = bool(int(sitepackages)) develop = bool(int(develop)) deps = [] for line in lines: md5, depstring = line.split(None, 1) deps.append((md5, depstring)) - return CreationConfig(md5, python, version, - distribute, sitepackages, develop, deps) - except KeyboardInterrupt: - raise - except: + return CreationConfig(md5, python, version, sitepackages, develop, deps) + except Exception: return None def matches(self, other): return (other and self.md5 == other.md5 and self.python == other.python and self.version == other.version - and self.distribute == other.distribute and self.sitepackages == other.sitepackages and self.develop == other.develop and self.deps == other.deps) @@ -148,7 +140,6 @@ python = self.envconfig._basepython_info.executable md5 = getdigest(python) version = tox.__version__ - distribute = self.envconfig.distribute sitepackages = self.envconfig.sitepackages develop = self.envconfig.develop deps = [] @@ -157,7 +148,7 @@ md5 = getdigest(raw_dep) deps.append((md5, raw_dep)) return CreationConfig(md5, python, version, - distribute, sitepackages, develop, deps) + sitepackages, develop, deps) def _getresolvedeps(self): l = [] @@ -183,10 +174,6 @@ config_interpreter = self.getsupportedinterpreter() args = [sys.executable, '-m', 'virtualenv'] - if self.envconfig.distribute: - args.append("--distribute") - else: - args.append("--setuptools") if self.envconfig.sitepackages: args.append('--system-site-packages') # add interpreter explicitly, to prevent using https://bitbucket.org/hpk42/tox/commits/7fb0ae11640d/ Changeset: 7fb0ae11640d User: hpk42 Date: 2015-04-21 14:39:13+00:00 Summary: fix issue233: avoid hanging with tox-setuptools integration example. Thanks simonb. Affected #: 2 files diff -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,8 @@ - remove the long-deprecated "distribute" option as it has no effect these days. +- fix issue233: avoid hanging with tox-setuptools integration example. Thanks simonb. + 1.9.2 ----------- diff -r 385a38e6afca771820ffd44ee23348ebb3fcf0bc -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 doc/example/basic.txt --- a/doc/example/basic.txt +++ b/doc/example/basic.txt @@ -254,7 +254,10 @@ #import here, cause outside the eggs aren't loaded import tox import shlex - errno = tox.cmdline(args=shlex.split(self.tox_args)) + args = self.tox_args + if args: + args = shlex.split(self.tox_args) + errno = tox.cmdline(args=args) sys.exit(errno) setup( Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From issues-reply at bitbucket.org Wed Apr 22 11:33:23 2015 From: issues-reply at bitbucket.org (Andrew Pashkin) Date: Wed, 22 Apr 2015 09:33:23 -0000 Subject: [Pytest-commit] Issue #727: Doctests: CWD vs PATH import ambiguity (pytest-dev/pytest) Message-ID: <20150422093323.920.94565@app13.ash-private.bitbucket.org> New issue 727: Doctests: CWD vs PATH import ambiguity https://bitbucket.org/pytest-dev/pytest/issue/727/doctests-cwd-vs-path-import-ambiguity Andrew Pashkin: Currently, when invoked with `--doctest-modules` option, Pytest imports modules from current working directory, which in some cases might break testing if it require to import package installed by `setup.py`. For example package can contain C-extensions - they can not be imported from project repository root, they need to be prepared for that by `distutils`. I see two strategies of resolving this issue: 1. Make Pytest prefer installed packages to packages from CWD 2. Make possible to explicitly set module(s) that py.test must import, so instead of `py.test --doctest-modules ./mypkg` invokation will look like `py.test --doctest-modules mypkg.*` or `py.test --doctest-modules mypkg.foo.bar`. I like this way more, because it completely unambugous - user will immediately understand, that Pytest will do `from mypkg.foo import bar` with respect to PATH. And PATH may contain either `site-packages` or current working directory. From commits-noreply at bitbucket.org Wed Apr 22 21:30:14 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 19:30:14 -0000 Subject: [Pytest-commit] commit/tox: 2 new changesets Message-ID: <20150422193014.29041.23418@app07.ash-private.bitbucket.org> 2 new commits in tox: https://bitbucket.org/hpk42/tox/commits/d13a94a309b8/ Changeset: d13a94a309b8 Branch: fix-issue-120-commands-subs User: Vladimir Vitvitskiy Date: 2015-04-21 18:47:13+00:00 Summary: fix issue #120: section subs in commands doesn't work Problem ----------- Section substitution for `commands` doesn't work correctly. Acceptance ---------------- When section substitution is specified as a single form of `commands` declaration it is replaced with parsed list of commands. When section substation happens as part of the other command line declaration - preserve original behaviour. Changes ------------ - fixes for the issue - some PEP8 violations are fixed - tests for the substation in `commands` are grouped Affected #: 2 files diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r d13a94a309b80b70263fd44c5796f711e0eaac55 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -208,6 +208,52 @@ assert getcontextname() == "jenkins" +class TestIniParserAgainstCommandsKey: + """Test parsing commands with substitutions""" + + def test_command_substitution_from_other_section(self, newconfig): + config = newconfig(""" + [section] + key = whatever + [testenv] + commands = + echo {[section]key} + """) + reader = IniReader(config._cfg) + x = reader.getargvlist("testenv", "commands") + assert x == [["echo", "whatever"]] + + def test_command_substitution_from_other_section_multiline(self, newconfig): + """Ensure referenced multiline commands form from other section injected as multiple commands.""" + config = newconfig(""" + [section] + commands = + cmd1 param11 param12 + # comment is omitted + cmd2 param21 \ + param22 + [base] + commands = cmd 1 \ + 2 3 4 + cmd 2 + [testenv] + commands = + {[section]commands} + {[section]commands} + # comment is omitted + echo {[base]commands} + """) + reader = IniReader(config._cfg) + x = reader.getargvlist("testenv", "commands") + assert x == [ + "cmd1 param11 param12".split(), + "cmd2 param21 param22".split(), + "cmd1 param11 param12".split(), + "cmd2 param21 param22".split(), + ["echo", "cmd", "1", "2", "3", "4", "cmd", "2"], + ] + + class TestIniParser: def test_getdefault_single(self, tmpdir, newconfig): config = newconfig(""" @@ -318,6 +364,14 @@ x = reader.getdefault("section", "key3") assert x == "" + def test_value_matches_section_substituion(self): + assert is_section_substitution("{[setup]commands}") + + def test_value_doesn_match_section_substitution(self): + assert is_section_substitution("{[ ]commands}") is None + assert is_section_substitution("{[setup]}") is None + assert is_section_substitution("{[setup] commands}") is None + def test_getdefault_other_section_substitution(self, newconfig): config = newconfig(""" [section] @@ -329,18 +383,6 @@ x = reader.getdefault("testenv", "key") assert x == "true" - def test_command_substitution_from_other_section(self, newconfig): - config = newconfig(""" - [section] - key = whatever - [testenv] - commands = - echo {[section]key} - """) - reader = IniReader(config._cfg) - x = reader.getargvlist("testenv", "commands") - assert x == [["echo", "whatever"]] - def test_argvlist(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -382,7 +424,6 @@ x = reader.getargvlist("section", "key2") assert x == [["cmd1", "with", "space", "grr"]] - def test_argvlist_quoting_in_command(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -394,7 +435,6 @@ x = reader.getargvlist("section", "key1") assert x == [["cmd1", "with space", "after the comment"]] - def test_argvlist_positional_substitution(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -483,7 +523,6 @@ expected = ['py.test', '-n5', '--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml', 'hello', 'world'] assert reader.getargvlist('section', 'key')[0] == expected - def test_getargv(self, newconfig): config = newconfig(""" [section] @@ -493,7 +532,6 @@ expected = ['some', 'command', 'with quoting'] assert reader.getargv('section', 'key') == expected - def test_getpath(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -514,13 +552,14 @@ key5=yes """) reader = IniReader(config._cfg) - assert reader.getbool("section", "key1") == True - assert reader.getbool("section", "key1a") == True - assert reader.getbool("section", "key2") == False - assert reader.getbool("section", "key2a") == False + assert reader.getbool("section", "key1") is True + assert reader.getbool("section", "key1a") is True + assert reader.getbool("section", "key2") is False + assert reader.getbool("section", "key2a") is False py.test.raises(KeyError, 'reader.getbool("section", "key3")') py.test.raises(tox.exception.ConfigError, 'reader.getbool("section", "key5")') + class TestConfigTestEnv: def test_commentchars_issue33(self, tmpdir, newconfig): config = newconfig(""" @@ -1559,7 +1598,7 @@ 'word', ' ', '[]', ' ', '[literal]', ' ', '{something}', ' ', '{some:other thing}', ' ', 'w', '{ord}', ' ', 'w', '{or}', 'd', ' ', 'w', '{ord}', ' ', 'w', '{o:rd}', ' ', 'w', '{o:r}', 'd', ' ', '{w:or}', 'd', ' ', 'w[]ord', ' ', '{posargs:{a key}}', - ] + ] assert parsed == expected @@ -1582,7 +1621,6 @@ parsed = list(p.words()) assert parsed == ['nosetests', ' ', '-v', ' ', '-a', ' ', '!deferred', ' ', '--with-doctest', ' ', '[]'] - @pytest.mark.skipif("sys.platform != 'win32'") def test_commands_with_backslash(self, newconfig): config = newconfig([r"hello\world"], """ diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r d13a94a309b80b70263fd44c5796f711e0eaac55 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -22,7 +22,14 @@ for version in '24,25,26,27,30,31,32,33,34,35'.split(','): default_factors['py' + version] = 'python%s.%s' % tuple(version) + def parseconfig(args=None, pkg=None): + """ + :param list[str] args: Optional list of arguments. + :type pkg: str + :rtype: :class:`Config` + :raise SystemExit: toxinit file is not found + """ if args is None: args = sys.argv[1:] parser = prepare_parse(pkg) @@ -509,16 +516,23 @@ def __str__(self): if self.indexserver: if self.indexserver.name == "default": - return self.name - return ":%s:%s" %(self.indexserver.name, self.name) + return self.name + return ":%s:%s" % (self.indexserver.name, self.name) return str(self.name) __repr__ = __str__ + class IndexServerConfig: def __init__(self, name, url=None): self.name = name self.url = url + +#: Check value matches substitution form +#: of referencing value from other section. E.g. {[base]commands} +is_section_substitution = re.compile("{\[[^{}\s]+\]\S+?}").match + + RE_ITEM_REF = re.compile( r''' (? 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/c70e7e6a4c2e/ Changeset: c70e7e6a4c2e User: hpk42 Date: 2015-04-22 19:30:11+00:00 Summary: Merged in wmyll6/tox/fix-issue-120-commands-subs (pull request #143) fix issue #120: section subs in commands doesn't work Affected #: 2 files diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r c70e7e6a4c2e2386e51158ae1d4a8f693069acb8 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -208,6 +208,52 @@ assert getcontextname() == "jenkins" +class TestIniParserAgainstCommandsKey: + """Test parsing commands with substitutions""" + + def test_command_substitution_from_other_section(self, newconfig): + config = newconfig(""" + [section] + key = whatever + [testenv] + commands = + echo {[section]key} + """) + reader = IniReader(config._cfg) + x = reader.getargvlist("testenv", "commands") + assert x == [["echo", "whatever"]] + + def test_command_substitution_from_other_section_multiline(self, newconfig): + """Ensure referenced multiline commands form from other section injected as multiple commands.""" + config = newconfig(""" + [section] + commands = + cmd1 param11 param12 + # comment is omitted + cmd2 param21 \ + param22 + [base] + commands = cmd 1 \ + 2 3 4 + cmd 2 + [testenv] + commands = + {[section]commands} + {[section]commands} + # comment is omitted + echo {[base]commands} + """) + reader = IniReader(config._cfg) + x = reader.getargvlist("testenv", "commands") + assert x == [ + "cmd1 param11 param12".split(), + "cmd2 param21 param22".split(), + "cmd1 param11 param12".split(), + "cmd2 param21 param22".split(), + ["echo", "cmd", "1", "2", "3", "4", "cmd", "2"], + ] + + class TestIniParser: def test_getdefault_single(self, tmpdir, newconfig): config = newconfig(""" @@ -318,6 +364,14 @@ x = reader.getdefault("section", "key3") assert x == "" + def test_value_matches_section_substituion(self): + assert is_section_substitution("{[setup]commands}") + + def test_value_doesn_match_section_substitution(self): + assert is_section_substitution("{[ ]commands}") is None + assert is_section_substitution("{[setup]}") is None + assert is_section_substitution("{[setup] commands}") is None + def test_getdefault_other_section_substitution(self, newconfig): config = newconfig(""" [section] @@ -329,18 +383,6 @@ x = reader.getdefault("testenv", "key") assert x == "true" - def test_command_substitution_from_other_section(self, newconfig): - config = newconfig(""" - [section] - key = whatever - [testenv] - commands = - echo {[section]key} - """) - reader = IniReader(config._cfg) - x = reader.getargvlist("testenv", "commands") - assert x == [["echo", "whatever"]] - def test_argvlist(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -382,7 +424,6 @@ x = reader.getargvlist("section", "key2") assert x == [["cmd1", "with", "space", "grr"]] - def test_argvlist_quoting_in_command(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -394,7 +435,6 @@ x = reader.getargvlist("section", "key1") assert x == [["cmd1", "with space", "after the comment"]] - def test_argvlist_positional_substitution(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -483,7 +523,6 @@ expected = ['py.test', '-n5', '--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml', 'hello', 'world'] assert reader.getargvlist('section', 'key')[0] == expected - def test_getargv(self, newconfig): config = newconfig(""" [section] @@ -493,7 +532,6 @@ expected = ['some', 'command', 'with quoting'] assert reader.getargv('section', 'key') == expected - def test_getpath(self, tmpdir, newconfig): config = newconfig(""" [section] @@ -514,13 +552,14 @@ key5=yes """) reader = IniReader(config._cfg) - assert reader.getbool("section", "key1") == True - assert reader.getbool("section", "key1a") == True - assert reader.getbool("section", "key2") == False - assert reader.getbool("section", "key2a") == False + assert reader.getbool("section", "key1") is True + assert reader.getbool("section", "key1a") is True + assert reader.getbool("section", "key2") is False + assert reader.getbool("section", "key2a") is False py.test.raises(KeyError, 'reader.getbool("section", "key3")') py.test.raises(tox.exception.ConfigError, 'reader.getbool("section", "key5")') + class TestConfigTestEnv: def test_commentchars_issue33(self, tmpdir, newconfig): config = newconfig(""" @@ -1559,7 +1598,7 @@ 'word', ' ', '[]', ' ', '[literal]', ' ', '{something}', ' ', '{some:other thing}', ' ', 'w', '{ord}', ' ', 'w', '{or}', 'd', ' ', 'w', '{ord}', ' ', 'w', '{o:rd}', ' ', 'w', '{o:r}', 'd', ' ', '{w:or}', 'd', ' ', 'w[]ord', ' ', '{posargs:{a key}}', - ] + ] assert parsed == expected @@ -1582,7 +1621,6 @@ parsed = list(p.words()) assert parsed == ['nosetests', ' ', '-v', ' ', '-a', ' ', '!deferred', ' ', '--with-doctest', ' ', '[]'] - @pytest.mark.skipif("sys.platform != 'win32'") def test_commands_with_backslash(self, newconfig): config = newconfig([r"hello\world"], """ diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r c70e7e6a4c2e2386e51158ae1d4a8f693069acb8 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -22,7 +22,14 @@ for version in '24,25,26,27,30,31,32,33,34,35'.split(','): default_factors['py' + version] = 'python%s.%s' % tuple(version) + def parseconfig(args=None, pkg=None): + """ + :param list[str] args: Optional list of arguments. + :type pkg: str + :rtype: :class:`Config` + :raise SystemExit: toxinit file is not found + """ if args is None: args = sys.argv[1:] parser = prepare_parse(pkg) @@ -509,16 +516,23 @@ def __str__(self): if self.indexserver: if self.indexserver.name == "default": - return self.name - return ":%s:%s" %(self.indexserver.name, self.name) + return self.name + return ":%s:%s" % (self.indexserver.name, self.name) return str(self.name) __repr__ = __str__ + class IndexServerConfig: def __init__(self, name, url=None): self.name = name self.url = url + +#: Check value matches substitution form +#: of referencing value from other section. E.g. {[base]commands} +is_section_substitution = re.compile("{\[[^{}\s]+\]\S+?}").match + + RE_ITEM_REF = re.compile( r''' (? 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/71ea3fac1a1d/ Changeset: 71ea3fac1a1d User: hpk42 Date: 2015-04-22 19:33:16+00:00 Summary: Merged in wmyll6/tox/issue-235-fix-installpkg (pull request #146) fix issue #235, --installpkg is broken Affected #: 2 files diff -r c70e7e6a4c2e2386e51158ae1d4a8f693069acb8 -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 tox/_cmdline.py --- a/tox/_cmdline.py +++ b/tox/_cmdline.py @@ -423,7 +423,14 @@ return False def installpkg(self, venv, sdist_path): - self.resultlog.set_header(installpkg=sdist_path) + """Install source package in the specified virtual environment. + + :param :class:`tox._config.VenvConfig`: Destination environment + :param str sdist_path: Path to the source distribution. + :return: True if package installed otherwise False. + :rtype: bool + """ + self.resultlog.set_header(installpkg=py.path.local(sdist_path)) action = self.newaction(venv, "installpkg", sdist_path) with action: try: @@ -434,6 +441,10 @@ return False def sdist(self): + """ + :return: Path to the source distribution + :rtype: py.path.local + """ if not self.config.option.sdistonly and (self.config.sdistsrc or self.config.option.installpkg): sdist_path = self.config.option.installpkg diff -r c70e7e6a4c2e2386e51158ae1d4a8f693069acb8 -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 tox/result.py --- a/tox/result.py +++ b/tox/result.py @@ -3,6 +3,7 @@ from tox import __version__ as toxver import json + class ResultLog: def __init__(self, dict=None): @@ -14,10 +15,13 @@ self.dict["host"] = py.std.socket.getfqdn() def set_header(self, installpkg): + """ + :param py.path.local installpkg: Path ot the package. + """ self.dict["installpkg"] = dict( - md5=installpkg.computehash("md5"), - sha256=installpkg.computehash("sha256"), - basename=installpkg.basename, + md5=installpkg.computehash("md5"), + sha256=installpkg.computehash("sha256"), + basename=installpkg.basename, ) def get_envlog(self, name): @@ -32,6 +36,7 @@ def loads_json(cls, data): return cls(json.loads(data)) + class EnvLog: def __init__(self, reportlog, name, dict): self.reportlog = reportlog Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Apr 22 21:33:18 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 19:33:18 -0000 Subject: [Pytest-commit] commit/tox: 2 new changesets Message-ID: <20150422193318.27333.24940@app14.ash-private.bitbucket.org> 2 new commits in tox: https://bitbucket.org/hpk42/tox/commits/47b8e230f7b3/ Changeset: 47b8e230f7b3 Branch: issue-235-fix-installpkg User: Vladimir Vitvitskiy Date: 2015-04-21 23:11:23+00:00 Summary: fix issue #235, --installpkg is broken Problem ----------- Session result log requires `py.path.local` for the source distribution when setting header. Passing str. Testing --------- `tox.ini` configuration:: ``` [testenv:X] commands={posargs} [testenv:Y] commands={posargs} ``` # download sdist pip install -d . ranger # install with a newly built fox version tox -e X -- tox -e Y --installpkg ranger-0.9.tar.gz --notest Affected #: 2 files diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r 47b8e230f7b3c5bd52b8d8d48e140db55e5f4359 tox/_cmdline.py --- a/tox/_cmdline.py +++ b/tox/_cmdline.py @@ -423,7 +423,14 @@ return False def installpkg(self, venv, sdist_path): - self.resultlog.set_header(installpkg=sdist_path) + """Install source package in the specified virtual environment. + + :param :class:`tox._config.VenvConfig`: Destination environment + :param str sdist_path: Path to the source distribution. + :return: True if package installed otherwise False. + :rtype: bool + """ + self.resultlog.set_header(installpkg=py.path.local(sdist_path)) action = self.newaction(venv, "installpkg", sdist_path) with action: try: @@ -434,6 +441,10 @@ return False def sdist(self): + """ + :return: Path to the source distribution + :rtype: py.path.local + """ if not self.config.option.sdistonly and (self.config.sdistsrc or self.config.option.installpkg): sdist_path = self.config.option.installpkg diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r 47b8e230f7b3c5bd52b8d8d48e140db55e5f4359 tox/result.py --- a/tox/result.py +++ b/tox/result.py @@ -3,6 +3,7 @@ from tox import __version__ as toxver import json + class ResultLog: def __init__(self, dict=None): @@ -14,10 +15,13 @@ self.dict["host"] = py.std.socket.getfqdn() def set_header(self, installpkg): + """ + :param py.path.local installpkg: Path ot the package. + """ self.dict["installpkg"] = dict( - md5=installpkg.computehash("md5"), - sha256=installpkg.computehash("sha256"), - basename=installpkg.basename, + md5=installpkg.computehash("md5"), + sha256=installpkg.computehash("sha256"), + basename=installpkg.basename, ) def get_envlog(self, name): @@ -32,6 +36,7 @@ def loads_json(cls, data): return cls(json.loads(data)) + class EnvLog: def __init__(self, reportlog, name, dict): self.reportlog = reportlog https://bitbucket.org/hpk42/tox/commits/71ea3fac1a1d/ Changeset: 71ea3fac1a1d User: hpk42 Date: 2015-04-22 19:33:16+00:00 Summary: Merged in wmyll6/tox/issue-235-fix-installpkg (pull request #146) fix issue #235, --installpkg is broken Affected #: 2 files diff -r c70e7e6a4c2e2386e51158ae1d4a8f693069acb8 -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 tox/_cmdline.py --- a/tox/_cmdline.py +++ b/tox/_cmdline.py @@ -423,7 +423,14 @@ return False def installpkg(self, venv, sdist_path): - self.resultlog.set_header(installpkg=sdist_path) + """Install source package in the specified virtual environment. + + :param :class:`tox._config.VenvConfig`: Destination environment + :param str sdist_path: Path to the source distribution. + :return: True if package installed otherwise False. + :rtype: bool + """ + self.resultlog.set_header(installpkg=py.path.local(sdist_path)) action = self.newaction(venv, "installpkg", sdist_path) with action: try: @@ -434,6 +441,10 @@ return False def sdist(self): + """ + :return: Path to the source distribution + :rtype: py.path.local + """ if not self.config.option.sdistonly and (self.config.sdistsrc or self.config.option.installpkg): sdist_path = self.config.option.installpkg diff -r c70e7e6a4c2e2386e51158ae1d4a8f693069acb8 -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 tox/result.py --- a/tox/result.py +++ b/tox/result.py @@ -3,6 +3,7 @@ from tox import __version__ as toxver import json + class ResultLog: def __init__(self, dict=None): @@ -14,10 +15,13 @@ self.dict["host"] = py.std.socket.getfqdn() def set_header(self, installpkg): + """ + :param py.path.local installpkg: Path ot the package. + """ self.dict["installpkg"] = dict( - md5=installpkg.computehash("md5"), - sha256=installpkg.computehash("sha256"), - basename=installpkg.basename, + md5=installpkg.computehash("md5"), + sha256=installpkg.computehash("sha256"), + basename=installpkg.basename, ) def get_envlog(self, name): @@ -32,6 +36,7 @@ def loads_json(cls, data): return cls(json.loads(data)) + class EnvLog: def __init__(self, reportlog, name, dict): self.reportlog = reportlog Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Apr 22 21:33:48 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 19:33:48 -0000 Subject: [Pytest-commit] commit/tox: hpk42: Merged in wmyll6/tox/fix-doc-env (pull request #145) Message-ID: <20150422193348.16718.51715@app14.ash-private.bitbucket.org> 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/f438d9422372/ Changeset: f438d9422372 User: hpk42 Date: 2015-04-22 19:33:46+00:00 Summary: Merged in wmyll6/tox/fix-doc-env (pull request #145) fix `docs` env Affected #: 3 files diff -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 -r f438d94223729f723debb722aa082650b44f394a doc/config-v2.txt --- a/doc/config-v2.txt +++ b/doc/config-v2.txt @@ -27,7 +27,7 @@ section for each combination. Examples of real life situations arising from this: - * http://code.larlet.fr/django-rest-framework/src/eed0f39a7e45/tox.ini + * https://github.com/tomchristie/django-rest-framework/blob/b001a146d73348af18cfc4c943d87f2f389349c9/tox.ini * https://bitbucket.org/tabo/django-treebeard/src/93b579395a9c/tox.ini @@ -216,7 +216,7 @@ ------------------------------------------------ The original `django-rest-framework tox.ini -`_ +`_ file has 159 lines and a lot of repetition, the new one would +have 20 lines and almost no repetition:: diff -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 -r f438d94223729f723debb722aa082650b44f394a doc/support.txt --- a/doc/support.txt +++ b/doc/support.txt @@ -21,9 +21,8 @@ If you are looking for on-site teaching or consulting support, contact holger at `merlinux.eu`_, an association of -experienced `well-known Python developers`_. +experienced well-known Python developers. -.. _`well-known Python developers`: http://merlinux.eu/people.txt .. _`Maciej Fijalkowski`: http://www.ohloh.net/accounts/fijal .. _`Benjamin Peterson`: http://www.ohloh.net/accounts/gutworth .. _`Testing In Python (TIP) mailing list`: http://lists.idyll.org/listinfo/testing-in-python diff -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 -r f438d94223729f723debb722aa082650b44f394a tox.ini --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps=sphinx {[testenv]deps} commands= - py.test -v + py.test -v \ --junitxml={envlogdir}/junit-{envname}.xml \ check_sphinx.py {posargs} Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Apr 22 21:33:48 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 19:33:48 -0000 Subject: [Pytest-commit] commit/tox: 4 new changesets Message-ID: <20150422193348.6437.63857@app08.ash-private.bitbucket.org> 4 new commits in tox: https://bitbucket.org/hpk42/tox/commits/5134a817a234/ Changeset: 5134a817a234 Branch: fix-doc-env User: Vladimir Vitvitskiy Date: 2015-04-21 21:01:13+00:00 Summary: fix doc environment - broken links Affected #: 1 file diff -r 7fb0ae11640dc5416ba58d58e2858cfe7d7f7c07 -r 5134a817a234062d15155f01e1a54b736321cfdf tox.ini --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps=sphinx {[testenv]deps} commands= - py.test -v + py.test -v \ --junitxml={envlogdir}/junit-{envname}.xml \ check_sphinx.py {posargs} https://bitbucket.org/hpk42/tox/commits/0d36122c714b/ Changeset: 0d36122c714b Branch: fix-doc-env User: Vladimir Vitvitskiy Date: 2015-04-21 21:07:13+00:00 Summary: fix broken links in the docs Affected #: 2 files diff -r 5134a817a234062d15155f01e1a54b736321cfdf -r 0d36122c714b343a6852afd608ddeb993dcf30c8 doc/config-v2.txt --- a/doc/config-v2.txt +++ b/doc/config-v2.txt @@ -27,7 +27,7 @@ section for each combination. Examples of real life situations arising from this: - * http://code.larlet.fr/django-rest-framework/src/eed0f39a7e45/tox.ini + * https://github.com/tomchristie/django-rest-framework/blob/b001a146d73348af18cfc4c943d87f2f389349c9/tox.ini * https://bitbucket.org/tabo/django-treebeard/src/93b579395a9c/tox.ini @@ -216,7 +216,7 @@ ------------------------------------------------ The original `django-rest-framework tox.ini -`_ +`_ file has 159 lines and a lot of repetition, the new one would +have 20 lines and almost no repetition:: diff -r 5134a817a234062d15155f01e1a54b736321cfdf -r 0d36122c714b343a6852afd608ddeb993dcf30c8 doc/support.txt --- a/doc/support.txt +++ b/doc/support.txt @@ -21,9 +21,8 @@ If you are looking for on-site teaching or consulting support, contact holger at `merlinux.eu`_, an association of -experienced `well-known Python developers`_. +experienced well-known Python developers. -.. _`well-known Python developers`: http://merlinux.eu/people.txt .. _`Maciej Fijalkowski`: http://www.ohloh.net/accounts/fijal .. _`Benjamin Peterson`: http://www.ohloh.net/accounts/gutworth .. _`Testing In Python (TIP) mailing list`: http://lists.idyll.org/listinfo/testing-in-python https://bitbucket.org/hpk42/tox/commits/57673b1d1c00/ Changeset: 57673b1d1c00 Branch: fix-doc-env User: wmyll6 Date: 2015-04-21 21:12:07+00:00 Summary: config-v2.txt edited online with Bitbucket Affected #: 1 file diff -r 0d36122c714b343a6852afd608ddeb993dcf30c8 -r 57673b1d1c007da84256b07d938f5f92968f6696 doc/config-v2.txt --- a/doc/config-v2.txt +++ b/doc/config-v2.txt @@ -216,7 +216,7 @@ ------------------------------------------------ The original `django-rest-framework tox.ini -`_ +`_ file has 159 lines and a lot of repetition, the new one would +have 20 lines and almost no repetition:: https://bitbucket.org/hpk42/tox/commits/f438d9422372/ Changeset: f438d9422372 User: hpk42 Date: 2015-04-22 19:33:46+00:00 Summary: Merged in wmyll6/tox/fix-doc-env (pull request #145) fix `docs` env Affected #: 3 files diff -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 -r f438d94223729f723debb722aa082650b44f394a doc/config-v2.txt --- a/doc/config-v2.txt +++ b/doc/config-v2.txt @@ -27,7 +27,7 @@ section for each combination. Examples of real life situations arising from this: - * http://code.larlet.fr/django-rest-framework/src/eed0f39a7e45/tox.ini + * https://github.com/tomchristie/django-rest-framework/blob/b001a146d73348af18cfc4c943d87f2f389349c9/tox.ini * https://bitbucket.org/tabo/django-treebeard/src/93b579395a9c/tox.ini @@ -216,7 +216,7 @@ ------------------------------------------------ The original `django-rest-framework tox.ini -`_ +`_ file has 159 lines and a lot of repetition, the new one would +have 20 lines and almost no repetition:: diff -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 -r f438d94223729f723debb722aa082650b44f394a doc/support.txt --- a/doc/support.txt +++ b/doc/support.txt @@ -21,9 +21,8 @@ If you are looking for on-site teaching or consulting support, contact holger at `merlinux.eu`_, an association of -experienced `well-known Python developers`_. +experienced well-known Python developers. -.. _`well-known Python developers`: http://merlinux.eu/people.txt .. _`Maciej Fijalkowski`: http://www.ohloh.net/accounts/fijal .. _`Benjamin Peterson`: http://www.ohloh.net/accounts/gutworth .. _`Testing In Python (TIP) mailing list`: http://lists.idyll.org/listinfo/testing-in-python diff -r 71ea3fac1a1dc6779e34b828cda204a8875ad000 -r f438d94223729f723debb722aa082650b44f394a tox.ini --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps=sphinx {[testenv]deps} commands= - py.test -v + py.test -v \ --junitxml={envlogdir}/junit-{envname}.xml \ check_sphinx.py {posargs} Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Apr 22 21:38:13 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 19:38:13 -0000 Subject: [Pytest-commit] commit/tox: hpk42: added changelogs for fix of issue 120 and 230 by Volodymyr Vitvitski. Message-ID: <20150422193813.4751.97799@app14.ash-private.bitbucket.org> 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/d3b25d3d9d60/ Changeset: d3b25d3d9d60 User: hpk42 Date: 2015-04-22 19:38:02+00:00 Summary: added changelogs for fix of issue 120 and 230 by Volodymyr Vitvitski. Affected #: 1 file diff -r f438d94223729f723debb722aa082650b44f394a -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,14 @@ - fix issue233: avoid hanging with tox-setuptools integration example. Thanks simonb. +- fix issue120: allow substitution for the commands section. Thanks + Volodymyr Vitvitski. + +- fix issue235: fix AttributeError with --installpkg. Thanks + Volodymyr Vitvitski. + + + 1.9.2 ----------- Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Apr 22 23:51:20 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 21:51:20 -0000 Subject: [Pytest-commit] commit/tox: Vladimir Vitvitskiy: fix PEP8 violations Message-ID: <20150422215120.20718.57894@app11.ash-private.bitbucket.org> 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/37151bb27e53/ Changeset: 37151bb27e53 User: Vladimir Vitvitskiy Date: 2015-04-22 21:18:46+00:00 Summary: fix PEP8 violations * tox.ini updated to run PEP8 checks along with flakes * added dev test environment to run any command in it or looponfail tests * fixed all PEP8 violations pytest-pep8 complained about * line width set to 99 Affected #: 16 files diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -65,8 +65,8 @@ def test_force_dep_version(self, initproj): """ - Make sure we can override dependencies configured in tox.ini when using the command line option - --force-dep. + Make sure we can override dependencies configured in tox.ini when using the command line + option --force-dep. """ initproj("example123-0.5", filedefs={ 'tox.ini': ''' @@ -83,7 +83,7 @@ config = parseconfig( ['--force-dep=dep1==1.5', '--force-dep=dep2==2.1', '--force-dep=dep3==3.0']) - assert config.option.force_dep== [ + assert config.option.force_dep == [ 'dep1==1.5', 'dep2==2.1', 'dep3==3.0'] assert [str(x) for x in config.envconfigs['python'].deps] == [ 'dep1==1.5', 'dep2==2.1', 'dep3==3.0', 'dep4==4.0', @@ -126,7 +126,6 @@ monkeypatch.undo() assert not venv.matching_platform() - @pytest.mark.parametrize("plat", ["win", "lin", ]) def test_config_parse_platform_with_factors(self, newconfig, plat, monkeypatch): monkeypatch.setattr(sys, "platform", "win32") @@ -171,6 +170,7 @@ """ % tmpdir) assert config.toxworkdir == tmpdir + class TestParseconfig: def test_search_parents(self, tmpdir): b = tmpdir.mkdir("a").mkdir("b") @@ -182,12 +182,13 @@ old.chdir() assert config.toxinipath == toxinipath + def test_get_homedir(monkeypatch): monkeypatch.setattr(py.path.local, "_gethomedir", classmethod(lambda x: {}[1])) assert not get_homedir() monkeypatch.setattr(py.path.local, "_gethomedir", - classmethod(lambda x: 0/0)) + classmethod(lambda x: 0 / 0)) assert not get_homedir() monkeypatch.setattr(py.path.local, "_gethomedir", classmethod(lambda x: "123")) @@ -224,7 +225,8 @@ assert x == [["echo", "whatever"]] def test_command_substitution_from_other_section_multiline(self, newconfig): - """Ensure referenced multiline commands form from other section injected as multiple commands.""" + """Ensure referenced multiline commands form from other section injected + as multiple commands.""" config = newconfig(""" [section] commands = @@ -275,7 +277,7 @@ reader = IniReader(config._cfg, fallbacksections=['mydefault']) assert reader is not None py.test.raises(tox.exception.ConfigError, - 'reader.getdefault("mydefault", "key2")') + 'reader.getdefault("mydefault", "key2")') def test_getdefault_fallback_sections(self, tmpdir, newconfig): config = newconfig(""" @@ -346,7 +348,7 @@ x = reader.getdefault("section", "key1") assert x == "hello" py.test.raises(tox.exception.ConfigError, - 'reader.getdefault("section", "key2")') + 'reader.getdefault("section", "key2")') def test_getdefault_environment_substitution_with_default(self, monkeypatch, newconfig): monkeypatch.setenv("KEY1", "hello") @@ -392,7 +394,7 @@ """) reader = IniReader(config._cfg) reader.addsubstitutions(item1="with space", item2="grr") - #py.test.raises(tox.exception.ConfigError, + # py.test.raises(tox.exception.ConfigError, # "reader.getargvlist('section', 'key1')") assert reader.getargvlist('section', 'key1') == [] x = reader.getargvlist("section", "key2") @@ -418,7 +420,7 @@ """) reader = IniReader(config._cfg) reader.addsubstitutions(item1="with space", item2="grr") - #py.test.raises(tox.exception.ConfigError, + # py.test.raises(tox.exception.ConfigError, # "reader.getargvlist('section', 'key1')") assert reader.getargvlist('section', 'key1') == [] x = reader.getargvlist("section", "key2") @@ -446,7 +448,7 @@ reader = IniReader(config._cfg) posargs = ['hello', 'world'] reader.addsubstitutions(posargs, item2="value2") - #py.test.raises(tox.exception.ConfigError, + # py.test.raises(tox.exception.ConfigError, # "reader.getargvlist('section', 'key1')") assert reader.getargvlist('section', 'key1') == [] argvlist = reader.getargvlist("section", "key2") @@ -455,7 +457,7 @@ reader = IniReader(config._cfg) reader.addsubstitutions([], item2="value2") - #py.test.raises(tox.exception.ConfigError, + # py.test.raises(tox.exception.ConfigError, # "reader.getargvlist('section', 'key1')") assert reader.getargvlist('section', 'key1') == [] argvlist = reader.getargvlist("section", "key2") @@ -490,8 +492,7 @@ x = reader.getargvlist("section", "key2") assert x == [["cmd1", "-f", "foo", "bar baz"]] - def test_positional_arguments_are_only_replaced_when_standing_alone(self, - tmpdir, newconfig): + def test_positional_arguments_are_only_replaced_when_standing_alone(self, tmpdir, newconfig): config = newconfig(""" [section] key= @@ -520,7 +521,9 @@ posargs = ['hello', 'world'] reader.addsubstitutions(posargs, envlogdir='ENV_LOG_DIR', envname='ENV_NAME') - expected = ['py.test', '-n5', '--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml', 'hello', 'world'] + expected = [ + 'py.test', '-n5', '--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml', 'hello', 'world' + ] assert reader.getargvlist('section', 'key')[0] == expected def test_getargv(self, newconfig): @@ -582,8 +585,8 @@ envconfig = config.envconfigs['python'] assert envconfig.commands == [["xyz", "--abc"]] assert envconfig.changedir == config.setupdir - assert envconfig.sitepackages == False - assert envconfig.develop == False + assert envconfig.sitepackages is False + assert envconfig.develop is False assert envconfig.envlogdir == envconfig.envdir.join("log") assert list(envconfig.setenv.keys()) == ['PYTHONHASHSEED'] hashseed = envconfig.setenv['PYTHONHASHSEED'] @@ -596,7 +599,7 @@ def test_sitepackages_switch(self, tmpdir, newconfig): config = newconfig(["--sitepackages"], "") envconfig = config.envconfigs['python'] - assert envconfig.sitepackages == True + assert envconfig.sitepackages is True def test_installpkg_tops_develop(self, newconfig): config = newconfig(["--installpkg=abc"], """ @@ -918,7 +921,7 @@ assert argv[0] == ["cmd1", "hello"] def test_take_dependencies_from_other_testenv(self, newconfig): - inisource=""" + inisource = """ [testenv] deps= pytest @@ -933,7 +936,7 @@ assert packages == ['pytest', 'pytest-cov', 'fun'] def test_take_dependencies_from_other_section(self, newconfig): - inisource=""" + inisource = """ [testing:pytest] deps= pytest @@ -953,7 +956,7 @@ assert packages == ['pytest', 'pytest-cov', 'mock', 'fun'] def test_multilevel_substitution(self, newconfig): - inisource=""" + inisource = """ [testing:pytest] deps= pytest @@ -978,7 +981,7 @@ assert packages == ['pytest', 'pytest-cov', 'mock', 'fun'] def test_recursive_substitution_cycle_fails(self, newconfig): - inisource=""" + inisource = """ [testing:pytest] deps= {[testing:mock]deps} @@ -1004,7 +1007,7 @@ assert conf.changedir.dirpath().realpath() == tmpdir.realpath() def test_factors(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = a-x,b @@ -1022,7 +1025,7 @@ assert [dep.name for dep in configs['b'].deps] == ["dep-all", "dep-b"] def test_factor_ops(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = {a,b}-{x,y} @@ -1040,7 +1043,7 @@ assert get_deps("b-y") == ["dep-a-or-b", "dep-ab-and-y"] def test_default_factors(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = py{26,27,33,34}-dep @@ -1055,7 +1058,7 @@ @pytest.mark.issue188 def test_factors_in_boolean(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = py{27,33} @@ -1069,7 +1072,7 @@ @pytest.mark.issue190 def test_factors_in_setenv(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = py27,py26 @@ -1083,7 +1086,7 @@ @pytest.mark.issue191 def test_factor_use_not_checked(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = py27-{a,b} @@ -1095,7 +1098,7 @@ @pytest.mark.issue198 def test_factors_groups_touch(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = {a,b}{-x,} @@ -1107,7 +1110,7 @@ assert set(configs.keys()) == set(['a', 'a-x', 'b', 'b-x']) def test_period_in_factor(self, newconfig): - inisource=""" + inisource = """ [tox] envlist = py27-{django1.6,django1.7} @@ -1185,7 +1188,7 @@ [testenv:py27] basepython=python2.7 """ - #py.test.raises(tox.exception.ConfigError, + # py.test.raises(tox.exception.ConfigError, # "newconfig(['-exyz'], inisource)") config = newconfig([], inisource) assert config.envlist == ["py26"] @@ -1222,7 +1225,7 @@ assert env.basepython == name else: assert name.startswith("py") - bp = "python%s.%s" %(name[2], name[3]) + bp = "python%s.%s" % (name[2], name[3]) assert env.basepython == bp def test_envlist_expansion(self, newconfig): @@ -1284,6 +1287,7 @@ assert env.basepython == "python2.4" assert env.commands == [['xyz']] + class TestHashseedOption: def _get_envconfigs(self, newconfig, args=None, tox_ini=None, @@ -1331,7 +1335,7 @@ args = ['--hashseed', ''] self._check_testenv(newconfig, '', args=args) - @pytest.mark.xfail(sys.version_info >= (3,2), + @pytest.mark.xfail(sys.version_info >= (3, 2), reason="at least Debian python 3.2/3.3 have a bug: " "http://bugs.python.org/issue11884") def test_passing_no_argument(self, tmpdir, newconfig): @@ -1378,6 +1382,7 @@ """ next_seed = [1000] # This function is guaranteed to generate a different value each time. + def make_hashseed(): next_seed[0] += 1 return str(next_seed[0]) @@ -1401,6 +1406,7 @@ self._check_hashseed(envconfigs["hash1"], '2') self._check_hashseed(envconfigs["hash2"], '123456789') + class TestIndexServer: def test_indexserver(self, tmpdir, newconfig): config = newconfig(""" @@ -1409,7 +1415,7 @@ name1 = XYZ name2 = ABC """) - assert config.indexserver['default'].url == None + assert config.indexserver['default'].url is None assert config.indexserver['name1'].url == "XYZ" assert config.indexserver['name2'].url == "ABC" @@ -1423,10 +1429,10 @@ config = newconfig([], inisource) assert config.indexserver['default'].url == "http://pypi.testrun.org" assert config.indexserver['name1'].url == "whatever" - config = newconfig(['-i','qwe'], inisource) + config = newconfig(['-i', 'qwe'], inisource) assert config.indexserver['default'].url == "qwe" assert config.indexserver['name1'].url == "whatever" - config = newconfig(['-i','name1=abc', '-i','qwe2'], inisource) + config = newconfig(['-i', 'name1=abc', '-i', 'qwe2'], inisource) assert config.indexserver['default'].url == "qwe2" assert config.indexserver['name1'].url == "abc" @@ -1447,8 +1453,8 @@ config = newconfig([], inisource) expected = "file://%s/.pip/downloads/simple" % config.homedir assert config.indexserver['default'].url == expected - assert config.indexserver['local1'].url == \ - config.indexserver['default'].url + assert config.indexserver['local1'].url == config.indexserver['default'].url + class TestParseEnv: @@ -1467,6 +1473,7 @@ config = newconfig([], inisource) assert config.envconfigs['hello'].recreate + class TestCmdInvocation: def test_help(self, cmd): result = cmd.run("tox", "-h") @@ -1544,6 +1551,7 @@ r'*deps=*dep1, dep2==5.0*', ]) + class TestArgumentParser: def test_dash_e_single_1(self): @@ -1591,12 +1599,15 @@ assert list(p.words()) == ['{sub:something with spaces}'] def test_command_parser_with_complex_word_set(self): - complex_case = 'word [] [literal] {something} {some:other thing} w{ord} w{or}d w{ord} w{o:rd} w{o:r}d {w:or}d w[]ord {posargs:{a key}}' + complex_case = ( + 'word [] [literal] {something} {some:other thing} w{ord} w{or}d w{ord} ' + 'w{o:rd} w{o:r}d {w:or}d w[]ord {posargs:{a key}}') p = CommandParser(complex_case) parsed = list(p.words()) expected = [ 'word', ' ', '[]', ' ', '[literal]', ' ', '{something}', ' ', '{some:other thing}', - ' ', 'w', '{ord}', ' ', 'w', '{or}', 'd', ' ', 'w', '{ord}', ' ', 'w', '{o:rd}', ' ', 'w', '{o:r}', 'd', ' ', '{w:or}', 'd', + ' ', 'w', '{ord}', ' ', 'w', '{or}', 'd', ' ', 'w', '{ord}', ' ', 'w', '{o:rd}', ' ', + 'w', '{o:r}', 'd', ' ', '{w:or}', 'd', ' ', 'w[]ord', ' ', '{posargs:{a key}}', ] @@ -1619,7 +1630,10 @@ cmd = "nosetests -v -a !deferred --with-doctest []" p = CommandParser(cmd) parsed = list(p.words()) - assert parsed == ['nosetests', ' ', '-v', ' ', '-a', ' ', '!deferred', ' ', '--with-doctest', ' ', '[]'] + assert parsed == [ + 'nosetests', ' ', '-v', ' ', '-a', ' ', '!deferred', ' ', + '--with-doctest', ' ', '[]' + ] @pytest.mark.skipif("sys.platform != 'win32'") def test_commands_with_backslash(self, newconfig): diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tests/test_interpreters.py --- a/tests/test_interpreters.py +++ b/tests/test_interpreters.py @@ -4,10 +4,12 @@ import pytest from tox.interpreters import * # noqa + @pytest.fixture def interpreters(): return Interpreters() + @pytest.mark.skipif("sys.platform != 'win32'") def test_locate_via_py(monkeypatch): class PseudoPy: @@ -16,6 +18,7 @@ assert args[1] == '-c' # Return value needs to actually exist! return sys.executable + @staticmethod def ret_pseudopy(name): assert name == 'py' @@ -24,6 +27,7 @@ monkeypatch.setattr(py.path.local, 'sysfind', ret_pseudopy) assert locate_via_py('3', '2') == sys.executable + def test_find_executable(): p = find_executable(sys.executable) assert p == py.path.local(sys.executable) @@ -41,10 +45,11 @@ p = find_executable(name) assert p popen = py.std.subprocess.Popen([str(p), '-V'], - stderr=py.std.subprocess.PIPE) + stderr=py.std.subprocess.PIPE) stdout, stderr = popen.communicate() assert ver in py.builtin._totext(stderr, "ascii") + def test_find_executable_extra(monkeypatch): @staticmethod def sysfind(x): @@ -53,6 +58,7 @@ t = find_executable("qweqwe") assert t == "hello" + def test_run_and_get_interpreter_info(): name = os.path.basename(sys.executable) info = run_and_get_interpreter_info(name, sys.executable) @@ -60,6 +66,7 @@ assert info.name == name assert info.executable == sys.executable + class TestInterpreters: def test_get_info_self_exceptions(self, interpreters): diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tests/test_result.py --- a/tests/test_result.py +++ b/tests/test_result.py @@ -4,12 +4,14 @@ import tox import pytest + @pytest.fixture def pkg(tmpdir): p = tmpdir.join("hello-1.0.tar.gz") p.write("whatever") return p + def test_pre_set_header(pkg): replog = ResultLog() d = replog.dict @@ -22,6 +24,7 @@ replog2 = ResultLog.loads_json(data) assert replog2.dict == replog.dict + def test_set_header(pkg): replog = ResultLog() d = replog.dict @@ -31,13 +34,15 @@ assert replog.dict["toxversion"] == tox.__version__ assert replog.dict["platform"] == sys.platform assert replog.dict["host"] == py.std.socket.getfqdn() - assert replog.dict["installpkg"] == {"basename": "hello-1.0.tar.gz", - "md5": pkg.computehash("md5"), - "sha256": pkg.computehash("sha256")} + assert replog.dict["installpkg"] == { + "basename": "hello-1.0.tar.gz", + "md5": pkg.computehash("md5"), + "sha256": pkg.computehash("sha256")} data = replog.dumps_json() replog2 = ResultLog.loads_json(data) assert replog2.dict == replog.dict + def test_addenv_setpython(pkg): replog = ResultLog() replog.set_header(installpkg=pkg) @@ -47,6 +52,7 @@ assert envlog.dict["python"]["version"] == sys.version assert envlog.dict["python"]["executable"] == sys.executable + def test_get_commandlog(pkg): replog = ResultLog() replog.set_header(installpkg=pkg) @@ -60,4 +66,3 @@ assert envlog.dict["setup"] setuplog2 = replog.get_envlog("py26").get_commandlog("setup") assert setuplog2.list == setuplog.list - diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tests/test_venv.py --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -1,12 +1,13 @@ import py import tox import pytest -import os, sys +import os +import sys import tox._config from tox._venv import * # noqa from tox.interpreters import NoInterpreterInfo -#def test_global_virtualenv(capfd): +# def test_global_virtualenv(capfd): # v = VirtualEnv() # l = v.list() # assert l @@ -14,8 +15,11 @@ # assert not out # assert not err # + + def test_getdigest(tmpdir): - assert getdigest(tmpdir) == "0"*32 + assert getdigest(tmpdir) == "0" * 32 + def test_getsupportedinterpreter(monkeypatch, newconfig, mocksession): config = newconfig([], """ @@ -25,8 +29,7 @@ venv = VirtualEnv(config.envconfigs['python'], session=mocksession) interp = venv.getsupportedinterpreter() # realpath needed for debian symlinks - assert py.path.local(interp).realpath() \ - == py.path.local(sys.executable).realpath() + assert py.path.local(interp).realpath() == py.path.local(sys.executable).realpath() monkeypatch.setattr(sys, 'platform', "win32") monkeypatch.setattr(venv.envconfig, 'basepython', 'jython') py.test.raises(tox.exception.UnsupportedInterpreter, @@ -58,14 +61,14 @@ assert "virtualenv" == str(args[2]) if sys.platform != "win32": # realpath is needed for stuff like the debian symlinks - assert py.path.local(sys.executable).realpath() \ - == py.path.local(args[0]).realpath() - #assert Envconfig.toxworkdir in args + assert py.path.local(sys.executable).realpath() == py.path.local(args[0]).realpath() + # assert Envconfig.toxworkdir in args assert venv.getcommandpath("easy_install", cwd=py.path.local()) interp = venv._getliveconfig().python assert interp == venv.envconfig._basepython_info.executable assert venv.path_config.check(exists=False) + @pytest.mark.skipif("sys.platform == 'win32'") def test_commandpath_venv_precendence(tmpdir, monkeypatch, mocksession, newconfig): @@ -80,6 +83,7 @@ p = venv.getcommandpath("easy_install") assert py.path.local(p).relto(envconfig.envbindir), p + def test_create_sitepackages(monkeypatch, mocksession, newconfig): config = newconfig([], """ [testenv:site] @@ -106,6 +110,7 @@ assert "--system-site-packages" not in map(str, args) assert "--no-site-packages" not in map(str, args) + def test_install_deps_wildcard(newmocksession): mocksession = newmocksession([], """ [tox] @@ -128,8 +133,8 @@ assert l[-1].cwd == venv.envconfig.config.toxinidir assert "pip" in str(args[0]) assert args[1] == "install" - #arg = "--download-cache=" + str(venv.envconfig.downloadcache) - #assert arg in args[2:] + # arg = "--download-cache=" + str(venv.envconfig.downloadcache) + # assert arg in args[2:] args = [arg for arg in args if str(arg).endswith("dep1-1.1.zip")] assert len(args) == 1 @@ -162,6 +167,7 @@ deps = list(filter(None, [x[1] for x in venv._getliveconfig().deps])) assert deps == ['dep1', 'dep2'] + def test_install_deps_indexserver(newmocksession): mocksession = newmocksession([], """ [tox] @@ -194,6 +200,7 @@ assert "-i ABC" in args assert "dep3" in args + def test_install_deps_pre(newmocksession): mocksession = newmocksession([], """ [testenv] @@ -213,6 +220,7 @@ assert "--pre " in args assert "dep1" in args + def test_installpkg_indexserver(newmocksession, tmpdir): mocksession = newmocksession([], """ [tox] @@ -228,6 +236,7 @@ args = " ".join(l[0].args) assert "-i ABC" in args + def test_install_recreate(newmocksession, tmpdir): pkg = tmpdir.ensure("package.tar.gz") mocksession = newmocksession(['--recreate'], """ @@ -241,6 +250,7 @@ venv.update() mocksession.report.expect("verbosity0", "*recreate*") + def test_test_hashseed_is_in_output(newmocksession): original_make_hashseed = tox._config.make_hashseed tox._config.make_hashseed = lambda: '123456789' @@ -255,6 +265,7 @@ venv.test() mocksession.report.expect("verbosity0", "python runtests: PYTHONHASHSEED='123456789'") + def test_test_runtests_action_command_is_in_output(newmocksession): mocksession = newmocksession([], ''' [testenv] @@ -265,6 +276,7 @@ venv.test() mocksession.report.expect("verbosity0", "*runtests*commands?0? | echo foo bar") + def test_install_error(newmocksession, monkeypatch): mocksession = newmocksession(['--recreate'], """ [testenv] @@ -277,6 +289,7 @@ mocksession.report.expect("error", "*not find*qwelkqw*") assert venv.status == "commands failed" + def test_install_command_not_installed(newmocksession, monkeypatch): mocksession = newmocksession(['--recreate'], """ [testenv] @@ -288,6 +301,7 @@ mocksession.report.expect("warning", "*test command found but not*") assert venv.status == 0 + def test_install_command_whitelisted(newmocksession, monkeypatch): mocksession = newmocksession(['--recreate'], """ [testenv] @@ -303,6 +317,7 @@ invert=True) assert venv.status == "commands failed" + @pytest.mark.skipif("not sys.platform.startswith('linux')") def test_install_command_not_installed_bash(newmocksession): mocksession = newmocksession(['--recreate'], """ @@ -340,6 +355,7 @@ for x in args: assert "--download-cache" not in args, args + class TestCreationConfig: def test_basic(self, newconfig, mocksession, tmpdir): @@ -435,7 +451,7 @@ venv = VirtualEnv(envconfig, session=mocksession) venv.update() cconfig = venv._getliveconfig() - cconfig.deps[:] = [("1"*32, "xyz.zip")] + cconfig.deps[:] = [("1" * 32, "xyz.zip")] cconfig.writeconfig(venv.path_config) mocksession._clearmocks() venv.update() @@ -453,6 +469,7 @@ venv.update() mocksession.report.expect("verbosity0", "*recreate*") + class TestVenvTest: def test_patchPATH(self, newmocksession, monkeypatch): @@ -468,14 +485,14 @@ assert oldpath == "xyz" res = os.environ['PATH'] assert res == "%s%sxyz" % (envconfig.envbindir, os.pathsep) - p = "xyz"+os.pathsep+str(envconfig.envbindir) + p = "xyz" + os.pathsep + str(envconfig.envbindir) monkeypatch.setenv("PATH", p) venv.patchPATH() res = os.environ['PATH'] - assert res == "%s%s%s" %(envconfig.envbindir, os.pathsep, p) + assert res == "%s%s%s" % (envconfig.envbindir, os.pathsep, p) assert envconfig.commands - monkeypatch.setattr(venv, '_pcall', lambda *args, **kwargs: 0/0) + monkeypatch.setattr(venv, '_pcall', lambda *args, **kwargs: 0 / 0) py.test.raises(ZeroDivisionError, "venv._install(list('123'))") py.test.raises(ZeroDivisionError, "venv.test()") py.test.raises(ZeroDivisionError, "venv.run_install_command(['qwe'])") @@ -488,6 +505,7 @@ assert 'PIP_REQUIRE_VIRTUALENV' not in os.environ assert '__PYVENV_LAUNCHER__' not in os.environ + def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch): pkg = tmpdir.ensure("package.tar.gz") monkeypatch.setenv("X123", "123") @@ -516,11 +534,12 @@ assert env['X123'] == "123" assert set(["ENV_VAR", "VIRTUAL_ENV", "PYTHONHASHSEED", "X123", "PATH"])\ - .issubset(env) + .issubset(env) - #for e in os.environ: + # for e in os.environ: # assert e in env + def test_installpkg_no_upgrade(tmpdir, newmocksession): pkg = tmpdir.ensure("package.tar.gz") mocksession = newmocksession([], "") @@ -532,6 +551,7 @@ assert len(l) == 1 assert '-U' not in l[0].args + def test_installpkg_upgrade(newmocksession, tmpdir): pkg = tmpdir.ensure("package.tar.gz") mocksession = newmocksession([], "") @@ -545,6 +565,7 @@ assert '-U' in l[0].args[:index] assert '--no-deps' in l[0].args[:index] + def test_run_install_command(newmocksession): mocksession = newmocksession([], "") venv = mocksession.getenv('python') @@ -559,6 +580,7 @@ env = l[0].env assert env is not None + def test_run_custom_install_command(newmocksession): mocksession = newmocksession([], """ [testenv] @@ -574,6 +596,7 @@ assert 'easy_install' in l[0].args[0] assert l[0].args[1:] == ['whatever'] + def test_command_relative_issue26(newmocksession, tmpdir, monkeypatch): mocksession = newmocksession([], """ [testenv] @@ -591,6 +614,7 @@ assert x4.endswith(os.sep + 'x') mocksession.report.expect("warning", "*test command found but not*") + def test_sethome_only_on_option(newmocksession, monkeypatch): mocksession = newmocksession([], "") venv = mocksession.getenv('python') @@ -598,6 +622,7 @@ monkeypatch.setattr(tox._venv, "hack_home_env", None) venv._install(["x"], action=action) + def test_sethome_works_on_option(newmocksession, monkeypatch): mocksession = newmocksession(["--set-home", "-i ALL=http://qwe"], "") venv = mocksession.getenv('python') @@ -622,6 +647,7 @@ assert not tmpdir.join(".pydistutils.cfg").check() assert "PIP_INDEX_URL" not in env + def test_hack_home_env_passthrough(tmpdir, monkeypatch): from tox._venv import hack_home_env env = hack_home_env(tmpdir, "http://index") diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tests/test_z_cmdline.py --- a/tests/test_z_cmdline.py +++ b/tests/test_z_cmdline.py @@ -12,35 +12,40 @@ from tox._cmdline import Session from tox._config import parseconfig + def test_report_protocol(newconfig): config = newconfig([], """ [testenv:mypython] deps=xy """) + class Popen: def __init__(self, *args, **kwargs): pass + def communicate(self): return "", "" + def wait(self): pass session = Session(config, popen=Popen, - Report=ReportExpectMock) + Report=ReportExpectMock) report = session.report report.expect("using") venv = session.getvenv("mypython") venv.update() report.expect("logpopen") + def test__resolve_pkg(tmpdir, mocksession): distshare = tmpdir.join("distshare") spec = distshare.join("pkg123-*") py.test.raises(tox.exception.MissingDirectory, - 'mocksession._resolve_pkg(spec)') + 'mocksession._resolve_pkg(spec)') distshare.ensure(dir=1) py.test.raises(tox.exception.MissingDependency, - 'mocksession._resolve_pkg(spec)') + 'mocksession._resolve_pkg(spec)') distshare.ensure("pkg123-1.3.5.zip") p = distshare.ensure("pkg123-1.4.5.zip") @@ -58,6 +63,7 @@ result = mocksession._resolve_pkg(spec) assert result == p + def test__resolve_pkg_doubledash(tmpdir, mocksession): distshare = tmpdir.join("distshare") p = distshare.ensure("pkg-mine-1.3.0.zip") @@ -68,7 +74,6 @@ assert res == p - class TestSession: def test_make_sdist(self, initproj): initproj("example123-0.5", filedefs={ @@ -116,7 +121,7 @@ action.popen(["echo", ]) match = mocksession.report.getnext("logpopen") assert match[1].outpath.relto(mocksession.config.logdir) - assert match[1].shell == False + assert match[1].shell is False def test_summary_status(self, initproj, capfd): initproj("logexample123-0.5", filedefs={ @@ -177,6 +182,7 @@ "*created sdist package at*", ]) + def test_minversion(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -191,6 +197,7 @@ ]) assert result.ret + def test_run_custom_install_command_error(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tox.ini': ''' @@ -204,6 +211,7 @@ ]) assert result.ret + def test_unknown_interpreter_and_env(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -226,6 +234,7 @@ "*ERROR*unknown*", ]) + def test_unknown_interpreter(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -242,6 +251,7 @@ "*ERROR*InterpreterNotFound*xyz_unknown_interpreter*", ]) + def test_skip_platform_mismatch(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -260,6 +270,7 @@ "*python*platform mismatch*" ]) + def test_skip_unknown_interpreter(cmd, initproj): initproj("interp123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -276,6 +287,7 @@ "*SKIPPED*InterpreterNotFound*xyz_unknown_interpreter*", ]) + def test_unknown_dep(cmd, initproj): initproj("dep123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -291,6 +303,7 @@ "*ERROR*could not install*qweqwe123*", ]) + def test_unknown_environment(cmd, initproj): initproj("env123-0.7", filedefs={ 'tox.ini': '' @@ -301,13 +314,13 @@ "*ERROR*unknown*environment*qpwoei*", ]) + def test_skip_sdist(cmd, initproj): initproj("pkg123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, 'setup.py': """ syntax error - """ - , + """, 'tox.ini': ''' [tox] skipsdist=True @@ -318,12 +331,12 @@ result = cmd.run("tox", ) assert result.ret == 0 + def test_minimal_setup_py_empty(cmd, initproj): initproj("pkg123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, 'setup.py': """ - """ - , + """, 'tox.ini': '' }) @@ -333,13 +346,13 @@ "*ERROR*empty*", ]) + def test_minimal_setup_py_comment_only(cmd, initproj): initproj("pkg123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, 'setup.py': """\n# some comment - """ - , + """, 'tox.ini': '' }) @@ -349,14 +362,14 @@ "*ERROR*empty*", ]) + def test_minimal_setup_py_non_functional(cmd, initproj): initproj("pkg123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, 'setup.py': """ import sys - """ - , + """, 'tox.ini': '' }) @@ -366,13 +379,13 @@ "*ERROR*check setup.py*", ]) + def test_sdist_fails(cmd, initproj): initproj("pkg123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, 'setup.py': """ syntax error - """ - , + """, 'tox.ini': '', }) result = cmd.run("tox", ) @@ -381,6 +394,7 @@ "*FAIL*could not package project*", ]) + def test_package_install_fails(cmd, initproj): initproj("pkg123-0.7", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, @@ -395,8 +409,7 @@ packages=['pkg123',], install_requires=['qweqwe123'], ) - """ - , + """, 'tox.ini': '', }) result = cmd.run("tox", ) @@ -406,14 +419,14 @@ ]) - class TestToxRun: @pytest.fixture def example123(self, initproj): initproj("example123-0.5", filedefs={ - 'tests': {'test_hello.py': """ - def test_hello(pytestconfig): - pass + 'tests': { + 'test_hello.py': """ + def test_hello(pytestconfig): + pass """, }, 'tox.ini': ''' @@ -475,6 +488,7 @@ assert not result.ret assert "sdist-make" not in result.stdout.str() + def test_usedevelop(initproj, cmd): initproj("example123", filedefs={'tox.ini': """ [testenv] @@ -484,6 +498,7 @@ assert not result.ret assert "sdist-make" not in result.stdout.str() + def test_usedevelop_mixed(initproj, cmd): initproj("example123", filedefs={'tox.ini': """ [testenv:devenv] @@ -502,11 +517,13 @@ assert not result.ret assert "sdist-make" in result.stdout.str() + def test_test_usedevelop(cmd, initproj): initproj("example123-0.5", filedefs={ - 'tests': {'test_hello.py': """ - def test_hello(pytestconfig): - pass + 'tests': { + 'test_hello.py': """ + def test_hello(pytestconfig): + pass """, }, 'tox.ini': ''' @@ -568,6 +585,7 @@ result = cmd.run("tox") assert not result.ret + def test_notest(initproj, cmd): initproj("example123", filedefs={'tox.ini': """ # content of: tox.ini @@ -586,6 +604,7 @@ "*py26*reusing*", ]) + def test_PYC(initproj, cmd, monkeypatch): initproj("example123", filedefs={'tox.ini': ''}) monkeypatch.setenv("PYTHONDOWNWRITEBYTECODE", 1) @@ -595,6 +614,7 @@ "*create*", ]) + def test_env_VIRTUALENV_PYTHON(initproj, cmd, monkeypatch): initproj("example123", filedefs={'tox.ini': ''}) monkeypatch.setenv("VIRTUALENV_PYTHON", '/FOO') @@ -604,6 +624,7 @@ "*create*", ]) + def test_sdistonly(initproj, cmd): initproj("example123", filedefs={'tox.ini': """ """}) @@ -614,6 +635,7 @@ ]) assert "-mvirtualenv" not in result.stdout.str() + def test_separate_sdist_no_sdistfile(cmd, initproj): distshare = cmd.tmpdir.join("distshare") initproj(("pkg123-foo", "0.7"), filedefs={ @@ -629,6 +651,7 @@ sdistfile = l[0] assert 'pkg123-foo-0.7.zip' in str(sdistfile) + def test_separate_sdist(cmd, initproj): distshare = cmd.tmpdir.join("distshare") initproj("pkg123-0.7", filedefs={ @@ -663,6 +686,7 @@ sdist_path = session.sdist() assert sdist_path == p + def test_installpkg(tmpdir, newconfig): p = tmpdir.ensure("pkg123-1.0.zip") config = newconfig(["--installpkg=%s" % p], "") @@ -670,6 +694,7 @@ sdist_path = session.sdist() assert sdist_path == p + @pytest.mark.xfail("sys.platform == 'win32' and sys.version_info < (2,6)", reason="test needs better impl") def test_envsitepackagesdir(cmd, initproj): @@ -685,6 +710,7 @@ X:*tox*site-packages* """) + def verify_json_report_format(data, testenvs=True): assert data["reportversion"] == "1" assert data["toxversion"] == tox.__version__ @@ -700,4 +726,3 @@ assert isinstance(pyinfo["version_info"], list) assert pyinfo["version"] assert pyinfo["executable"] - diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tox.ini --- a/tox.ini +++ b/tox.ini @@ -20,8 +20,29 @@ [testenv:flakes] deps = pytest-flakes>=0.2 -commands = py.test --flakes -m flakes tox tests + pytest-pep8 + +commands = py.test -x --flakes --pep8 tox tests + +[testenv:dev] +# required to make looponfail reload on every source code change +usedevelop = True + +deps = + pytest-xdist>=1.11 +commands = {posargs:py.test -s -x -f -v} [pytest] -rsyncdirs=tests tox -addopts = -rsxXf +rsyncdirs = tests tox +addopts = -rsxX +# pytest-xdist plugin configuration +looponfailroots = tox tests +norecursedirs = .hg .tox + +# pytest-pep8 plugin configuration +pep8maxlinelength = 99 +# W503 - line break before binary operator +# E402 - module level import not at top of file +# E731 - do not assign a lambda expression, use a def +pep8ignore = + *.py W503 E402 E731 diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tox/__init__.py --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,10 +1,12 @@ # __version__ = '2.0.0.dev1' + class exception: class Error(Exception): def __str__(self): - return "%s: %s" %(self.__class__.__name__, self.args[0]) + return "%s: %s" % (self.__class__.__name__, self.args[0]) + class ConfigError(Error): """ error in tox configuration. """ class UnsupportedInterpreter(Error): diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tox/_cmdline.py --- a/tox/_cmdline.py +++ b/tox/_cmdline.py @@ -17,9 +17,11 @@ from tox.result import ResultLog from subprocess import STDOUT + def now(): return py.std.time.time() + def main(args=None): try: config = parseconfig(args, 'tox') @@ -28,6 +30,7 @@ except KeyboardInterrupt: raise SystemExit(2) + class Action(object): def __init__(self, session, venv, msg, args): self.venv = venv @@ -55,12 +58,10 @@ def setactivity(self, name, msg): self.activity = name - self.report.verbosity0("%s %s: %s" %(self.venvname, name, msg), - bold=True) + self.report.verbosity0("%s %s: %s" % (self.venvname, name, msg), bold=True) def info(self, name, msg): - self.report.verbosity1("%s %s: %s" %(self.venvname, name, msg), - bold=True) + self.report.verbosity1("%s %s: %s" % (self.venvname, name, msg), bold=True) def _initlogpath(self, actionid): if self.venv: @@ -83,8 +84,8 @@ resultjson = self.session.config.option.resultjson if resultjson or redirect: fout = self._initlogpath(self.id) - fout.write("actionid=%s\nmsg=%s\ncmdargs=%r\nenv=%s\n" %( - self.id, self.msg, args, env)) + fout.write("actionid=%s\nmsg=%s\ncmdargs=%r\nenv=%s\n" % ( + self.id, self.msg, args, env)) fout.flush() self.popen_outpath = outpath = py.path.local(fout.name) fin = outpath.open() @@ -154,9 +155,9 @@ if hasattr(self, "commandlog"): self.commandlog.add_command(popen.args, out, ret) raise tox.exception.InvocationError( - "%s (see %s)" %(invoked, outpath), ret) + "%s (see %s)" % (invoked, outpath), ret) else: - raise tox.exception.InvocationError("%r" %(invoked, )) + raise tox.exception.InvocationError("%r" % (invoked, )) if not out and outpath: out = outpath.read() if hasattr(self, "commandlog"): @@ -170,8 +171,8 @@ arg = cwd.bestrelpath(arg) newargs.append(str(arg)) - #subprocess does not always take kindly to .py scripts - #so adding the interpreter here. + # subprocess does not always take kindly to .py scripts + # so adding the interpreter here. if sys.platform == "win32": ext = os.path.splitext(str(newargs[0]))[1].lower() if ext == '.py' and self.venv: @@ -184,39 +185,37 @@ if env is None: env = os.environ.copy() return self.session.popen(args, shell=False, cwd=str(cwd), - universal_newlines=True, - stdout=stdout, stderr=stderr, env=env) - + universal_newlines=True, + stdout=stdout, stderr=stderr, env=env) class Reporter(object): actionchar = "-" + def __init__(self, session): self.tw = py.io.TerminalWriter() self.session = session self._reportedlines = [] - #self.cumulated_time = 0.0 + # self.cumulated_time = 0.0 def logpopen(self, popen, env): """ log information about the action.popen() created process. """ cmd = " ".join(map(str, popen.args)) if popen.outpath: - self.verbosity1(" %s$ %s >%s" %(popen.cwd, cmd, - popen.outpath, - )) + self.verbosity1(" %s$ %s >%s" % (popen.cwd, cmd, popen.outpath,)) else: - self.verbosity1(" %s$ %s " %(popen.cwd, cmd)) + self.verbosity1(" %s$ %s " % (popen.cwd, cmd)) def logaction_start(self, action): msg = action.msg + " " + " ".join(map(str, action.args)) - self.verbosity2("%s start: %s" %(action.venvname, msg), bold=True) + self.verbosity2("%s start: %s" % (action.venvname, msg), bold=True) assert not hasattr(action, "_starttime") action._starttime = now() def logaction_finish(self, action): duration = now() - action._starttime - #self.cumulated_time += duration - self.verbosity2("%s finish: %s after %.2f seconds" %( + # self.cumulated_time += duration + self.verbosity2("%s finish: %s after %.2f seconds" % ( action.venvname, action.msg, duration), bold=True) def startsummary(self): @@ -228,8 +227,7 @@ def using(self, msg): if self.session.config.option.verbosity >= 1: - self.logline("using %s" %(msg,), bold=True) - + self.logline("using %s" % (msg,), bold=True) def keyboard_interrupt(self): self.error("KEYBOARDINTERRUPT") @@ -275,7 +273,7 @@ if self.session.config.option.verbosity >= 2: self.logline("%s" % msg, **opts) - #def log(self, msg): + # def log(self, msg): # py.builtin.print_(msg, file=sys.stderr) @@ -288,13 +286,15 @@ self.report = Report(self) self.make_emptydir(config.logdir) config.logdir.ensure(dir=1) - #self.report.using("logdir %s" %(self.config.logdir,)) - self.report.using("tox.ini: %s" %(self.config.toxinipath,)) + # self.report.using("logdir %s" %(self.config.logdir,)) + self.report.using("tox.ini: %s" % (self.config.toxinipath,)) self._spec2pkg = {} self._name2venv = {} try: - self.venvlist = [self.getvenv(x) - for x in self.config.envlist] + self.venvlist = [ + self.getvenv(x) + for x in self.config.envlist + ] except LookupError: raise SystemExit(1) self._actions = [] @@ -321,15 +321,14 @@ return action def runcommand(self): - self.report.using("tox-%s from %s" %(tox.__version__, - tox.__file__)) + self.report.using("tox-%s from %s" % (tox.__version__, tox.__file__)) if self.config.minversion: minversion = NormalizedVersion(self.config.minversion) toxversion = NormalizedVersion(tox.__version__) if toxversion < minversion: self.report.error( - "tox version is %s, required is at least %s" %( - toxversion, minversion)) + "tox version is %s, required is at least %s" % ( + toxversion, minversion)) raise SystemExit(1) if self.config.option.showconfig: self.showconfig() @@ -342,7 +341,7 @@ for relpath in pathlist: src = srcdir.join(relpath) if not src.check(): - self.report.error("missing source file: %s" %(src,)) + self.report.error("missing source file: %s" % (src,)) raise SystemExit(1) target = destdir.join(relpath) target.dirpath().ensure(dir=1) @@ -358,7 +357,7 @@ self.make_emptydir(self.config.distdir) action.popen([sys.executable, setup, "sdist", "--formats=zip", "--dist-dir", self.config.distdir, ], - cwd=self.config.setupdir) + cwd=self.config.setupdir) try: return self.config.distdir.listdir()[0] except py.error.ENOENT: @@ -375,12 +374,11 @@ ) raise SystemExit(1) self.report.error( - 'No dist directory found. Please check setup.py, e.g with:\n'\ + 'No dist directory found. Please check setup.py, e.g with:\n' ' python setup.py sdist' - ) + ) raise SystemExit(1) - def make_emptydir(self, path): if path.check(): self.report.info(" removing %s" % path) @@ -446,25 +444,25 @@ :rtype: py.path.local """ if not self.config.option.sdistonly and (self.config.sdistsrc or - self.config.option.installpkg): + self.config.option.installpkg): sdist_path = self.config.option.installpkg if not sdist_path: sdist_path = self.config.sdistsrc sdist_path = self._resolve_pkg(sdist_path) self.report.info("using package %r, skipping 'sdist' activity " % - str(sdist_path)) + str(sdist_path)) else: try: sdist_path = self._makesdist() except tox.exception.InvocationError: v = sys.exc_info()[1] self.report.error("FAIL could not package project - v = %r" % - v) + v) return sdistfile = self.config.distshare.join(sdist_path.basename) if sdistfile != sdist_path: self.report.info("copying new sdistfile to %r" % - str(sdistfile)) + str(sdistfile)) try: sdistfile.dirpath().ensure(dir=1) except py.error.Error: @@ -513,24 +511,23 @@ for venv in self.venvlist: status = venv.status if isinstance(status, tox.exception.InterpreterNotFound): - msg = " %s: %s" %(venv.envconfig.envname, str(status)) + msg = " %s: %s" % (venv.envconfig.envname, str(status)) if self.config.option.skip_missing_interpreters: self.report.skip(msg) else: retcode = 1 self.report.error(msg) elif status == "platform mismatch": - msg = " %s: %s" %(venv.envconfig.envname, str(status)) + msg = " %s: %s" % (venv.envconfig.envname, str(status)) self.report.verbosity1(msg) elif status and status != "skipped tests": - msg = " %s: %s" %(venv.envconfig.envname, str(status)) + msg = " %s: %s" % (venv.envconfig.envname, str(status)) self.report.error(msg) retcode = 1 else: if not status: status = "commands succeeded" - self.report.good(" %s: %s" %(venv.envconfig.envname, - status)) + self.report.good(" %s: %s" % (venv.envconfig.envname, status)) if not retcode: self.report.good(" congratulations :)") @@ -586,7 +583,6 @@ versions.append("virtualenv-%s" % version.strip()) self.report.keyvalue("tool-versions:", " ".join(versions)) - def _resolve_pkg(self, pkgspec): try: return self._spec2pkg[pkgspec] @@ -614,7 +610,7 @@ items.append((ver, x)) else: self.report.warning("could not determine version of: %s" % - str(x)) + str(x)) items.sort() if not items: raise tox.exception.MissingDependency(pkgspec) @@ -624,6 +620,8 @@ _rex_getversion = py.std.re.compile("[\w_\-\+\.]+-(.*)(\.zip|\.tar.gz)") + + def getversion(basename): m = _rex_getversion.match(basename) if m is None: diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -45,7 +45,7 @@ if inipath.check(): break else: - feedback("toxini file %r not found" %(basename), sysexit=True) + feedback("toxini file %r not found" % (basename), sysexit=True) try: parseini(config, inipath) except tox.exception.InterpreterNotFound: @@ -54,98 +54,104 @@ py.builtin.print_("ERROR: " + str(exn)) return config + def feedback(msg, sysexit=False): py.builtin.print_("ERROR: " + msg, file=sys.stderr) if sysexit: raise SystemExit(1) + class VersionAction(argparse.Action): def __call__(self, argparser, *args, **kwargs): name = argparser.pkgname mod = __import__(name) version = mod.__version__ - py.builtin.print_("%s imported from %s" %(version, mod.__file__)) + py.builtin.print_("%s imported from %s" % (version, mod.__file__)) raise SystemExit(0) + class CountAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): if hasattr(namespace, self.dest): - setattr(namespace, self.dest, int(getattr(namespace, self.dest))+1) + setattr(namespace, self.dest, int(getattr(namespace, self.dest)) + 1) else: setattr(namespace, self.dest, 0) + def prepare_parse(pkgname): parser = argparse.ArgumentParser(description=__doc__,) - #formatter_class=argparse.ArgumentDefaultsHelpFormatter) + # formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.pkgname = pkgname parser.add_argument("--version", nargs=0, action=VersionAction, - dest="version", - help="report version information to stdout.") + dest="version", + help="report version information to stdout.") parser.add_argument("-v", nargs=0, action=CountAction, default=0, - dest="verbosity", - help="increase verbosity of reporting output.") + dest="verbosity", + help="increase verbosity of reporting output.") parser.add_argument("--showconfig", action="store_true", - help="show configuration information for all environments. ") + help="show configuration information for all environments. ") parser.add_argument("-l", "--listenvs", action="store_true", - dest="listenvs", help="show list of test environments") + dest="listenvs", help="show list of test environments") parser.add_argument("-c", action="store", default="tox.ini", - dest="configfile", - help="use the specified config file name.") + dest="configfile", + help="use the specified config file name.") parser.add_argument("-e", action="append", dest="env", - metavar="envlist", - help="work against specified environments (ALL selects all).") + metavar="envlist", + help="work against specified environments (ALL selects all).") parser.add_argument("--notest", action="store_true", dest="notest", - help="skip invoking test commands.") + help="skip invoking test commands.") parser.add_argument("--sdistonly", action="store_true", dest="sdistonly", - help="only perform the sdist packaging activity.") + help="only perform the sdist packaging activity.") parser.add_argument("--installpkg", action="store", default=None, - metavar="PATH", - help="use specified package for installation into venv, instead of " - "creating an sdist.") + metavar="PATH", + help="use specified package for installation into venv, instead of " + "creating an sdist.") parser.add_argument("--develop", action="store_true", dest="develop", - help="install package in the venv using 'setup.py develop' via " - "'pip -e .'") + help="install package in the venv using 'setup.py develop' via " + "'pip -e .'") parser.add_argument("--set-home", action="store_true", dest="sethome", - help="(experimental) force creating a new $HOME for each test " - "environment and create .pydistutils.cfg|pip.conf files " - "if index servers are specified with tox. ") + help="(experimental) force creating a new $HOME for each test " + "environment and create .pydistutils.cfg|pip.conf files " + "if index servers are specified with tox. ") parser.add_argument('-i', action="append", - dest="indexurl", metavar="URL", - help="set indexserver url (if URL is of form name=url set the " - "url for the 'name' indexserver, specifically)") + dest="indexurl", metavar="URL", + help="set indexserver url (if URL is of form name=url set the " + "url for the 'name' indexserver, specifically)") parser.add_argument("--pre", action="store_true", dest="pre", - help="install pre-releases and development versions of dependencies. " - "This will pass the --pre option to install_command (pip by default).") + help="install pre-releases and development versions of dependencies. " + "This will pass the --pre option to install_command " + "(pip by default).") parser.add_argument("-r", "--recreate", action="store_true", - dest="recreate", - help="force recreation of virtual environments") + dest="recreate", + help="force recreation of virtual environments") parser.add_argument("--result-json", action="store", - dest="resultjson", metavar="PATH", - help="write a json file with detailed information about " - "all commands and results involved. This will turn off " - "pass-through output from running test commands which is " - "instead captured into the json result file.") + dest="resultjson", metavar="PATH", + help="write a json file with detailed information about " + "all commands and results involved. This will turn off " + "pass-through output from running test commands which is " + "instead captured into the json result file.") # We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED. parser.add_argument("--hashseed", action="store", - metavar="SEED", default=None, - help="set PYTHONHASHSEED to SEED before running commands. " - "Defaults to a random integer in the range [1, 4294967295] " - "([1, 1024] on Windows). " - "Passing 'noset' suppresses this behavior.") + metavar="SEED", default=None, + help="set PYTHONHASHSEED to SEED before running commands. " + "Defaults to a random integer in the range [1, 4294967295] " + "([1, 1024] on Windows). " + "Passing 'noset' suppresses this behavior.") parser.add_argument("--force-dep", action="append", - metavar="REQ", default=None, - help="Forces a certain version of one of the dependencies " - "when configuring the virtual environment. REQ Examples " - "'pytest<2.7' or 'django>=1.6'.") + metavar="REQ", default=None, + help="Forces a certain version of one of the dependencies " + "when configuring the virtual environment. REQ Examples " + "'pytest<2.7' or 'django>=1.6'.") parser.add_argument("--sitepackages", action="store_true", - help="override sitepackages setting to True in all envs") + help="override sitepackages setting to True in all envs") parser.add_argument("--skip-missing-interpreters", action="store_true", - help="don't fail tests for missing interpreters") + help="don't fail tests for missing interpreters") parser.add_argument("args", nargs="*", - help="additional arguments available to command positional substitution") + help="additional arguments available to command positional substitution") return parser + class Config(object): def __init__(self): self.envconfigs = {} @@ -167,8 +173,9 @@ @property def envbindir(self): - if (sys.platform == "win32" and "jython" not in self.basepython - and "pypy" not in self.basepython): + if (sys.platform == "win32" + and "jython" not in self.basepython + and "pypy" not in self.basepython): return self.envdir.join("Scripts") else: return self.envdir.join("bin") @@ -185,8 +192,8 @@ def envsitepackagesdir(self): self.getsupportedinterpreter() # for throwing exceptions x = self.config.interpreters.get_sitepackagesdir( - info=self._basepython_info, - envdir=self.envdir) + info=self._basepython_info, + envdir=self.envdir) return x def getsupportedinterpreter(self): @@ -200,14 +207,14 @@ if not info.version_info: raise tox.exception.InvocationError( 'Failed to get version_info for %s: %s' % (info.name, info.err)) - if info.version_info < (2,6): + if info.version_info < (2, 6): raise tox.exception.UnsupportedInterpreter( "python2.5 is not supported anymore, sorry") return info.executable +testenvprefix = "testenv:" -testenvprefix = "testenv:" def get_homedir(): try: @@ -215,12 +222,14 @@ except Exception: return None + def make_hashseed(): max_seed = 4294967295 if sys.platform == 'win32': max_seed = 1024 return str(random.randint(1, max_seed)) + class parseini: def __init__(self, config, inipath): config.toxinipath = inipath @@ -287,8 +296,7 @@ config.indexserver[name] = IndexServerConfig(name, override) reader.addsubstitutions(toxworkdir=config.toxworkdir) - config.distdir = reader.getpath(toxsection, "distdir", - "{toxworkdir}/dist") + config.distdir = reader.getpath(toxsection, "distdir", "{toxworkdir}/dist") reader.addsubstitutions(distdir=config.distdir) config.distshare = reader.getpath(toxsection, "distshare", distshare_default) @@ -335,18 +343,18 @@ def _makeenvconfig(self, name, section, subs, config): vc = VenvConfig(config=config, envname=name) factors = set(name.split('-')) - reader = IniReader(self._cfg, fallbacksections=["testenv"], - factors=factors) + reader = IniReader(self._cfg, fallbacksections=["testenv"], factors=factors) reader.addsubstitutions(**subs) - vc.develop = not config.option.installpkg and \ - reader.getbool(section, "usedevelop", config.option.develop) + vc.develop = ( + not config.option.installpkg + and reader.getbool(section, "usedevelop", config.option.develop)) vc.envdir = reader.getpath(section, "envdir", "{toxworkdir}/%s" % name) vc.args_are_paths = reader.getbool(section, "args_are_paths", True) if reader.getdefault(section, "python", None): raise tox.exception.ConfigError( "'python=' key was renamed to 'basepython='") bp = next((default_factors[f] for f in factors if f in default_factors), - sys.executable) + sys.executable) vc.basepython = reader.getdefault(section, "basepython", bp) vc._basepython_info = config.interpreters.get_info(vc.basepython) reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname, @@ -410,8 +418,9 @@ break vc.platform = platform - vc.sitepackages = self.config.option.sitepackages or \ - reader.getbool(section, "sitepackages", False) + vc.sitepackages = ( + self.config.option.sitepackages + or reader.getbool(section, "sitepackages", False)) vc.downloadcache = None downloadcache = reader.getdefault(section, "downloadcache") @@ -424,10 +433,10 @@ section, "install_command", "pip install {opts} {packages}", - ) + ) if '{packages}' not in vc.install_command: raise tox.exception.ConfigError( - "'install_command' must contain '{packages}' substitution") + "'install_command' must contain '{packages}' substitution") vc.pip_pre = config.option.pre or reader.getbool( section, "pip_pre", False) @@ -488,10 +497,12 @@ env = [env] return mapcat(_expand_envstr, env) + def _split_factor_expr(expr): partial_envs = _expand_envstr(expr) return [set(e.split('-')) for e in partial_envs] + def _expand_envstr(envstr): # split by commas not in groups tokens = re.split(r'((?:\{[^}]+\})+)|,', envstr) @@ -505,9 +516,11 @@ return mapcat(expand, envlist) + def mapcat(f, seq): return list(itertools.chain.from_iterable(map(f, seq))) + class DepConfig: def __init__(self, name, indexserver=None): self.name = name @@ -710,7 +723,7 @@ x = self._replace(x) finally: assert self._subststack.pop() == (section, name) - #print "getdefault", section, name, "returned", repr(x) + # print "getdefault", section, name, "returned", repr(x) return x def _apply_factors(self, s): @@ -740,7 +753,7 @@ else: envkey = match_value - if not envkey in os.environ and default is None: + if envkey not in os.environ and default is None: raise tox.exception.ConfigError( "substitution env:%r: unkown environment variable %r" % (envkey, envkey)) @@ -750,11 +763,11 @@ def _substitute_from_other_section(self, key): if key.startswith("[") and "]" in key: i = key.find("]") - section, item = key[1:i], key[i+1:] + section, item = key[1:i], key[i + 1:] if section in self._cfg and item in self._cfg[section]: if (section, item) in self._subststack: - raise ValueError('%s already in %s' %( - (section, item), self._subststack)) + raise ValueError('%s already in %s' % ( + (section, item), self._subststack)) x = str(self._cfg[section][item]) self._subststack.append((section, item)) try: @@ -785,13 +798,14 @@ return '{%s}' % sub_value handlers = { - 'env' : self._replace_env, - None : self._replace_substitution, - } + 'env': self._replace_env, + None: self._replace_substitution, + } try: sub_type = g['sub_type'] except KeyError: - raise tox.exception.ConfigError("Malformed substitution; no substitution type provided") + raise tox.exception.ConfigError( + "Malformed substitution; no substitution type provided") try: handler = handlers[sub_type] @@ -808,6 +822,7 @@ def _parse_command(self, command): pass + class CommandParser(object): class State(object): @@ -824,11 +839,11 @@ def word_has_ended(): return ((cur_char in string.whitespace and ps.word and - ps.word[-1] not in string.whitespace) or - (cur_char == '{' and ps.depth == 0 and not ps.word.endswith('\\')) or - (ps.depth == 0 and ps.word and ps.word[-1] == '}') or - (cur_char not in string.whitespace and ps.word and - ps.word.strip() == '')) + ps.word[-1] not in string.whitespace) or + (cur_char == '{' and ps.depth == 0 and not ps.word.endswith('\\')) or + (ps.depth == 0 and ps.word and ps.word[-1] == '}') or + (cur_char not in string.whitespace and ps.word and + ps.word.strip() == '')) def yield_this_word(): yieldword = ps.word @@ -869,8 +884,8 @@ yield_this_word() return ps.yield_words + def getcontextname(): if any(env in os.environ for env in ['JENKINS_URL', 'HUDSON_URL']): return 'jenkins' return None - diff -r d3b25d3d9d603ac562030d12e18216f3b6268fd2 -r 37151bb27e530549ef2a17d3920f2bfea2421a87 tox/_exception.py --- a/tox/_exception.py +++ b/tox/_exception.py @@ -1,16 +1,28 @@ class Error(Exception): def __str__(self): - return "%s: %s" %(self.__class__.__name__, self.args[0]) + return "%s: %s" % (self.__class__.__name__, self.args[0]) + + class UnsupportedInterpreter(Error): "signals an unsupported Interpreter" + + class InterpreterNotFound(Error): "signals that an interpreter could not be found" + + class InvocationError(Error): """ an error while invoking a script. """ + + class MissingFile(Error): """ an error while invoking a script. """ + + class MissingDirectory(Error): """ a directory did not exist. """ + + class MissingDependency(Error): """ a dependency could not be found or determined. """ This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Apr 22 23:52:58 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 21:52:58 -0000 Subject: [Pytest-commit] commit/tox: hpk42: add changelog for pep8 changes Message-ID: <20150422215258.8684.37806@app07.ash-private.bitbucket.org> 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/9383b904df18/ Changeset: 9383b904df18 User: hpk42 Date: 2015-04-22 21:52:52+00:00 Summary: add changelog for pep8 changes Affected #: 1 file diff -r 37151bb27e530549ef2a17d3920f2bfea2421a87 -r 9383b904df1808de2e546000e49f8231c2f94f79 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ - fix issue235: fix AttributeError with --installpkg. Thanks Volodymyr Vitvitski. +- tox has now somewhat pep8 clean code, thanks to Volodymyr Vitvitski. 1.9.2 Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 23 01:52:19 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 23:52:19 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merged in hpk42/pytest-patches/reintroduce_pytest_fixture (pull request #279) Message-ID: <20150422235219.25840.58544@app13.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/6f7fb5f6a382/ Changeset: 6f7fb5f6a382 Branch: pytest-2.7 User: flub Date: 2015-04-22 23:52:13+00:00 Summary: Merged in hpk42/pytest-patches/reintroduce_pytest_fixture (pull request #279) reintroduced _pytest fixture of the pytester plugin Affected #: 2 files diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,8 @@ - fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing when tests raised SystemExit. Thanks Holger Krekel. +- reintroduced _pytest fixture of the pytester plugin which is used + at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -15,6 +15,24 @@ from _pytest.main import Session, EXIT_OK +# used at least by pytest-xdist plugin + at pytest.fixture +def _pytest(request): + """ Return a helper which offers a gethookrecorder(hook) + method which returns a HookRecorder instance which helps + to make assertions about called hooks. + """ + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + def get_public_names(l): """Only return names from iterator l without a leading underscore.""" Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 23 01:52:18 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 23:52:18 -0000 Subject: [Pytest-commit] commit/pytest: 3 new changesets Message-ID: <20150422235218.5330.73746@app13.ash-private.bitbucket.org> 3 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/79264aa5deef/ Changeset: 79264aa5deef Branch: reintroduce_pytest_fixture User: hpk42 Date: 2015-04-22 15:06:00+00:00 Summary: reintroduced _pytest fixture of the pytester plugin which is used at least by pytest-xdist. Affected #: 2 files diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r 79264aa5deef8260da334006f0866ff4a423f1cf CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,8 @@ - fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing when tests raised SystemExit. Thanks Holger Krekel. +- reintroduced _pytest fixture of the pytester plugin which is used + at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r 79264aa5deef8260da334006f0866ff4a423f1cf _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -15,6 +15,19 @@ from _pytest.main import Session, EXIT_OK +# used at least by pytest-xdist plugin +def pytest_funcarg___pytest(request): + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + def get_public_names(l): """Only return names from iterator l without a leading underscore.""" https://bitbucket.org/pytest-dev/pytest/commits/1cb853615ad8/ Changeset: 1cb853615ad8 Branch: reintroduce_pytest_fixture User: hpk42 Date: 2015-04-22 19:04:36+00:00 Summary: accomodate Floris' comments. (The reason was i just reinstanted the old code :) Affected #: 1 file diff -r 79264aa5deef8260da334006f0866ff4a423f1cf -r 1cb853615ad8b9dfba6ae6651b9323e660064b55 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -16,7 +16,12 @@ from _pytest.main import Session, EXIT_OK # used at least by pytest-xdist plugin -def pytest_funcarg___pytest(request): + at pytest.fixture +def _pytest(request): + """ Return a helper which offers a gethookrecorder(hook) + method which returns a HookRecorder instance which helps + to make assertions about called hooks. + """ return PytestArg(request) class PytestArg: https://bitbucket.org/pytest-dev/pytest/commits/6f7fb5f6a382/ Changeset: 6f7fb5f6a382 Branch: pytest-2.7 User: flub Date: 2015-04-22 23:52:13+00:00 Summary: Merged in hpk42/pytest-patches/reintroduce_pytest_fixture (pull request #279) reintroduced _pytest fixture of the pytester plugin Affected #: 2 files diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,8 @@ - fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing when tests raised SystemExit. Thanks Holger Krekel. +- reintroduced _pytest fixture of the pytester plugin which is used + at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -15,6 +15,24 @@ from _pytest.main import Session, EXIT_OK +# used at least by pytest-xdist plugin + at pytest.fixture +def _pytest(request): + """ Return a helper which offers a gethookrecorder(hook) + method which returns a HookRecorder instance which helps + to make assertions about called hooks. + """ + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + def get_public_names(l): """Only return names from iterator l without a leading underscore.""" Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 23 01:57:03 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 22 Apr 2015 23:57:03 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150422235703.6513.64135@app08.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/05b2ce3c000f/ Changeset: 05b2ce3c000f User: flub Date: 2015-04-22 23:55:58+00:00 Summary: Merge _pytest fixture reintroduction from pytest-2.7 branch This was accidentally removed while some plugins depend on it. Affected #: 2 files diff -r f54e97fa410507a24f2533ce62fc6d7cbb038247 -r 05b2ce3c000f0cb654e0c25009254b44658b0100 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,8 @@ - fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing when tests raised SystemExit. Thanks Holger Krekel. +- reintroduced _pytest fixture of the pytester plugin which is used + at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r f54e97fa410507a24f2533ce62fc6d7cbb038247 -r 05b2ce3c000f0cb654e0c25009254b44658b0100 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -15,6 +15,24 @@ from _pytest.main import Session, EXIT_OK +# used at least by pytest-xdist plugin + at pytest.fixture +def _pytest(request): + """ Return a helper which offers a gethookrecorder(hook) + method which returns a HookRecorder instance which helps + to make assertions about called hooks. + """ + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + def get_public_names(l): """Only return names from iterator l without a leading underscore.""" https://bitbucket.org/pytest-dev/pytest/commits/1ee222525a03/ Changeset: 1ee222525a03 Branch: reintroduce_pytest_fixture User: flub Date: 2015-04-22 23:56:25+00:00 Summary: Close merged bugfix branch Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Thu Apr 23 02:02:21 2015 From: builds at drone.io (Drone.io Build) Date: Thu, 23 Apr 2015 00:02:21 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 87 Message-ID: <20150423000221.30317.62546@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/87 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3966:6f7fb5f6a382 Author : Floris Bruynooghe Branch : pytest-2.7 Message: Merged in hpk42/pytest-patches/reintroduce_pytest_fixture (pull request #279) -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Thu Apr 23 12:03:19 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 23 Apr 2015 10:03:19 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in nicoddemus/pytest/cx_freeze_ubuntu (pull request #280) Message-ID: <20150423100319.12582.47139@app02.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/41aae470f1ad/ Changeset: 41aae470f1ad Branch: pytest-2.7 User: hpk42 Date: 2015-04-23 10:03:14+00:00 Summary: Merged in nicoddemus/pytest/cx_freeze_ubuntu (pull request #280) Fix py27-cxfreeze tox environment Affected #: 3 files diff -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 .hgignore --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,8 @@ doc/*/_build build/ dist/ +testing/cx_freeze/build +testing/cx_freeze/cx_freeze_source *.egg-info issue/ env/ diff -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 testing/cx_freeze/install_cx_freeze.py --- /dev/null +++ b/testing/cx_freeze/install_cx_freeze.py @@ -0,0 +1,66 @@ +""" +Installs cx_freeze from source, but first patching +setup.py as described here: + +http://stackoverflow.com/questions/25107697/compiling-cx-freeze-under-ubuntu +""" +import glob +import shutil +import tarfile +import os +import sys +import platform + +if __name__ == '__main__': + if 'ubuntu' not in platform.version().lower(): + + print('Not Ubuntu, installing using pip. (platform.version() is %r)' % + platform.version()) + res = os.system('pip install cx_freeze') + if res != 0: + sys.exit(res) + sys.exit(0) + + if os.path.isdir('cx_freeze_source'): + shutil.rmtree('cx_freeze_source') + os.mkdir('cx_freeze_source') + + res = os.system('pip install --download cx_freeze_source --no-use-wheel ' + 'cx_freeze') + if res != 0: + sys.exit(res) + + packages = glob.glob('cx_freeze_source/*.tar.gz') + assert len(packages) == 1 + tar_filename = packages[0] + + tar_file = tarfile.open(tar_filename) + try: + tar_file.extractall(path='cx_freeze_source') + finally: + tar_file.close() + + basename = os.path.basename(tar_filename).replace('.tar.gz', '') + setup_py_filename = 'cx_freeze_source/%s/setup.py' % basename + with open(setup_py_filename) as f: + lines = f.readlines() + + line_to_patch = 'if not vars.get("Py_ENABLE_SHARED", 0):' + for index, line in enumerate(lines): + if line_to_patch in line: + indent = line[:line.index(line_to_patch)] + lines[index] = indent + 'if True:\n' + print('Patched line %d' % (index + 1)) + break + else: + sys.exit('Could not find line in setup.py to patch!') + + with open(setup_py_filename, 'w') as f: + f.writelines(lines) + + os.chdir('cx_freeze_source/%s' % basename) + res = os.system('python setup.py install') + if res != 0: + sys.exit(res) + + sys.exit(0) \ No newline at end of file diff -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 tox.ini --- a/tox.ini +++ b/tox.ini @@ -125,10 +125,10 @@ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [] [testenv:py27-cxfreeze] -deps=cx_freeze changedir=testing/cx_freeze basepython=python2.7 commands= + {envpython} install_cx_freeze.py {envpython} runtests_setup.py build --build-exe build {envpython} tox_run.py Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 23 12:03:20 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 23 Apr 2015 10:03:20 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150423100320.6654.33573@app07.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/c0db30ab8e17/ Changeset: c0db30ab8e17 Branch: cx_freeze_ubuntu User: nicoddemus Date: 2015-04-22 22:46:06+00:00 Summary: Fix py27-cxfreeze tox environment Use a custom script to install a patched version of cx_freeze, as required in Ubuntu 14.04 systems Affected #: 3 files diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r c0db30ab8e17bee0e977cea1fe55f75a632aeaeb .hgignore --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,8 @@ doc/*/_build build/ dist/ +testing/cx_freeze/build +testing/cx_freeze/cx_freeze_source *.egg-info issue/ env/ diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r c0db30ab8e17bee0e977cea1fe55f75a632aeaeb testing/cx_freeze/install_cx_freeze.py --- /dev/null +++ b/testing/cx_freeze/install_cx_freeze.py @@ -0,0 +1,66 @@ +""" +Installs cx_freeze from source, but first patching +setup.py as described here: + +http://stackoverflow.com/questions/25107697/compiling-cx-freeze-under-ubuntu +""" +import glob +import shutil +import tarfile +import os +import sys +import platform + +if __name__ == '__main__': + if 'ubuntu' not in platform.version().lower(): + + print('Not Ubuntu, installing using pip. (platform.version() is %r)' % + platform.version()) + res = os.system('pip install cx_freeze') + if res != 0: + sys.exit(res) + sys.exit(0) + + if os.path.isdir('cx_freeze_source'): + shutil.rmtree('cx_freeze_source') + os.mkdir('cx_freeze_source') + + res = os.system('pip install --download cx_freeze_source --no-use-wheel ' + 'cx_freeze') + if res != 0: + sys.exit(res) + + packages = glob.glob('cx_freeze_source/*.tar.gz') + assert len(packages) == 1 + tar_filename = packages[0] + + tar_file = tarfile.open(tar_filename) + try: + tar_file.extractall(path='cx_freeze_source') + finally: + tar_file.close() + + basename = os.path.basename(tar_filename).replace('.tar.gz', '') + setup_py_filename = 'cx_freeze_source/%s/setup.py' % basename + with open(setup_py_filename) as f: + lines = f.readlines() + + line_to_patch = 'if not vars.get("Py_ENABLE_SHARED", 0):' + for index, line in enumerate(lines): + if line_to_patch in line: + indent = line[:line.index(line_to_patch)] + lines[index] = indent + 'if True:\n' + print('Patched line %d' % (index + 1)) + break + else: + sys.exit('Could not find line in setup.py to patch!') + + with open(setup_py_filename, 'w') as f: + f.writelines(lines) + + os.chdir('cx_freeze_source/%s' % basename) + res = os.system('python setup.py install') + if res != 0: + sys.exit(res) + + sys.exit(0) \ No newline at end of file diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r c0db30ab8e17bee0e977cea1fe55f75a632aeaeb tox.ini --- a/tox.ini +++ b/tox.ini @@ -125,10 +125,10 @@ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [] [testenv:py27-cxfreeze] -deps=cx_freeze changedir=testing/cx_freeze basepython=python2.7 commands= + {envpython} install_cx_freeze.py {envpython} runtests_setup.py build --build-exe build {envpython} tox_run.py https://bitbucket.org/pytest-dev/pytest/commits/41aae470f1ad/ Changeset: 41aae470f1ad Branch: pytest-2.7 User: hpk42 Date: 2015-04-23 10:03:14+00:00 Summary: Merged in nicoddemus/pytest/cx_freeze_ubuntu (pull request #280) Fix py27-cxfreeze tox environment Affected #: 3 files diff -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 .hgignore --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,8 @@ doc/*/_build build/ dist/ +testing/cx_freeze/build +testing/cx_freeze/cx_freeze_source *.egg-info issue/ env/ diff -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 testing/cx_freeze/install_cx_freeze.py --- /dev/null +++ b/testing/cx_freeze/install_cx_freeze.py @@ -0,0 +1,66 @@ +""" +Installs cx_freeze from source, but first patching +setup.py as described here: + +http://stackoverflow.com/questions/25107697/compiling-cx-freeze-under-ubuntu +""" +import glob +import shutil +import tarfile +import os +import sys +import platform + +if __name__ == '__main__': + if 'ubuntu' not in platform.version().lower(): + + print('Not Ubuntu, installing using pip. (platform.version() is %r)' % + platform.version()) + res = os.system('pip install cx_freeze') + if res != 0: + sys.exit(res) + sys.exit(0) + + if os.path.isdir('cx_freeze_source'): + shutil.rmtree('cx_freeze_source') + os.mkdir('cx_freeze_source') + + res = os.system('pip install --download cx_freeze_source --no-use-wheel ' + 'cx_freeze') + if res != 0: + sys.exit(res) + + packages = glob.glob('cx_freeze_source/*.tar.gz') + assert len(packages) == 1 + tar_filename = packages[0] + + tar_file = tarfile.open(tar_filename) + try: + tar_file.extractall(path='cx_freeze_source') + finally: + tar_file.close() + + basename = os.path.basename(tar_filename).replace('.tar.gz', '') + setup_py_filename = 'cx_freeze_source/%s/setup.py' % basename + with open(setup_py_filename) as f: + lines = f.readlines() + + line_to_patch = 'if not vars.get("Py_ENABLE_SHARED", 0):' + for index, line in enumerate(lines): + if line_to_patch in line: + indent = line[:line.index(line_to_patch)] + lines[index] = indent + 'if True:\n' + print('Patched line %d' % (index + 1)) + break + else: + sys.exit('Could not find line in setup.py to patch!') + + with open(setup_py_filename, 'w') as f: + f.writelines(lines) + + os.chdir('cx_freeze_source/%s' % basename) + res = os.system('python setup.py install') + if res != 0: + sys.exit(res) + + sys.exit(0) \ No newline at end of file diff -r 6f7fb5f6a3822d0736eb3c0ac405441c17705940 -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 tox.ini --- a/tox.ini +++ b/tox.ini @@ -125,10 +125,10 @@ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [] [testenv:py27-cxfreeze] -deps=cx_freeze changedir=testing/cx_freeze basepython=python2.7 commands= + {envpython} install_cx_freeze.py {envpython} runtests_setup.py build --build-exe build {envpython} tox_run.py Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 23 12:07:23 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 23 Apr 2015 10:07:23 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: merge cxfreeze fix Message-ID: <20150423100723.13999.50977@app11.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/34ec01b366b9/ Changeset: 34ec01b366b9 User: hpk42 Date: 2015-04-23 10:07:12+00:00 Summary: merge cxfreeze fix Affected #: 3 files diff -r 05b2ce3c000f0cb654e0c25009254b44658b0100 -r 34ec01b366b95afec4c4928b21e2020389a35bee .hgignore --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,8 @@ doc/*/_build build/ dist/ +testing/cx_freeze/build +testing/cx_freeze/cx_freeze_source *.egg-info issue/ env/ diff -r 05b2ce3c000f0cb654e0c25009254b44658b0100 -r 34ec01b366b95afec4c4928b21e2020389a35bee testing/cx_freeze/install_cx_freeze.py --- /dev/null +++ b/testing/cx_freeze/install_cx_freeze.py @@ -0,0 +1,66 @@ +""" +Installs cx_freeze from source, but first patching +setup.py as described here: + +http://stackoverflow.com/questions/25107697/compiling-cx-freeze-under-ubuntu +""" +import glob +import shutil +import tarfile +import os +import sys +import platform + +if __name__ == '__main__': + if 'ubuntu' not in platform.version().lower(): + + print('Not Ubuntu, installing using pip. (platform.version() is %r)' % + platform.version()) + res = os.system('pip install cx_freeze') + if res != 0: + sys.exit(res) + sys.exit(0) + + if os.path.isdir('cx_freeze_source'): + shutil.rmtree('cx_freeze_source') + os.mkdir('cx_freeze_source') + + res = os.system('pip install --download cx_freeze_source --no-use-wheel ' + 'cx_freeze') + if res != 0: + sys.exit(res) + + packages = glob.glob('cx_freeze_source/*.tar.gz') + assert len(packages) == 1 + tar_filename = packages[0] + + tar_file = tarfile.open(tar_filename) + try: + tar_file.extractall(path='cx_freeze_source') + finally: + tar_file.close() + + basename = os.path.basename(tar_filename).replace('.tar.gz', '') + setup_py_filename = 'cx_freeze_source/%s/setup.py' % basename + with open(setup_py_filename) as f: + lines = f.readlines() + + line_to_patch = 'if not vars.get("Py_ENABLE_SHARED", 0):' + for index, line in enumerate(lines): + if line_to_patch in line: + indent = line[:line.index(line_to_patch)] + lines[index] = indent + 'if True:\n' + print('Patched line %d' % (index + 1)) + break + else: + sys.exit('Could not find line in setup.py to patch!') + + with open(setup_py_filename, 'w') as f: + f.writelines(lines) + + os.chdir('cx_freeze_source/%s' % basename) + res = os.system('python setup.py install') + if res != 0: + sys.exit(res) + + sys.exit(0) \ No newline at end of file diff -r 05b2ce3c000f0cb654e0c25009254b44658b0100 -r 34ec01b366b95afec4c4928b21e2020389a35bee tox.ini --- a/tox.ini +++ b/tox.ini @@ -125,10 +125,10 @@ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [] [testenv:py27-cxfreeze] -deps=cx_freeze changedir=testing/cx_freeze basepython=python2.7 commands= + {envpython} install_cx_freeze.py {envpython} runtests_setup.py build --build-exe build {envpython} tox_run.py Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Thu Apr 23 12:13:06 2015 From: builds at drone.io (Drone.io Build) Date: Thu, 23 Apr 2015 10:13:06 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 90 Message-ID: <20150423101306.82002.68361@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/90 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3968:41aae470f1ad Author : holger krekel Branch : pytest-2.7 Message: Merged in nicoddemus/pytest/cx_freeze_ubuntu (pull request #280) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Thu Apr 23 12:14:18 2015 From: builds at drone.io (Drone.io Build) Date: Thu, 23 Apr 2015 10:14:18 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 91 Message-ID: <20150423101417.18901.64204@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/91 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4157:34ec01b366b9 Author : holger krekel Branch : default Message: merge cxfreeze fix -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Thu Apr 23 21:13:37 2015 From: issues-reply at bitbucket.org (Venkatesh-Prasad Ranganath) Date: Thu, 23 Apr 2015 19:13:37 -0000 Subject: [Pytest-commit] Issue #728: py.test fails with version conflict with python 2.7 (pytest-dev/pytest) Message-ID: <20150423191337.11501.1217@app06.ash-private.bitbucket.org> New issue 728: py.test fails with version conflict with python 2.7 https://bitbucket.org/pytest-dev/pytest/issue/728/pytest-fails-with-version-conflict-with Venkatesh-Prasad Ranganath: I am trying to build pytest with python 2.7 on Mac OSX and I encounter the following error when I execute "python2.7 setup.py test". Any idea what might be wrong? running test Traceback (most recent call last): File "pytest.py", line 10, in raise SystemExit(pytest.main()) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 32, in main config = _prepareconfig(args, plugins) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 85, in _prepareconfig pluginmanager=pluginmanager, args=args) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 521, in __call__ return self._docall(self.methods, kwargs) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 528, in _docall firstresult=self.firstresult).execute() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 393, in execute return wrapped_call(method(*args), self.execute) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 109, in wrapped_call wrap_controller.send(call_outcome) File "/Users/temp/pytest-2.7.0/_pytest/helpconfig.py", line 28, in pytest_cmdline_parse config = outcome.get_result() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 138, in get_result py.builtin._reraise(*ex) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 123, in __init__ self.result = func() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 394, in execute res = method(*args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 636, in pytest_cmdline_parse self.parse(args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 746, in parse self._preparse(args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 713, in _preparse self.pluginmanager.consider_setuptools_entrypoints() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 278, in consider_setuptools_entrypoints plugin = ep.load() File "/Library/Python/2.7/site-packages/pkg_resources.py", line 2169, in load self.require(env, installer) File "/Library/Python/2.7/site-packages/pkg_resources.py", line 2183, in require items = working_set.resolve(reqs, env, installer) File "/Library/Python/2.7/site-packages/pkg_resources.py", line 627, in resolve raise VersionConflict(dist, req) pkg_resources.VersionConflict: (pytest 2.7.0 (/Users/temp/pytest-2.7.0), Requirement.parse('pytest==2.5.2')) From issues-reply at bitbucket.org Thu Apr 23 21:16:34 2015 From: issues-reply at bitbucket.org (Venkatesh-Prasad Ranganath) Date: Thu, 23 Apr 2015 19:16:34 -0000 Subject: [Pytest-commit] Issue #729: py.test fails with version conflict with python 3.4 (pytest-dev/pytest) Message-ID: <20150423191634.32131.12742@app13.ash-private.bitbucket.org> New issue 729: py.test fails with version conflict with python 3.4 https://bitbucket.org/pytest-dev/pytest/issue/729/pytest-fails-with-version-conflict-with Venkatesh-Prasad Ranganath: I am trying to build pytest with python 3.4 on Mac OSX and I encounter the following error when I execute "python2.7 setup.py test". Any idea what might be wrong? ``` running test Traceback (most recent call last): File "pytest.py", line 10, in raise SystemExit(pytest.main()) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 32, in main config = _prepareconfig(args, plugins) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 85, in _prepareconfig pluginmanager=pluginmanager, args=args) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 521, in __call__ return self._docall(self.methods, kwargs) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 528, in _docall firstresult=self.firstresult).execute() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 393, in execute return wrapped_call(method(*args), self.execute) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 109, in wrapped_call wrap_controller.send(call_outcome) File "/Users/temp/pytest-2.7.0/_pytest/helpconfig.py", line 28, in pytest_cmdline_parse config = outcome.get_result() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 137, in get_result raise ex[1].with_traceback(ex[2]) File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 123, in __init__ self.result = func() File "/Users/temp/pytest-2.7.0/_pytest/core.py", line 394, in execute res = method(*args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 636, in pytest_cmdline_parse self.parse(args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 746, in parse self._preparse(args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 715, in _preparse self.known_args_namespace = ns = self._parser.parse_known_args(args) File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 198, in parse_known_args optparser = self._getparser() File "/Users/temp/pytest-2.7.0/_pytest/config.py", line 186, in _getparser arggroup.add_argument(*n, **a) File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/argparse.py", line 1349, in add_argument return self._add_action(action) File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/argparse.py", line 1553, in _add_action action = super(_ArgumentGroup, self)._add_action(action) File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/argparse.py", line 1363, in _add_action self._check_conflict(action) File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/argparse.py", line 1502, in _check_conflict conflict_handler(action, confl_optionals) File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/argparse.py", line 1511, in _handle_conflict_error raise ArgumentError(action, message % conflict_string) argparse.ArgumentError: argument --log-format: conflicting option string: --log-format ``` From issues-reply at bitbucket.org Fri Apr 24 13:05:26 2015 From: issues-reply at bitbucket.org (Ronny Pfannschmidt) Date: Fri, 24 Apr 2015 11:05:26 -0000 Subject: [Pytest-commit] Issue #730: deprecating --genscript (pytest-dev/pytest) Message-ID: <20150424110526.20801.2188@app03.ash-private.bitbucket.org> New issue 730: deprecating --genscript https://bitbucket.org/pytest-dev/pytest/issue/730/deprecating-genscript Ronny Pfannschmidt: the genscript method for distributing pytest seems to be used not that widely, doesnt really support plugins and googleing for it reveals a lot of issues people have meanwhile putting apps into zips became much better https://www.python.org/dev/peps/pep-0441/ From issues-reply at bitbucket.org Sat Apr 25 05:31:03 2015 From: issues-reply at bitbucket.org (Carl Meyer) Date: Sat, 25 Apr 2015 03:31:03 -0000 Subject: [Pytest-commit] Issue #731: Long reprs containing curly braces can break assertion rewriting. (pytest-dev/pytest) Message-ID: <20150425033103.8477.55460@app11.ash-private.bitbucket.org> New issue 731: Long reprs containing curly braces can break assertion rewriting. https://bitbucket.org/pytest-dev/pytest/issue/731/long-reprs-containing-curly-braces-can Carl Meyer: `pytest.assertion.util._collapse_false(explanation)` breaks if the input explanation text has unbalanced curly braces. This explanation text often contains object `repr()`s. `pytest.assertion.util.assertrepr_compare` uses `py.io.saferepr` to get the object `repr()`s and include them in the explanation. `py.io.saferepr` will elide overly long object `repr()`s with `...`. In doing so, it can sometimes elide an `}` but not the corresponding `{`, if a `repr()` contains curly braces in it. When this happens, it causes `_collapse_false` to fail with `AssertionError: unbalanced braces`. I think this could be fixed in `saferepr` by making it smart about curly braces. I can work on a patch for that, if it's deemed worth doing. From commits-noreply at bitbucket.org Sat Apr 25 05:48:25 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 25 Apr 2015 03:48:25 -0000 Subject: [Pytest-commit] commit/pytest: nicoddemus: Remove duplicated step in CONTRIBUTING Message-ID: <20150425034825.11526.28371@app12.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/dcad687f46dc/ Changeset: dcad687f46dc Branch: contributing-fix User: nicoddemus Date: 2015-04-25 03:48:11+00:00 Summary: Remove duplicated step in CONTRIBUTING Affected #: 1 file diff -r b89271e4512145c7a723a49ac760ee39240531f5 -r dcad687f46dca09cf413f31609b3508613e8be59 CONTRIBUTING.rst --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -136,12 +136,6 @@ fine to use ``pytest`` as your fork repository name because it will live under your user. -#. Create a development environment - (will implicitly use http://www.virtualenv.org/en/latest/):: - - $ make develop - $ source .env/bin/activate - #. Clone your fork locally using `Mercurial `_ (``hg``) and create a branch:: Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From issues-reply at bitbucket.org Sat Apr 25 07:36:21 2015 From: issues-reply at bitbucket.org (Gleb Dubovik) Date: Sat, 25 Apr 2015 05:36:21 -0000 Subject: [Pytest-commit] Issue #732: Unregistered plugin still receives hook calls (pytest-dev/pytest) Message-ID: <20150425053621.14474.56775@app09.ash-private.bitbucket.org> New issue 732: Unregistered plugin still receives hook calls https://bitbucket.org/pytest-dev/pytest/issue/732/unregistered-plugin-still-receives-hook Gleb Dubovik: When an instance of a class-based plugin is unregistered, it continues to receive hook calls from all items in the same file. This happens because some hooks are sent through Node.ihook which is an instance of FSHookProxy. When FSHookProxy is created (on the first call of the given hook), it copies original HookCaller from PluginManager through HookRelay. _getcaller() -> HookCaller.new_cached_caller(). This creates a copy of all methods in HookCaller and even though the instance of the caller in PluginManager is refreshed on plugin removal, the copy in FSHookProxy is not refreshed and retains references to removed plugin methods. Moreover, if new plugin is registered during test file execution (say, on the second test), it will not receive any hook calls. However, if new plugin exposes a hook that was never called before, that call will get through. Is it an intended behavior that the set of hooks is frozen within one fspath? From commits-noreply at bitbucket.org Sat Apr 25 09:08:35 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 25 Apr 2015 07:08:35 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in hpk42/pytest-patches/plugin_no_pytest (pull request #278) Message-ID: <20150425070835.21794.25584@app05.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/90f9b67b555f/ Changeset: 90f9b67b555f User: hpk42 Date: 2015-04-25 07:08:21+00:00 Summary: Merged in hpk42/pytest-patches/plugin_no_pytest (pull request #278) Refactor pluginmanagement Affected #: 20 files diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,15 @@ from ``inline_run()`` to allow temporary modules to be reloaded. Thanks Eduardo Schettino. +- internally refactor pluginmanager API and code so that there + is a clear distinction between a pytest-agnostic rather simple + pluginmanager and the PytestPluginManager which adds a lot of + behaviour, among it handling of the local conftest files. + In terms of documented methods this is a backward compatible + change but it might still break 3rd party plugins which relied on + details like especially the pluginmanager.add_shutdown() API. + Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/assertion/__init__.py --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -70,12 +70,11 @@ config._assertstate = AssertionState(config, mode) config._assertstate.hook = hook config._assertstate.trace("configured with mode set to %r" % (mode,)) - - -def pytest_unconfigure(config): - hook = config._assertstate.hook - if hook is not None and hook in sys.meta_path: - sys.meta_path.remove(hook) + def undo(): + hook = config._assertstate.hook + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + config.add_cleanup(undo) def pytest_collection(session): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -37,13 +37,13 @@ pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown - pluginmanager.add_shutdown(capman.reset_capturings) + early_config.add_cleanup(capman.reset_capturings) # make sure logging does not raise exceptions at the end def silence_logging_at_shutdown(): if "logging" in sys.modules: sys.modules["logging"].raiseExceptions = False - pluginmanager.add_shutdown(silence_logging_at_shutdown) + early_config.add_cleanup(silence_logging_at_shutdown) # finally trigger conftest loading but while capturing (issue93) capman.init_capturings() diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -53,6 +53,10 @@ "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml resultlog doctest").split() +builtin_plugins = set(default_plugins) +builtin_plugins.add("pytester") + + def _preloadplugins(): assert not _preinit _preinit.append(get_plugin_manager()) @@ -77,19 +81,31 @@ raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) pluginmanager = get_plugin_manager() - try: - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - except Exception: - pluginmanager.ensure_shutdown() - raise + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + +def exclude_pytest_names(name): + return not name.startswith(name) or name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + class PytestPluginManager(PluginManager): - def __init__(self, hookspecs=[hookspec]): - super(PytestPluginManager, self).__init__(hookspecs=hookspecs) + def __init__(self): + super(PytestPluginManager, self).__init__(prefix="pytest_", + excludefunc=exclude_pytest_names) + self._warnings = [] + self._plugin_distinfo = [] + self._globalplugins = [] + + # state related to local conftest plugins + self._path2confmods = {} + self._conftestpath2mod = {} + self._confcutdir = None + + self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): err = sys.stderr @@ -100,6 +116,25 @@ pass self.set_tracing(err.write) + def register(self, plugin, name=None, conftest=False): + ret = super(PytestPluginManager, self).register(plugin, name) + if ret and not conftest: + self._globalplugins.append(plugin) + return ret + + def _do_register(self, plugin, name): + # called from core PluginManager class + if hasattr(self, "config"): + self.config._register_plugin(plugin, name) + return super(PytestPluginManager, self)._do_register(plugin, name) + + def unregister(self, plugin): + super(PytestPluginManager, self).unregister(plugin) + try: + self._globalplugins.remove(plugin) + except ValueError: + pass + def pytest_configure(self, config): config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " @@ -110,6 +145,172 @@ for warning in self._warnings: config.warn(code="I1", message=warning) + # + # internal API for local conftest plugin handling + # + def _set_initial_conftests(self, namespace): + """ load initial conftest files given a preparsed "namespace". + As conftest files may add their own command line options + which have arguments ('--my-opt somepath') we might get some + false positives. All builtin and 3rd party plugins will have + been loaded, however, so common options will not confuse our logic + here. + """ + current = py.path.local() + self._confcutdir = current.join(namespace.confcutdir, abs=True) \ + if namespace.confcutdir else None + testpaths = namespace.file_or_dir + foundanchor = False + for path in testpaths: + path = str(path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = current.join(path, abs=1) + if exists(anchor): # we found some file object + self._try_load_conftest(anchor) + foundanchor = True + if not foundanchor: + self._try_load_conftest(current) + + def _try_load_conftest(self, anchor): + self._getconftestmodules(anchor) + # let's also consider test* subdirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): + self._getconftestmodules(x) + + def _getconftestmodules(self, path): + try: + return self._path2confmods[path] + except KeyError: + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.check(file=1): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._path2confmods[path] = clist + return clist + + def _rget_with_confmod(self, name, path): + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest(self, conftestpath): + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + try: + mod = conftestpath.pyimport() + except Exception: + raise ConftestImportFailure(conftestpath, sys.exc_info()) + self._conftestpath2mod[conftestpath] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._path2confmods: + for path, mods in self._path2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace("loaded conftestmodule %r" %(mod)) + self.consider_conftest(mod) + return mod + + # + # API for bootstrapping plugin loading + # + # + + def consider_setuptools_entrypoints(self): + try: + from pkg_resources import iter_entry_points, DistributionNotFound + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = ep.name + if name.startswith("pytest_"): + name = name[7:] + if ep.name in self._name2plugin or name in self._name2plugin: + continue + try: + plugin = ep.load() + except DistributionNotFound: + continue + self._plugin_distinfo.append((ep.dist, plugin)) + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.consider_pluginarg(opt2) + + def consider_pluginarg(self, arg): + if arg.startswith("no:"): + name = arg[3:] + plugin = self.getplugin(name) + if plugin is not None: + self.unregister(plugin) + self._name2plugin[name] = -1 + else: + if self.getplugin(arg) is None: + self.import_plugin(arg) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__, + conftest=True): + self.consider_module(conftestmodule) + + def consider_env(self): + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + + def consider_module(self, mod): + self._import_plugin_specs(getattr(mod, "pytest_plugins", None)) + + def _import_plugin_specs(self, spec): + if spec: + if isinstance(spec, str): + spec = spec.split(",") + for import_spec in spec: + self.import_plugin(import_spec) + + def import_plugin(self, modname): + # most often modname refers to builtin modules, e.g. "pytester", + # "terminal" or "capture". Those plugins are registered under their + # basename for historic purposes but must be imported with the + # _pytest prefix. + assert isinstance(modname, str) + if self.getplugin(modname) is not None: + return + if modname in builtin_plugins: + importspec = "_pytest." + modname + else: + importspec = modname + try: + __import__(importspec) + except ImportError: + raise + except Exception as e: + import pytest + if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): + raise + self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + mod = sys.modules[importspec] + self.register(mod, modname) + self.consider_module(mod) + class Parser: """ Parser for command line arguments and ini-file values. """ @@ -464,96 +665,6 @@ return action._formatted_action_invocation -class Conftest(object): - """ the single place for accessing values and interacting - towards conftest modules from pytest objects. - """ - def __init__(self, onimport=None): - self._path2confmods = {} - self._onimport = onimport - self._conftestpath2mod = {} - self._confcutdir = None - - def setinitial(self, namespace): - """ load initial conftest files given a preparsed "namespace". - As conftest files may add their own command line options - which have arguments ('--my-opt somepath') we might get some - false positives. All builtin and 3rd party plugins will have - been loaded, however, so common options will not confuse our logic - here. - """ - current = py.path.local() - self._confcutdir = current.join(namespace.confcutdir, abs=True) \ - if namespace.confcutdir else None - testpaths = namespace.file_or_dir - foundanchor = False - for path in testpaths: - path = str(path) - # remove node-id syntax - i = path.find("::") - if i != -1: - path = path[:i] - anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object - self._try_load_conftest(anchor) - foundanchor = True - if not foundanchor: - self._try_load_conftest(current) - - def _try_load_conftest(self, anchor): - self.getconftestmodules(anchor) - # let's also consider test* subdirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self.getconftestmodules(x) - - def getconftestmodules(self, path): - try: - return self._path2confmods[path] - except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self.importconftest(conftestpath) - clist.append(mod) - self._path2confmods[path] = clist - return clist - - def rget_with_confmod(self, name, path): - modules = self.getconftestmodules(path) - for mod in reversed(modules): - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def importconftest(self, conftestpath): - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - try: - mod = conftestpath.pyimport() - except Exception: - raise ConftestImportFailure(conftestpath, sys.exc_info()) - self._conftestpath2mod[conftestpath] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._path2confmods: - for path, mods in self._path2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - if self._onimport: - self._onimport(mod) - return mod - def _ensure_removed_sysmodule(modname): try: @@ -589,13 +700,11 @@ #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") - self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook self._inicache = {} self._opt2dest = {} self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") - self.pluginmanager.set_register_callback(self._register_plugin) self._configured = False def _register_plugin(self, plugin, name): @@ -612,16 +721,23 @@ if self._configured: call_plugin(plugin, "pytest_configure", {'config': self}) - def do_configure(self): + def add_cleanup(self, func): + """ Add a function to be called when the config object gets out of + use (usually coninciding with pytest_unconfigure).""" + self._cleanup.append(func) + + def _do_configure(self): assert not self._configured self._configured = True self.hook.pytest_configure(config=self) - def do_unconfigure(self): - assert self._configured - self._configured = False - self.hook.pytest_unconfigure(config=self) - self.pluginmanager.ensure_shutdown() + def _ensure_unconfigure(self): + if self._configured: + self._configured = False + self.hook.pytest_unconfigure(config=self) + while self._cleanup: + fin = self._cleanup.pop() + fin() def warn(self, code, message): """ generate a warning for this test session. """ @@ -636,11 +752,6 @@ self.parse(args) return self - def pytest_unconfigure(config): - while config._cleanup: - fin = config._cleanup.pop() - fin() - def notify_exception(self, excinfo, option=None): if option and option.fulltrace: style = "long" @@ -675,10 +786,6 @@ config.pluginmanager.consider_pluginarg(x) return config - def _onimportconftest(self, conftestmodule): - self.trace("loaded conftestmodule %r" %(conftestmodule,)) - self.pluginmanager.consider_conftest(conftestmodule) - def _processopt(self, opt): for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest @@ -688,11 +795,11 @@ setattr(self.option, opt.dest, opt.default) def _getmatchingplugins(self, fspath): - return self.pluginmanager._plugins + \ - self._conftest.getconftestmodules(fspath) + return self.pluginmanager._globalplugins + \ + self.pluginmanager._getconftestmodules(fspath) def pytest_load_initial_conftests(self, early_config): - self._conftest.setinitial(early_config.known_args_namespace) + self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) pytest_load_initial_conftests.trylast = True def _initini(self, args): @@ -799,7 +906,7 @@ def _getconftest_pathlist(self, name, path): try: - mod, relroots = self._conftest.rget_with_confmod(name, path) + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() @@ -933,3 +1040,4 @@ #if obj != pytest: # pytest.__all__.append(name) setattr(pytest, name, value) + diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,14 +1,9 @@ """ -pytest PluginManager, basic initialization and tracing. +PluginManager, basic initialization and tracing. """ -import os import sys import inspect import py -# don't import pytest to avoid circular imports - -assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) py3 = sys.version_info > (3,0) @@ -139,202 +134,133 @@ class PluginManager(object): - def __init__(self, hookspecs=None, prefix="pytest_"): + """ Core Pluginmanager class which manages registration + of plugin objects and 1:N hook calling. + + You can register new hooks by calling ``addhooks(module_or_class)``. + You can register plugin objects (which contain hooks) by calling + ``register(plugin)``. The Pluginmanager is initialized with a + prefix that is searched for in the names of the dict of registered + plugin objects. An optional excludefunc allows to blacklist names which + are not considered as hooks despite a matching prefix. + + For debugging purposes you can call ``set_tracing(writer)`` + which will subsequently send debug information to the specified + write function. + """ + + def __init__(self, prefix, excludefunc=None): + self._prefix = prefix + self._excludefunc = excludefunc self._name2plugin = {} self._plugins = [] - self._conftestplugins = [] self._plugin2hookcallers = {} - self._warnings = [] self.trace = TagTracer().get("pluginmanage") - self._plugin_distinfo = [] - self._shutdown = [] - self.hook = HookRelay(hookspecs or [], pm=self, prefix=prefix) + self.hook = HookRelay(pm=self) def set_tracing(self, writer): + """ turn on tracing to the given writer method and + return an undo function. """ self.trace.root.setwriter(writer) # reconfigure HookCalling to perform tracing assert not hasattr(self, "_wrapping") self._wrapping = True + hooktrace = self.hook.trace + def _docall(self, methods, kwargs): - trace = self.hookrelay.trace - trace.root.indent += 1 - trace(self.name, kwargs) + hooktrace.root.indent += 1 + hooktrace(self.name, kwargs) box = yield if box.excinfo is None: - trace("finish", self.name, "-->", box.result) - trace.root.indent -= 1 + hooktrace("finish", self.name, "-->", box.result) + hooktrace.root.indent -= 1 - undo = add_method_wrapper(HookCaller, _docall) - self.add_shutdown(undo) + return add_method_wrapper(HookCaller, _docall) - def do_configure(self, config): - # backward compatibility - config.do_configure() + def make_hook_caller(self, name, plugins): + caller = getattr(self.hook, name) + methods = self.listattr(name, plugins=plugins) + return HookCaller(caller.name, caller.firstresult, + argnames=caller.argnames, methods=methods) - def set_register_callback(self, callback): - assert not hasattr(self, "_registercallback") - self._registercallback = callback - - def register(self, plugin, name=None, prepend=False, conftest=False): + def register(self, plugin, name=None): + """ Register a plugin with the given name and ensure that all its + hook implementations are integrated. If the name is not specified + we use the ``__name__`` attribute of the plugin object or, if that + doesn't exist, the id of the plugin. This method will raise a + ValueError if the eventual name is already registered. """ + name = name or self._get_canonical_name(plugin) if self._name2plugin.get(name, None) == -1: return - name = name or getattr(plugin, '__name__', str(id(plugin))) - if self.isregistered(plugin, name): + if self.hasplugin(name): raise ValueError("Plugin already registered: %s=%s\n%s" %( name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) - reg = getattr(self, "_registercallback", None) - if reg is not None: - reg(plugin, name) # may call addhooks - hookcallers = list(self.hook._scan_plugin(plugin)) + # allow subclasses to intercept here by calling a helper + return self._do_register(plugin, name) + + def _do_register(self, plugin, name): + hookcallers = list(self._scan_plugin(plugin)) self._plugin2hookcallers[plugin] = hookcallers self._name2plugin[name] = plugin - if conftest: - self._conftestplugins.append(plugin) - else: - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - # finally make sure that the methods of the new plugin take part + self._plugins.append(plugin) + # rescan all methods for the hookcallers we found for hookcaller in hookcallers: - hookcaller.scan_methods() + self._scan_methods(hookcaller) return True def unregister(self, plugin): - try: - self._plugins.remove(plugin) - except KeyError: - self._conftestplugins.remove(plugin) + """ unregister the plugin object and all its contained hook implementations + from internal data structures. """ + self._plugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: - hookcaller.scan_methods() + self._scan_methods(hookcaller) - def add_shutdown(self, func): - self._shutdown.append(func) - - def ensure_shutdown(self): - while self._shutdown: - func = self._shutdown.pop() - func() - self._plugins = self._conftestplugins = [] - self._name2plugin.clear() - - def isregistered(self, plugin, name=None): - if self.getplugin(name) is not None: - return True - return plugin in self._plugins or plugin in self._conftestplugins - - def addhooks(self, spec, prefix="pytest_"): - self.hook._addhooks(spec, prefix=prefix) + def addhooks(self, module_or_class): + """ add new hook definitions from the given module_or_class using + the prefix/excludefunc with which the PluginManager was initialized. """ + isclass = int(inspect.isclass(module_or_class)) + names = [] + for name in dir(module_or_class): + if name.startswith(self._prefix): + method = module_or_class.__dict__[name] + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(name, firstresult=firstresult, + argnames=varnames(method, startindex=isclass)) + setattr(self.hook, name, hc) + names.append(name) + if not names: + raise ValueError("did not find new %r hooks in %r" + %(self._prefix, module_or_class)) def getplugins(self): - return self._plugins + self._conftestplugins + """ return the complete list of registered plugins. NOTE that + you will get the internal list and need to make a copy if you + modify the list.""" + return self._plugins - def skipifmissing(self, name): - if not self.hasplugin(name): - import pytest - pytest.skip("plugin %r is missing" % name) + def isregistered(self, plugin): + """ Return True if the plugin is already registered under its + canonical name. """ + return self.hasplugin(self._get_canonical_name(plugin)) or \ + plugin in self._plugins def hasplugin(self, name): - return bool(self.getplugin(name)) + """ Return True if there is a registered with the given name. """ + return name in self._name2plugin def getplugin(self, name): - if name is None: - return None - try: - return self._name2plugin[name] - except KeyError: - return self._name2plugin.get("_pytest." + name, None) - - # API for bootstrapping - # - def _envlist(self, varname): - val = os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) - - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.consider_pluginarg(opt2) - - def consider_pluginarg(self, arg): - if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 - else: - if self.getplugin(arg) is None: - self.import_plugin(arg) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): - self.consider_module(conftestmodule) - - def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) - - def import_plugin(self, modname): - assert isinstance(modname, str) - if self.getplugin(modname) is not None: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except ImportError: - if modname.startswith("pytest_"): - return self.import_plugin(modname[7:]) - raise - except: - e = sys.exc_info()[1] - import pytest - if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): - raise - self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) + """ Return a plugin or None for the given name. """ + return self._name2plugin.get(name) def listattr(self, attrname, plugins=None): if plugins is None: - plugins = self._plugins + self._conftestplugins + plugins = self._plugins l = [] last = [] wrappers = [] @@ -355,20 +281,43 @@ l.extend(wrappers) return l + def _scan_methods(self, hookcaller): + hookcaller.methods = self.listattr(hookcaller.name) + def call_plugin(self, plugin, methname, kwargs): return MultiCall(methods=self.listattr(methname, plugins=[plugin]), kwargs=kwargs, firstresult=True).execute() -def importplugin(importspec): - name = importspec - try: - mod = "_pytest." + name - __import__(mod) - return sys.modules[mod] - except ImportError: - __import__(importspec) - return sys.modules[importspec] + def _scan_plugin(self, plugin): + def fail(msg, *args): + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) + + for name in dir(plugin): + if name[0] == "_" or not name.startswith(self._prefix): + continue + hook = getattr(self.hook, name, None) + method = getattr(plugin, name) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + if getattr(method, 'optionalhook', False): + continue + fail("found unknown hook: %r", name) + for arg in varnames(method): + if arg not in hook.argnames: + fail("argument %r not available\n" + "actual definition: %s\n" + "available hookargs: %s", + arg, formatdef(method), + ", ".join(hook.argnames)) + yield hook + + def _get_canonical_name(self, plugin): + return getattr(plugin, "__name__", None) or str(id(plugin)) + + class MultiCall: """ execute a call into multiple python functions/methods. """ @@ -441,65 +390,13 @@ class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] + def __init__(self, pm): self._pm = pm self.trace = pm.trace.root.get("hook") - self.prefix = prefix - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspec, prefix): - added = False - isclass = int(inspect.isclass(hookspec)) - for name, method in vars(hookspec).items(): - if name.startswith(prefix): - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self, name, hc) - added = True - #print ("setting new hook", name) - if not added: - raise ValueError("did not find new %r hooks in %r" %( - prefix, hookspec,)) - - def _getcaller(self, name, plugins): - caller = getattr(self, name) - methods = self._pm.listattr(name, plugins=plugins) - if methods: - return caller.new_cached_caller(methods) - return caller - - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - - for name in dir(plugin): - if not name.startswith(self.prefix): - continue - hook = getattr(self, name, None) - method = getattr(plugin, name) - if hook is None: - is_optional = getattr(method, 'optionalhook', False) - if not isgenerichook(name) and not is_optional: - fail("found unknown hook: %r", name) - continue - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook class HookCaller: - def __init__(self, hookrelay, name, firstresult, argnames, methods=()): - self.hookrelay = hookrelay + def __init__(self, name, firstresult, argnames, methods=()): self.name = name self.firstresult = firstresult self.argnames = ["__multicall__"] @@ -507,16 +404,9 @@ assert "self" not in argnames # sanity check self.methods = methods - def new_cached_caller(self, methods): - return HookCaller(self.hookrelay, self.name, self.firstresult, - argnames=self.argnames, methods=methods) - def __repr__(self): return "" %(self.name,) - def scan_methods(self): - self.methods = self.hookrelay._pm.listattr(self.name) - def __call__(self, **kwargs): return self._docall(self.methods, kwargs) @@ -531,13 +421,9 @@ class PluginValidationError(Exception): """ plugin failed validation. """ -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") def formatdef(func): return "%s%s" % ( func.__name__, inspect.formatargspec(*inspect.getargspec(func)) ) - diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/doctest.py --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -132,7 +132,7 @@ def collect(self): import doctest if self.fspath.basename == "conftest.py": - module = self.config._conftest.importconftest(self.fspath) + module = self.config._conftest._importconftest(self.fspath) else: try: module = self.fspath.pyimport() diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -28,24 +28,20 @@ config = outcome.get_result() if config.option.debug: path = os.path.abspath("pytestdebug.log") - f = open(path, 'w') - config._debugfile = f - f.write("versions pytest-%s, py-%s, " + debugfile = open(path, 'w') + debugfile.write("versions pytest-%s, py-%s, " "python-%s\ncwd=%s\nargs=%s\n\n" %( pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(f.write) + config.pluginmanager.set_tracing(debugfile.write) sys.stderr.write("writing pytestdebug information to %s\n" % path) - - at pytest.mark.trylast -def pytest_unconfigure(config): - if hasattr(config, '_debugfile'): - config._debugfile.close() - sys.stderr.write("wrote pytestdebug information to %s\n" % - config._debugfile.name) - config.trace.root.setwriter(None) - + def unset_tracing(): + debugfile.close() + sys.stderr.write("wrote pytestdebug information to %s\n" % + debugfile.name) + config.trace.root.setwriter(None) + config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): if config.option.version: @@ -58,9 +54,9 @@ sys.stderr.write(line + "\n") return 0 elif config.option.help: - config.do_configure() + config._do_configure() showhelp(config) - config.do_unconfigure() + config._ensure_unconfigure() return 0 def showhelp(config): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -6,7 +6,7 @@ def pytest_addhooks(pluginmanager): """called at plugin load time to allow adding new hooks via a call to - pluginmanager.registerhooks(module).""" + pluginmanager.addhooks(module_or_class, prefix).""" def pytest_namespace(): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -77,7 +77,7 @@ initstate = 0 try: try: - config.do_configure() + config._do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) initstate = 2 @@ -107,9 +107,7 @@ config.hook.pytest_sessionfinish( session=session, exitstatus=session.exitstatus) - if initstate >= 1: - config.do_unconfigure() - config.pluginmanager.ensure_shutdown() + config._ensure_unconfigure() return session.exitstatus def pytest_cmdline_main(config): @@ -160,7 +158,7 @@ def __getattr__(self, name): plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.hook._getcaller(name, plugins) + x = self.config.pluginmanager.make_hook_caller(name, plugins) self.__dict__[name] = x return x @@ -510,7 +508,7 @@ def __init__(self, config): FSCollector.__init__(self, config.rootdir, parent=None, config=config, session=self) - self.config.pluginmanager.register(self, name="session", prepend=True) + self.config.pluginmanager.register(self, name="session") self._testsfailed = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") @@ -521,10 +519,12 @@ def _makeid(self): return "" + @pytest.mark.tryfirst def pytest_collectstart(self): if self.shouldstop: raise self.Interrupted(self.shouldstop) + @pytest.mark.tryfirst def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): self._testsfailed += 1 diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/mark.py --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -44,14 +44,14 @@ def pytest_cmdline_main(config): if config.option.markers: - config.do_configure() + config._do_configure() tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() - config.do_unconfigure() + config._ensure_unconfigure() return 0 pytest_cmdline_main.tryfirst = True diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -83,7 +83,8 @@ self.calls.append(ParsedCall(hookcaller.name, kwargs)) yield self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - pluginmanager.add_shutdown(self._undo_wrapping) + #if hasattr(pluginmanager, "config"): + # pluginmanager.add_shutdown(self._undo_wrapping) def finish_recording(self): self._undo_wrapping() @@ -589,12 +590,7 @@ # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) - def ensure_unconfigure(): - if hasattr(config.pluginmanager, "_config"): - config.pluginmanager.do_unconfigure(config) - config.pluginmanager.ensure_shutdown() - - self.request.addfinalizer(ensure_unconfigure) + self.request.addfinalizer(config._ensure_unconfigure) return config def parseconfigure(self, *args): @@ -606,8 +602,8 @@ """ config = self.parseconfig(*args) - config.do_configure() - self.request.addfinalizer(config.do_unconfigure) + config._do_configure() + self.request.addfinalizer(config._ensure_unconfigure) return config def getitem(self, source, funcname="test_func"): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/conftest.py --- a/testing/conftest.py +++ b/testing/conftest.py @@ -66,6 +66,7 @@ error.append(error[0]) raise AssertionError("\n".join(error)) + at pytest.mark.trylast def pytest_runtest_teardown(item, __multicall__): item.config._basedir.chdir() if hasattr(item.config, '_openfiles'): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1487,7 +1487,7 @@ reprec = testdir.inline_run("-v","-s") reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config - l = config._conftest.getconftestmodules(p)[0].l + l = config.pluginmanager._getconftestmodules(p)[0].l assert l == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 def test_scope_ordering(self, testdir): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/test_conftest.py --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,7 +1,6 @@ from textwrap import dedent import py, pytest -from _pytest.config import Conftest - +from _pytest.config import PytestPluginManager @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -16,7 +15,7 @@ return tmpdir def ConftestWithSetinitial(path): - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) return conftest @@ -25,51 +24,41 @@ def __init__(self): self.file_or_dir = args self.confcutdir = str(confcutdir) - conftest.setinitial(Namespace()) + conftest._set_initial_conftests(Namespace()) class TestConftestValueAccessGlobal: def test_basic_init(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() p = basedir.join("adir") - assert conftest.rget_with_confmod("a", p)[1] == 1 - - def test_onimport(self, basedir): - l = [] - conftest = Conftest(onimport=l.append) - adir = basedir.join("adir") - conftest_setinitial(conftest, [adir], confcutdir=basedir) - assert len(l) == 1 - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("b", adir.join("b"))[1] == 2 - assert len(l) == 2 + assert conftest._rget_with_confmod("a", p)[1] == 1 def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() len(conftest._path2confmods) - conftest.getconftestmodules(basedir) + conftest._getconftestmodules(basedir) snap1 = len(conftest._path2confmods) #assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('adir')) + conftest._getconftestmodules(basedir.join('adir')) assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('b')) + conftest._getconftestmodules(basedir.join('b')) assert len(conftest._path2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): - conftest.rget_with_confmod('a', basedir) + conftest._rget_with_confmod('a', basedir) def test_value_access_by_path(self, basedir): conftest = ConftestWithSetinitial(basedir) adir = basedir.join("adir") - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("a", adir.join("b"))[1] == 1.5 + assert conftest._rget_with_confmod("a", adir)[1] == 1 + assert conftest._rget_with_confmod("a", adir.join("b"))[1] == 1.5 def test_value_access_with_confmod(self, basedir): startdir = basedir.join("adir", "b") startdir.ensure("xx", dir=True) conftest = ConftestWithSetinitial(startdir) - mod, value = conftest.rget_with_confmod("a", startdir) + mod, value = conftest._rget_with_confmod("a", startdir) assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") @@ -85,9 +74,9 @@ def test_doubledash_considered(testdir): conf = testdir.mkdir("--option") conf.join("conftest.py").ensure() - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.basename, conf.basename]) - l = conftest.getconftestmodules(conf) + l = conftest._getconftestmodules(conf) assert len(l) == 1 def test_issue151_load_all_conftests(testdir): @@ -96,7 +85,7 @@ p = testdir.mkdir(name) p.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, names) d = list(conftest._conftestpath2mod.values()) assert len(d) == len(names) @@ -105,15 +94,15 @@ testdir.makeconftest("x=3") p = testdir.makepyfile(""" import py, pytest - from _pytest.config import Conftest - conf = Conftest() - mod = conf.importconftest(py.path.local("conftest.py")) + from _pytest.config import PytestPluginManager + conf = PytestPluginManager() + mod = conf._importconftest(py.path.local("conftest.py")) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) subconf = py.path.local().ensure("sub", "conftest.py") subconf.write("y=4") - mod2 = conf.importconftest(subconf) + mod2 = conf._importconftest(subconf) assert mod != mod2 assert mod2.y == 4 import conftest @@ -125,27 +114,27 @@ def test_conftestcutdir(testdir): conf = testdir.makeconftest("") p = testdir.mkdir("x") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p) - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 0 - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 0 assert conf not in conftest._conftestpath2mod # but we can still import a conftest directly - conftest.importconftest(conf) - l = conftest.getconftestmodules(conf.dirpath()) + conftest._importconftest(conf) + l = conftest._getconftestmodules(conf.dirpath()) assert l[0].__file__.startswith(str(conf)) # and all sub paths get updated properly - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) def test_conftestcutdir_inplace_considered(testdir): conf = testdir.makeconftest("") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) @@ -153,7 +142,7 @@ def test_setinitial_conftest_subdirs(testdir, name): sub = testdir.mkdir(name) subconftest = sub.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ('whatever', '.dotdir'): assert subconftest in conftest._conftestpath2mod @@ -199,9 +188,9 @@ ct2.write("") def impct(p): return p - conftest = Conftest() - monkeypatch.setattr(conftest, 'importconftest', impct) - assert conftest.getconftestmodules(sub) == [ct1, ct2] + conftest = PytestPluginManager() + monkeypatch.setattr(conftest, '_importconftest', impct) + assert conftest._getconftestmodules(sub) == [ct1, ct2] def test_fixture_dependency(testdir, monkeypatch): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -3,234 +3,48 @@ from _pytest.config import get_plugin_manager -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - pytest.raises(ImportError, lambda: pluginmanager.consider_env()) + at pytest.fixture +def pm(): + return PluginManager("he") - def test_preparse_args(self): - pluginmanager = PluginManager() - pytest.raises(ImportError, lambda: - pluginmanager.consider_preparse(["xyz", "-p", "hello123"])) + at pytest.fixture +def pytestpm(): + return PytestPluginManager() - def test_plugin_prevent_register(self): - pluginmanager = PluginManager() - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l1 = pluginmanager.getplugins() - pluginmanager.register(42, name="abc") - l2 = pluginmanager.getplugins() - assert len(l2) == len(l1) - def test_plugin_prevent_register_unregistered_alredy_registered(self): - pluginmanager = PluginManager() - pluginmanager.register(42, name="abc") - l1 = pluginmanager.getplugins() - assert 42 in l1 - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l2 = pluginmanager.getplugins() - assert 42 not in l2 +class TestPluginManager: + def test_plugin_double_register(self, pm): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="abc") - def test_plugin_double_register(self): - pm = PluginManager() - pm.register(42, name="abc") - pytest.raises(ValueError, lambda: pm.register(42, name="abc")) - - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(skipping1=""" - import pytest - pytest.skip("hello") - """) - p.copy(p.dirpath("skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "WI1*skipped plugin*skipping1*hello*", - "WI1*skipped plugin*skipping2*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(xy123="#") - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pluginmanager.getplugins()) - pluginmanager.consider_env() - l2 = len(pluginmanager.getplugins()) - assert l2 == l1 + 1 - assert pluginmanager.getplugin('xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = pytest.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "pytest_mytestplugin" - dist = None - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - plugin = pluginmanager.getplugin("mytestplugin") - assert plugin.x == 42 - - def test_consider_setuptools_not_installed(self, monkeypatch): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - # ok, we did not explode - - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import pytest - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') - assert plugin is not None - """) - monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) - - def test_import_plugin_importname(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("pytest_hello") - len1 = len(pluginmanager.getplugins()) - pluginmanager.import_plugin("pytest_hello") - len2 = len(pluginmanager.getplugins()) - assert len1 == len2 - plugin1 = pluginmanager.getplugin("pytest_hello") - assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("pytest_hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") - pluginname = "pkg.plug" - pluginmanager.import_plugin(pluginname) - mod = pluginmanager.getplugin("pkg.plug") - assert mod.x == 3 - - def test_consider_module(self, testdir): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_p1="#") - testdir.makepyfile(pytest_p2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_p1", "pytest_p2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("pytest_p1").__name__ == "pytest_p1" - assert pluginmanager.getplugin("pytest_p2").__name__ == "pytest_p2" - - def test_consider_module_import_module(self, testdir): - mod = py.std.types.ModuleType("x") - mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = get_plugin_manager() - reprec = testdir.make_hook_recorder(pluginmanager) - #syspath.prepend(aplugin.dirpath()) - py.std.sys.path.insert(0, str(aplugin.dirpath())) - pluginmanager.consider_module(mod) - call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__name__ == "pytest_a" - - # check that it is not registered twice - pluginmanager.consider_module(mod) - l = reprec.getcalls("pytest_plugin_registered") - assert len(l) == 1 - - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - - def test_consider_conftest_deps(self, testdir): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() - pp = PluginManager() - pytest.raises(ImportError, lambda: pp.consider_conftest(mod)) - - def test_pm(self): - pp = PluginManager() + def test_pm(self, pm): class A: pass a1, a2 = A(), A() - pp.register(a1) - assert pp.isregistered(a1) - pp.register(a2, "hello") - assert pp.isregistered(a2) - l = pp.getplugins() + pm.register(a1) + assert pm.isregistered(a1) + pm.register(a2, "hello") + assert pm.isregistered(a2) + l = pm.getplugins() assert a1 in l assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) - - def test_pm_ordering(self): - pp = PluginManager() - class A: pass - a1, a2 = A(), A() - pp.register(a1) - pp.register(a2, "hello") - l = pp.getplugins() - assert l.index(a1) < l.index(a2) - a3 = A() - pp.register(a3, prepend=True) - l = pp.getplugins() - assert l.index(a3) == 0 - - def test_register_imported_modules(self): - pp = PluginManager() - mod = py.std.types.ModuleType("x.y.pytest_hello") - pp.register(mod) - assert pp.isregistered(mod) - l = pp.getplugins() - assert mod in l - pytest.raises(ValueError, "pp.register(mod)") - pytest.raises(ValueError, lambda: pp.register(mod)) - #assert not pp.isregistered(mod2) - assert pp.getplugins() == l - - def test_canonical_import(self, monkeypatch): - mod = py.std.types.ModuleType("pytest_xyz") - monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) - pp = PluginManager() - pp.import_plugin('pytest_xyz') - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) + assert pm.getplugin('hello') == a2 + pm.unregister(a1) + assert not pm.isregistered(a1) def test_register_mismatch_method(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_gurgel(self): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register_mismatch_arg(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_configure(self, asd): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register(self): pm = get_plugin_manager() @@ -250,7 +64,7 @@ assert pm.getplugins()[-1:] == [my2] def test_listattr(self): - plugins = PluginManager() + plugins = PluginManager("xyz") class api1: x = 41 class api2: @@ -263,27 +77,6 @@ l = list(plugins.listattr('x')) assert l == [41, 42, 43] - def test_hook_tracing(self): - pm = get_plugin_manager() - saveindent = [] - class api1: - x = 41 - def pytest_plugin_registered(self, plugin): - saveindent.append(pm.trace.root.indent) - raise ValueError(42) - l = [] - pm.set_tracing(l.append) - indent = pm.trace.root.indent - p = api1() - pm.register(p) - - assert pm.trace.root.indent == indent - assert len(l) == 2 - assert 'pytest_plugin_registered' in l[0] - assert 'finish' in l[1] - pytest.raises(ValueError, lambda: pm.register(api1())) - assert pm.trace.root.indent == indent - assert saveindent[0] > indent class TestPytestPluginInteractions: @@ -301,7 +94,7 @@ return xyz + 1 """) config = get_plugin_manager().config - config._conftest.importconftest(conf) + config.pluginmanager._importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] @@ -350,7 +143,7 @@ parser.addoption('--test123', action="store_true", default=True) """) - config._conftest.importconftest(p) + config.pluginmanager._importconftest(p) assert config.option.test123 def test_configure(self, testdir): @@ -362,20 +155,43 @@ config.pluginmanager.register(A()) assert len(l) == 0 - config.do_configure() + config._do_configure() assert len(l) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] - config.do_unconfigure() + config._ensure_unconfigure() config.pluginmanager.register(A()) assert len(l) == 2 + def test_hook_tracing(self): + pytestpm = get_plugin_manager() # fully initialized with plugins + saveindent = [] + class api1: + x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pytestpm.trace.root.indent) + raise ValueError(42) + l = [] + pytestpm.set_tracing(l.append) + indent = pytestpm.trace.root.indent + p = api1() + pytestpm.register(p) + + assert pytestpm.trace.root.indent == indent + assert len(l) == 2 + assert 'pytest_plugin_registered' in l[0] + assert 'finish' in l[1] + with pytest.raises(ValueError): + pytestpm.register(api1()) + assert pytestpm.trace.root.indent == indent + assert saveindent[0] > indent + # lower level API def test_listattr(self): - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") class My2: x = 42 pluginmanager.register(My2()) @@ -395,7 +211,7 @@ def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -572,7 +388,7 @@ def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -624,11 +440,12 @@ class TestHookRelay: - def test_happypath(self): + def test_hapmypath(self): class Api: def hello(self, arg): "api hook 1" - pm = PluginManager([Api], prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) hook = pm.hook assert hasattr(hook, 'hello') assert repr(hook.hello).find("hello") != -1 @@ -647,7 +464,8 @@ class Api: def hello(self, arg): "api hook 1" - pm = PluginManager(Api, prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, argwrong): return arg + 1 @@ -656,19 +474,20 @@ assert "argwrong" in str(exc.value) def test_only_kwargs(self): - pm = PluginManager() + pm = PluginManager("he") class Api: def hello(self, arg): "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - pytest.raises(TypeError, lambda: mcm.hello(3)) + pm.addhooks(Api) + pytest.raises(TypeError, lambda: pm.hook.hello(3)) def test_firstresult_definition(self): class Api: def hello(self, arg): "api hook 1" hello.firstresult = True - pm = PluginManager([Api], "he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, arg): return arg + 1 @@ -771,15 +590,16 @@ "*trylast*last*", ]) -def test_importplugin_issue375(testdir): +def test_importplugin_issue375(testdir, pytestpm): testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe="import aaaa") - excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) + with pytest.raises(ImportError) as excinfo: + pytestpm.import_plugin("qwe") assert "qwe" not in str(excinfo.value) assert "aaaa" in str(excinfo.value) class TestWrapMethod: - def test_basic_happypath(self): + def test_basic_hapmypath(self): class A: def f(self): return "A.f" @@ -880,3 +700,178 @@ with pytest.raises(ValueError): A().error() assert l == [1] + + +### to be shifted to own test file +from _pytest.config import PytestPluginManager + +class TestPytestPluginManager: + def test_register_imported_modules(self): + pm = PytestPluginManager() + mod = py.std.types.ModuleType("x.y.pytest_hello") + pm.register(mod) + assert pm.isregistered(mod) + l = pm.getplugins() + assert mod in l + pytest.raises(ValueError, "pm.register(mod)") + pytest.raises(ValueError, lambda: pm.register(mod)) + #assert not pm.isregistered(mod2) + assert pm.getplugins() == l + + def test_canonical_import(self, monkeypatch): + mod = py.std.types.ModuleType("pytest_xyz") + monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) + pm = PytestPluginManager() + pm.import_plugin('pytest_xyz') + assert pm.getplugin('pytest_xyz') == mod + assert pm.isregistered(mod) + + def test_consider_module(self, testdir, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(pytest_p1="#") + testdir.makepyfile(pytest_p2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_p1", "pytest_p2"] + pytestpm.consider_module(mod) + assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1" + assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" + + def test_consider_module_import_module(self, testdir): + pytestpm = get_plugin_manager() + mod = py.std.types.ModuleType("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="#") + reprec = testdir.make_hook_recorder(pytestpm) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + pytestpm.consider_module(mod) + call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name) + assert call.plugin.__name__ == "pytest_a" + + # check that it is not registered twice + pytestpm.consider_module(mod) + l = reprec.getcalls("pytest_plugin_registered") + assert len(l) == 1 + + def test_consider_env_fails_to_import(self, monkeypatch, pytestpm): + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + with pytest.raises(ImportError): + pytestpm.consider_env() + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(skipping1=""" + import pytest + pytest.skip("hello") + """) + p.copy(p.dirpath("skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "WI1*skipped plugin*skipping1*hello*", + "WI1*skipped plugin*skipping2*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(xy123="#") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(pytestpm.getplugins()) + pytestpm.consider_env() + l2 = len(pytestpm.getplugins()) + assert l2 == l1 + 1 + assert pytestpm.getplugin('xy123') + pytestpm.consider_env() + l3 = len(pytestpm.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm): + pkg_resources = pytest.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "pytest_mytestplugin" + dist = None + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pytestpm.consider_setuptools_entrypoints() + plugin = pytestpm.getplugin("mytestplugin") + assert plugin.x == 42 + + def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + pytestpm.consider_setuptools_entrypoints() + # ok, we did not explode + + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): + testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import pytest + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') + assert plugin is not None + """) + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") + result = testdir.runpytest(p) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwx.y")') + + testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pytestpm.import_plugin("pytest_hello") + len1 = len(pytestpm.getplugins()) + pytestpm.import_plugin("pytest_hello") + len2 = len(pytestpm.getplugins()) + assert len1 == len2 + plugin1 = pytestpm.getplugin("pytest_hello") + assert plugin1.__name__.endswith('pytest_hello') + plugin2 = pytestpm.getplugin("pytest_hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwex.y")') + + testdir.syspathinsert() + testdir.mkpydir("pkg").join("plug.py").write("x=3") + pluginname = "pkg.plug" + pytestpm.import_plugin(pluginname) + mod = pytestpm.getplugin("pkg.plug") + assert mod.x == 3 + + def test_consider_conftest_deps(self, testdir, pytestpm): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + with pytest.raises(ImportError): + pytestpm.consider_conftest(mod) + + +class TestPytestPluginManagerBootstrapming: + def test_preparse_args(self, pytestpm): + pytest.raises(ImportError, lambda: + pytestpm.consider_preparse(["xyz", "-p", "hello123"])) + + def test_plugin_prevent_register(self, pytestpm): + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l1 = pytestpm.getplugins() + pytestpm.register(42, name="abc") + l2 = pytestpm.getplugins() + assert len(l2) == len(l1) + + def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): + pytestpm.register(42, name="abc") + l1 = pytestpm.getplugins() + assert 42 in l1 + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l2 = pytestpm.getplugins() + assert 42 not in l2 This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Sat Apr 25 09:09:45 2015 From: builds at drone.io (Drone.io Build) Date: Sat, 25 Apr 2015 07:09:45 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 93 Message-ID: <20150425070945.21349.87460@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/93 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4175:90f9b67b555f Author : holger krekel Branch : default Message: Merged in hpk42/pytest-patches/plugin_no_pytest (pull request #278) -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Sun Apr 26 01:05:55 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 25 Apr 2015 23:05:55 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Close branch contributing-fix Message-ID: <20150425230555.9783.73528@app12.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/2b1495c45107/ Changeset: 2b1495c45107 Branch: contributing-fix User: hpk42 Date: 2015-04-25 23:05:50+00:00 Summary: Close branch contributing-fix Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun Apr 26 01:05:55 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 25 Apr 2015 23:05:55 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in contributing-fix (pull request #281) Message-ID: <20150425230555.18675.13094@app08.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/1fa7f1414201/ Changeset: 1fa7f1414201 Branch: pytest-2.7 User: hpk42 Date: 2015-04-25 23:05:50+00:00 Summary: Merged in contributing-fix (pull request #281) Remove duplicated step in CONTRIBUTING Affected #: 1 file diff -r 41aae470f1ad196d7ed3fa53e9a7074b44b316c3 -r 1fa7f14142010fb83f00dd46dd4dbc5daaeb4910 CONTRIBUTING.rst --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -136,12 +136,6 @@ fine to use ``pytest`` as your fork repository name because it will live under your user. -#. Create a development environment - (will implicitly use http://www.virtualenv.org/en/latest/):: - - $ make develop - $ source .env/bin/activate - #. Clone your fork locally using `Mercurial `_ (``hg``) and create a branch:: Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun Apr 26 01:06:33 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 25 Apr 2015 23:06:33 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: merge contribution fix Message-ID: <20150425230633.16022.94321@app14.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/e08baadb3d0b/ Changeset: e08baadb3d0b User: hpk42 Date: 2015-04-25 23:06:17+00:00 Summary: merge contribution fix Affected #: 1 file diff -r 90f9b67b555f24f26798193206f58930f2ea1306 -r e08baadb3d0b309df91b1189131e79c87583ad5b CONTRIBUTING.rst --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -136,12 +136,6 @@ fine to use ``pytest`` as your fork repository name because it will live under your user. -#. Create a development environment - (will implicitly use http://www.virtualenv.org/en/latest/):: - - $ make develop - $ source .env/bin/activate - #. Clone your fork locally using `Mercurial `_ (``hg``) and create a branch:: Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Sun Apr 26 01:15:38 2015 From: builds at drone.io (Drone.io Build) Date: Sat, 25 Apr 2015 23:15:38 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 94 Message-ID: <20150425231538.129432.70587@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/94 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3971:1fa7f1414201 Author : holger krekel Branch : pytest-2.7 Message: Merged in contributing-fix (pull request #281) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Sun Apr 26 01:27:47 2015 From: builds at drone.io (Drone.io Build) Date: Sat, 25 Apr 2015 23:27:47 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 96 Message-ID: <20150425232747.4015.73068@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/96 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4178:e08baadb3d0b Author : holger krekel Branch : default Message: merge contribution fix -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Sat Apr 25 09:08:35 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 25 Apr 2015 07:08:35 -0000 Subject: [Pytest-commit] commit/pytest: 17 new changesets Message-ID: <20150425070835.18488.19429@app13.ash-private.bitbucket.org> 17 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/f5541d71ac9c/ Changeset: f5541d71ac9c Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 08:04:13+00:00 Summary: remove redundant py check as our setup.py excludes py <=1.4 already Affected #: 1 file diff -r 2361a9322d2fd01a0addeab80fc1bd9a15e60a08 -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -7,8 +7,6 @@ import py # don't import pytest to avoid circular imports -assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) py3 = sys.version_info > (3,0) https://bitbucket.org/pytest-dev/pytest/commits/21aefbeaf96f/ Changeset: 21aefbeaf96f Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 11:31:46+00:00 Summary: minimize HookRelay to become a pure container, refactor initialization and tests of plugin management to be a bit better split between pytest and pytest-independent bits Affected #: 7 files diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -87,9 +87,17 @@ pluginmanager.ensure_shutdown() raise +def exclude_pytest_names(name): + return not name.startswith(name) or name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + class PytestPluginManager(PluginManager): - def __init__(self, hookspecs=[hookspec]): - super(PytestPluginManager, self).__init__(hookspecs=hookspecs) + def __init__(self): + super(PytestPluginManager, self).__init__(prefix="pytest_", + excludefunc=exclude_pytest_names) + self._warnings = [] + self._plugin_distinfo = [] + self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): err = sys.stderr @@ -100,6 +108,14 @@ pass self.set_tracing(err.write) + def getplugin(self, name): + if name is None: + return name + plugin = super(PytestPluginManager, self).getplugin(name) + if plugin is None: + plugin = super(PytestPluginManager, self).getplugin("_pytest." + name) + return plugin + def pytest_configure(self, config): config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " @@ -110,6 +126,89 @@ for warning in self._warnings: config.warn(code="I1", message=warning) + # + # API for bootstrapping plugin loading + # + # + def _envlist(self, varname): + val = os.environ.get(varname, None) + if val is not None: + return val.split(',') + return () + + def consider_env(self): + for spec in self._envlist("PYTEST_PLUGINS"): + self.import_plugin(spec) + + def consider_setuptools_entrypoints(self): + try: + from pkg_resources import iter_entry_points, DistributionNotFound + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = ep.name + if name.startswith("pytest_"): + name = name[7:] + if ep.name in self._name2plugin or name in self._name2plugin: + continue + try: + plugin = ep.load() + except DistributionNotFound: + continue + self._plugin_distinfo.append((ep.dist, plugin)) + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.consider_pluginarg(opt2) + + def consider_pluginarg(self, arg): + if arg.startswith("no:"): + name = arg[3:] + plugin = self.getplugin(name) + if plugin is not None: + self.unregister(plugin) + self._name2plugin[name] = -1 + else: + if self.getplugin(arg) is None: + self.import_plugin(arg) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__, + conftest=True): + self.consider_module(conftestmodule) + + def consider_module(self, mod): + attr = getattr(mod, "pytest_plugins", ()) + if attr: + if not isinstance(attr, (list, tuple)): + attr = (attr,) + for spec in attr: + self.import_plugin(spec) + + def import_plugin(self, modname): + assert isinstance(modname, str) + if self.getplugin(modname) is not None: + return + try: + mod = importplugin(modname) + except KeyboardInterrupt: + raise + except ImportError: + if modname.startswith("pytest_"): + return self.import_plugin(modname[7:]) + raise + except: + e = sys.exc_info()[1] + import pytest + if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): + raise + self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + self.register(mod, modname) + self.consider_module(mod) + class Parser: """ Parser for command line arguments and ini-file values. """ @@ -933,3 +1032,15 @@ #if obj != pytest: # pytest.__all__.append(name) setattr(pytest, name, value) + + +def importplugin(importspec): + name = importspec + try: + mod = "_pytest." + name + __import__(mod) + return sys.modules[mod] + except ImportError: + __import__(importspec) + return sys.modules[importspec] + diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,12 +1,10 @@ """ -pytest PluginManager, basic initialization and tracing. +PluginManager, basic initialization and tracing. """ import os import sys import inspect import py -# don't import pytest to avoid circular imports - py3 = sys.version_info > (3,0) @@ -137,16 +135,16 @@ class PluginManager(object): - def __init__(self, hookspecs=None, prefix="pytest_"): + def __init__(self, prefix, excludefunc=None): + self._prefix = prefix + self._excludefunc = excludefunc self._name2plugin = {} self._plugins = [] self._conftestplugins = [] self._plugin2hookcallers = {} - self._warnings = [] self.trace = TagTracer().get("pluginmanage") - self._plugin_distinfo = [] self._shutdown = [] - self.hook = HookRelay(hookspecs or [], pm=self, prefix=prefix) + self.hook = HookRelay(pm=self) def set_tracing(self, writer): self.trace.root.setwriter(writer) @@ -174,6 +172,39 @@ assert not hasattr(self, "_registercallback") self._registercallback = callback + def make_hook_caller(self, name, plugins): + caller = getattr(self.hook, name) + methods = self.listattr(name, plugins=plugins) + if methods: + return HookCaller(self.hook, caller.name, caller.firstresult, + argnames=caller.argnames, methods=methods) + return caller + + def _scan_plugin(self, plugin): + def fail(msg, *args): + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) + + for name in dir(plugin): + if name[0] == "_" or not name.startswith(self._prefix): + continue + hook = getattr(self.hook, name, None) + method = getattr(plugin, name) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + if getattr(method, 'optionalhook', False): + continue + fail("found unknown hook: %r", name) + for arg in varnames(method): + if arg not in hook.argnames: + fail("argument %r not available\n" + "actual definition: %s\n" + "available hookargs: %s", + arg, formatdef(method), + ", ".join(hook.argnames)) + yield hook + def register(self, plugin, name=None, prepend=False, conftest=False): if self._name2plugin.get(name, None) == -1: return @@ -185,7 +216,7 @@ reg = getattr(self, "_registercallback", None) if reg is not None: reg(plugin, name) # may call addhooks - hookcallers = list(self.hook._scan_plugin(plugin)) + hookcallers = list(self._scan_plugin(plugin)) self._plugin2hookcallers[plugin] = hookcallers self._name2plugin[name] = plugin if conftest: @@ -227,108 +258,29 @@ return True return plugin in self._plugins or plugin in self._conftestplugins - def addhooks(self, spec, prefix="pytest_"): - self.hook._addhooks(spec, prefix=prefix) + def addhooks(self, module_or_class): + isclass = int(inspect.isclass(module_or_class)) + names = [] + for name in dir(module_or_class): + if name.startswith(self._prefix): + method = module_or_class.__dict__[name] + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self.hook, name, firstresult=firstresult, + argnames=varnames(method, startindex=isclass)) + setattr(self.hook, name, hc) + names.append(name) + if not names: + raise ValueError("did not find new %r hooks in %r" + %(self._prefix, module_or_class)) def getplugins(self): return self._plugins + self._conftestplugins - def skipifmissing(self, name): - if not self.hasplugin(name): - import pytest - pytest.skip("plugin %r is missing" % name) - def hasplugin(self, name): return bool(self.getplugin(name)) def getplugin(self, name): - if name is None: - return None - try: - return self._name2plugin[name] - except KeyError: - return self._name2plugin.get("_pytest." + name, None) - - # API for bootstrapping - # - def _envlist(self, varname): - val = os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) - - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.consider_pluginarg(opt2) - - def consider_pluginarg(self, arg): - if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 - else: - if self.getplugin(arg) is None: - self.import_plugin(arg) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): - self.consider_module(conftestmodule) - - def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) - - def import_plugin(self, modname): - assert isinstance(modname, str) - if self.getplugin(modname) is not None: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except ImportError: - if modname.startswith("pytest_"): - return self.import_plugin(modname[7:]) - raise - except: - e = sys.exc_info()[1] - import pytest - if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): - raise - self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) + return self._name2plugin.get(name) def listattr(self, attrname, plugins=None): if plugins is None: @@ -358,16 +310,6 @@ kwargs=kwargs, firstresult=True).execute() -def importplugin(importspec): - name = importspec - try: - mod = "_pytest." + name - __import__(mod) - return sys.modules[mod] - except ImportError: - __import__(importspec) - return sys.modules[importspec] - class MultiCall: """ execute a call into multiple python functions/methods. """ @@ -439,60 +381,9 @@ class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] + def __init__(self, pm): self._pm = pm self.trace = pm.trace.root.get("hook") - self.prefix = prefix - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspec, prefix): - added = False - isclass = int(inspect.isclass(hookspec)) - for name, method in vars(hookspec).items(): - if name.startswith(prefix): - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self, name, hc) - added = True - #print ("setting new hook", name) - if not added: - raise ValueError("did not find new %r hooks in %r" %( - prefix, hookspec,)) - - def _getcaller(self, name, plugins): - caller = getattr(self, name) - methods = self._pm.listattr(name, plugins=plugins) - if methods: - return caller.new_cached_caller(methods) - return caller - - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - - for name in dir(plugin): - if not name.startswith(self.prefix): - continue - hook = getattr(self, name, None) - method = getattr(plugin, name) - if hook is None: - is_optional = getattr(method, 'optionalhook', False) - if not isgenerichook(name) and not is_optional: - fail("found unknown hook: %r", name) - continue - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook class HookCaller: @@ -505,10 +396,6 @@ assert "self" not in argnames # sanity check self.methods = methods - def new_cached_caller(self, methods): - return HookCaller(self.hookrelay, self.name, self.firstresult, - argnames=self.argnames, methods=methods) - def __repr__(self): return "" %(self.name,) @@ -529,13 +416,9 @@ class PluginValidationError(Exception): """ plugin failed validation. """ -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") def formatdef(func): return "%s%s" % ( func.__name__, inspect.formatargspec(*inspect.getargspec(func)) ) - diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -6,7 +6,7 @@ def pytest_addhooks(pluginmanager): """called at plugin load time to allow adding new hooks via a call to - pluginmanager.registerhooks(module).""" + pluginmanager.addhooks(module_or_class, prefix).""" def pytest_namespace(): diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -160,7 +160,7 @@ def __getattr__(self, name): plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.hook._getcaller(name, plugins) + x = self.config.pluginmanager.make_hook_caller(name, plugins) self.__dict__[name] = x return x diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,236 +1,62 @@ import pytest, py, os from _pytest.core import * # noqa -from _pytest.config import get_plugin_manager +from _pytest.config import get_plugin_manager, importplugin -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - pytest.raises(ImportError, lambda: pluginmanager.consider_env()) + at pytest.fixture +def pm(): + return PluginManager("he") - def test_preparse_args(self): - pluginmanager = PluginManager() - pytest.raises(ImportError, lambda: - pluginmanager.consider_preparse(["xyz", "-p", "hello123"])) + at pytest.fixture +def pytestpm(): + return PytestPluginManager() - def test_plugin_prevent_register(self): - pluginmanager = PluginManager() - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l1 = pluginmanager.getplugins() - pluginmanager.register(42, name="abc") - l2 = pluginmanager.getplugins() - assert len(l2) == len(l1) - def test_plugin_prevent_register_unregistered_alredy_registered(self): - pluginmanager = PluginManager() - pluginmanager.register(42, name="abc") - l1 = pluginmanager.getplugins() - assert 42 in l1 - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l2 = pluginmanager.getplugins() - assert 42 not in l2 +class TestPluginManager: + def test_plugin_double_register(self, pm): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="abc") - def test_plugin_double_register(self): - pm = PluginManager() - pm.register(42, name="abc") - pytest.raises(ValueError, lambda: pm.register(42, name="abc")) - - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(skipping1=""" - import pytest - pytest.skip("hello") - """) - p.copy(p.dirpath("skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "WI1*skipped plugin*skipping1*hello*", - "WI1*skipped plugin*skipping2*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(xy123="#") - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pluginmanager.getplugins()) - pluginmanager.consider_env() - l2 = len(pluginmanager.getplugins()) - assert l2 == l1 + 1 - assert pluginmanager.getplugin('xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = pytest.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "pytest_mytestplugin" - dist = None - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - plugin = pluginmanager.getplugin("mytestplugin") - assert plugin.x == 42 - - def test_consider_setuptools_not_installed(self, monkeypatch): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - # ok, we did not explode - - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import pytest - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') - assert plugin is not None - """) - monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) - - def test_import_plugin_importname(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("pytest_hello") - len1 = len(pluginmanager.getplugins()) - pluginmanager.import_plugin("pytest_hello") - len2 = len(pluginmanager.getplugins()) - assert len1 == len2 - plugin1 = pluginmanager.getplugin("pytest_hello") - assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("pytest_hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") - pluginname = "pkg.plug" - pluginmanager.import_plugin(pluginname) - mod = pluginmanager.getplugin("pkg.plug") - assert mod.x == 3 - - def test_consider_module(self, testdir): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_p1="#") - testdir.makepyfile(pytest_p2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_p1", "pytest_p2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("pytest_p1").__name__ == "pytest_p1" - assert pluginmanager.getplugin("pytest_p2").__name__ == "pytest_p2" - - def test_consider_module_import_module(self, testdir): - mod = py.std.types.ModuleType("x") - mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = get_plugin_manager() - reprec = testdir.make_hook_recorder(pluginmanager) - #syspath.prepend(aplugin.dirpath()) - py.std.sys.path.insert(0, str(aplugin.dirpath())) - pluginmanager.consider_module(mod) - call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__name__ == "pytest_a" - - # check that it is not registered twice - pluginmanager.consider_module(mod) - l = reprec.getcalls("pytest_plugin_registered") - assert len(l) == 1 - - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - - def test_consider_conftest_deps(self, testdir): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() - pp = PluginManager() - pytest.raises(ImportError, lambda: pp.consider_conftest(mod)) - - def test_pm(self): - pp = PluginManager() + def test_pm(self, pm): class A: pass a1, a2 = A(), A() - pp.register(a1) - assert pp.isregistered(a1) - pp.register(a2, "hello") - assert pp.isregistered(a2) - l = pp.getplugins() + pm.register(a1) + assert pm.isregistered(a1) + pm.register(a2, "hello") + assert pm.isregistered(a2) + l = pm.getplugins() assert a1 in l assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) + assert pm.getplugin('hello') == a2 + pm.unregister(a1) + assert not pm.isregistered(a1) - def test_pm_ordering(self): - pp = PluginManager() + def test_pm_ordering(self, pm): class A: pass a1, a2 = A(), A() - pp.register(a1) - pp.register(a2, "hello") - l = pp.getplugins() + pm.register(a1) + pm.register(a2, "hello") + l = pm.getplugins() assert l.index(a1) < l.index(a2) a3 = A() - pp.register(a3, prepend=True) - l = pp.getplugins() + pm.register(a3, prepend=True) + l = pm.getplugins() assert l.index(a3) == 0 - def test_register_imported_modules(self): - pp = PluginManager() - mod = py.std.types.ModuleType("x.y.pytest_hello") - pp.register(mod) - assert pp.isregistered(mod) - l = pp.getplugins() - assert mod in l - pytest.raises(ValueError, "pp.register(mod)") - pytest.raises(ValueError, lambda: pp.register(mod)) - #assert not pp.isregistered(mod2) - assert pp.getplugins() == l - - def test_canonical_import(self, monkeypatch): - mod = py.std.types.ModuleType("pytest_xyz") - monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) - pp = PluginManager() - pp.import_plugin('pytest_xyz') - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) - def test_register_mismatch_method(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_gurgel(self): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register_mismatch_arg(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_configure(self, asd): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register(self): pm = get_plugin_manager() @@ -250,7 +76,7 @@ assert pm.getplugins()[-1:] == [my2] def test_listattr(self): - plugins = PluginManager() + plugins = PluginManager("xyz") class api1: x = 41 class api2: @@ -263,27 +89,6 @@ l = list(plugins.listattr('x')) assert l == [41, 42, 43] - def test_hook_tracing(self): - pm = get_plugin_manager() - saveindent = [] - class api1: - x = 41 - def pytest_plugin_registered(self, plugin): - saveindent.append(pm.trace.root.indent) - raise ValueError(42) - l = [] - pm.set_tracing(l.append) - indent = pm.trace.root.indent - p = api1() - pm.register(p) - - assert pm.trace.root.indent == indent - assert len(l) == 2 - assert 'pytest_plugin_registered' in l[0] - assert 'finish' in l[1] - pytest.raises(ValueError, lambda: pm.register(api1())) - assert pm.trace.root.indent == indent - assert saveindent[0] > indent class TestPytestPluginInteractions: @@ -372,10 +177,33 @@ config.pluginmanager.register(A()) assert len(l) == 2 + def test_hook_tracing(self): + pytestpm = get_plugin_manager() # fully initialized with plugins + saveindent = [] + class api1: + x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pytestpm.trace.root.indent) + raise ValueError(42) + l = [] + pytestpm.set_tracing(l.append) + indent = pytestpm.trace.root.indent + p = api1() + pytestpm.register(p) + + assert pytestpm.trace.root.indent == indent + assert len(l) == 2 + assert 'pytest_plugin_registered' in l[0] + assert 'finish' in l[1] + with pytest.raises(ValueError): + pytestpm.register(api1()) + assert pytestpm.trace.root.indent == indent + assert saveindent[0] > indent + # lower level API def test_listattr(self): - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") class My2: x = 42 pluginmanager.register(My2()) @@ -395,7 +223,7 @@ def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -572,7 +400,7 @@ def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -624,11 +452,12 @@ class TestHookRelay: - def test_happypath(self): + def test_hapmypath(self): class Api: def hello(self, arg): "api hook 1" - pm = PluginManager([Api], prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) hook = pm.hook assert hasattr(hook, 'hello') assert repr(hook.hello).find("hello") != -1 @@ -647,7 +476,8 @@ class Api: def hello(self, arg): "api hook 1" - pm = PluginManager(Api, prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, argwrong): return arg + 1 @@ -656,19 +486,20 @@ assert "argwrong" in str(exc.value) def test_only_kwargs(self): - pm = PluginManager() + pm = PluginManager("he") class Api: def hello(self, arg): "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - pytest.raises(TypeError, lambda: mcm.hello(3)) + pm.addhooks(Api) + pytest.raises(TypeError, lambda: pm.hook.hello(3)) def test_firstresult_definition(self): class Api: def hello(self, arg): "api hook 1" hello.firstresult = True - pm = PluginManager([Api], "he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, arg): return arg + 1 @@ -779,7 +610,7 @@ assert "aaaa" in str(excinfo.value) class TestWrapMethod: - def test_basic_happypath(self): + def test_basic_hapmypath(self): class A: def f(self): return "A.f" @@ -880,3 +711,182 @@ with pytest.raises(ValueError): A().error() assert l == [1] + + +### to be shifted to own test file +from _pytest.config import PytestPluginManager + +class TestPytestPluginManager: + def test_register_imported_modules(self): + pm = PytestPluginManager() + mod = py.std.types.ModuleType("x.y.pytest_hello") + pm.register(mod) + assert pm.isregistered(mod) + l = pm.getplugins() + assert mod in l + pytest.raises(ValueError, "pm.register(mod)") + pytest.raises(ValueError, lambda: pm.register(mod)) + #assert not pm.isregistered(mod2) + assert pm.getplugins() == l + + def test_canonical_import(self, monkeypatch): + mod = py.std.types.ModuleType("pytest_xyz") + monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) + pm = PytestPluginManager() + pm.import_plugin('pytest_xyz') + assert pm.getplugin('pytest_xyz') == mod + assert pm.isregistered(mod) + + def test_consider_module(self, testdir, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(pytest_p1="#") + testdir.makepyfile(pytest_p2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_p1", "pytest_p2"] + pytestpm.consider_module(mod) + assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1" + assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" + + def test_consider_module_import_module(self, testdir): + pytestpm = get_plugin_manager() + mod = py.std.types.ModuleType("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="#") + reprec = testdir.make_hook_recorder(pytestpm) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + pytestpm.consider_module(mod) + call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name) + assert call.plugin.__name__ == "pytest_a" + + # check that it is not registered twice + pytestpm.consider_module(mod) + l = reprec.getcalls("pytest_plugin_registered") + assert len(l) == 1 + + def test_consider_env_fails_to_import(self, monkeypatch, pytestpm): + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + with pytest.raises(ImportError): + pytestpm.consider_env() + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(skipping1=""" + import pytest + pytest.skip("hello") + """) + p.copy(p.dirpath("skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "WI1*skipped plugin*skipping1*hello*", + "WI1*skipped plugin*skipping2*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(xy123="#") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(pytestpm.getplugins()) + pytestpm.consider_env() + l2 = len(pytestpm.getplugins()) + assert l2 == l1 + 1 + assert pytestpm.getplugin('xy123') + pytestpm.consider_env() + l3 = len(pytestpm.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm): + pkg_resources = pytest.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "pytest_mytestplugin" + dist = None + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pytestpm.consider_setuptools_entrypoints() + plugin = pytestpm.getplugin("mytestplugin") + assert plugin.x == 42 + + def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + pytestpm.consider_setuptools_entrypoints() + # ok, we did not explode + + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): + testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import pytest + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') + assert plugin is not None + """) + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") + result = testdir.runpytest(p) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwx.y")') + + testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pytestpm.import_plugin("pytest_hello") + len1 = len(pytestpm.getplugins()) + pytestpm.import_plugin("pytest_hello") + len2 = len(pytestpm.getplugins()) + assert len1 == len2 + plugin1 = pytestpm.getplugin("pytest_hello") + assert plugin1.__name__.endswith('pytest_hello') + plugin2 = pytestpm.getplugin("pytest_hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwex.y")') + + testdir.syspathinsert() + testdir.mkpydir("pkg").join("plug.py").write("x=3") + pluginname = "pkg.plug" + pytestpm.import_plugin(pluginname) + mod = pytestpm.getplugin("pkg.plug") + assert mod.x == 3 + + def test_config_sets_conftesthandle_onimport(self, testdir): + config = testdir.parseconfig([]) + assert config._conftest._onimport == config._onimportconftest + + def test_consider_conftest_deps(self, testdir, pytestpm): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + with pytest.raises(ImportError): + pytestpm.consider_conftest(mod) + + +class TestPytestPluginManagerBootstrapming: + def test_preparse_args(self, pytestpm): + pytest.raises(ImportError, lambda: + pytestpm.consider_preparse(["xyz", "-p", "hello123"])) + + def test_plugin_prevent_register(self, pytestpm): + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l1 = pytestpm.getplugins() + pytestpm.register(42, name="abc") + l2 = pytestpm.getplugins() + assert len(l2) == len(l1) + + def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): + pytestpm.register(42, name="abc") + l1 = pytestpm.getplugins() + assert 42 in l1 + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l2 = pytestpm.getplugins() + assert 42 not in l2 diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,7 +1,7 @@ import pytest import os from _pytest.pytester import HookRecorder -from _pytest.core import PluginManager +from _pytest.config import PytestPluginManager from _pytest.main import EXIT_OK, EXIT_TESTSFAILED @@ -93,8 +93,8 @@ @pytest.mark.parametrize("holder", make_holder()) def test_hookrecorder_basic(holder): - pm = PluginManager() - pm.hook._addhooks(holder, "pytest_") + pm = PytestPluginManager() + pm.addhooks(holder) rec = HookRecorder(pm) pm.hook.pytest_xyz(arg=123) call = rec.popcall("pytest_xyz") diff -r f5541d71ac9cdb7897ccdb993f9f2896715a6f3c -r 21aefbeaf96fb994bce816d3884707100d0b74da testing/test_terminal.py --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -457,7 +457,9 @@ ]) assert result.ret == 1 - pytestconfig.pluginmanager.skipifmissing("xdist") + if not pytestconfig.pluginmanager.hasplugin("xdist"): + pytest.skip("xdist plugin not installed") + result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ "*FAIL*test_verbose_reporting.py::test_fail*", https://bitbucket.org/pytest-dev/pytest/commits/d9e0c52761ab/ Changeset: d9e0c52761ab Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 11:31:46+00:00 Summary: avoid prepend to register api as it's redundant wrt to hooks Affected #: 3 files diff -r 21aefbeaf96fb994bce816d3884707100d0b74da -r d9e0c52761aba632a3e48daed1688f1fe103f1ad _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -205,7 +205,7 @@ ", ".join(hook.argnames)) yield hook - def register(self, plugin, name=None, prepend=False, conftest=False): + def register(self, plugin, name=None, conftest=False): if self._name2plugin.get(name, None) == -1: return name = name or getattr(plugin, '__name__', str(id(plugin))) @@ -222,10 +222,7 @@ if conftest: self._conftestplugins.append(plugin) else: - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) + self._plugins.append(plugin) # finally make sure that the methods of the new plugin take part for hookcaller in hookcallers: hookcaller.scan_methods() diff -r 21aefbeaf96fb994bce816d3884707100d0b74da -r d9e0c52761aba632a3e48daed1688f1fe103f1ad _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -510,7 +510,7 @@ def __init__(self, config): FSCollector.__init__(self, config.rootdir, parent=None, config=config, session=self) - self.config.pluginmanager.register(self, name="session", prepend=True) + self.config.pluginmanager.register(self, name="session") self._testsfailed = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") @@ -521,10 +521,12 @@ def _makeid(self): return "" + @pytest.mark.tryfirst def pytest_collectstart(self): if self.shouldstop: raise self.Interrupted(self.shouldstop) + @pytest.mark.tryfirst def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): self._testsfailed += 1 diff -r 21aefbeaf96fb994bce816d3884707100d0b74da -r d9e0c52761aba632a3e48daed1688f1fe103f1ad testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -32,18 +32,6 @@ pm.unregister(a1) assert not pm.isregistered(a1) - def test_pm_ordering(self, pm): - class A: pass - a1, a2 = A(), A() - pm.register(a1) - pm.register(a2, "hello") - l = pm.getplugins() - assert l.index(a1) < l.index(a2) - a3 = A() - pm.register(a3, prepend=True) - l = pm.getplugins() - assert l.index(a3) == 0 - def test_register_mismatch_method(self): pm = get_plugin_manager() class hello: https://bitbucket.org/pytest-dev/pytest/commits/158322cbc5cc/ Changeset: 158322cbc5cc Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 11:33:01+00:00 Summary: move bookkeeping of conftest plugins in core pluginmanager to PytestPluginManager Affected #: 2 files diff -r d9e0c52761aba632a3e48daed1688f1fe103f1ad -r 158322cbc5cc6b9a9864ab851201f8d71a43d328 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -97,6 +97,7 @@ excludefunc=exclude_pytest_names) self._warnings = [] self._plugin_distinfo = [] + self._globalplugins = [] self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): @@ -108,6 +109,19 @@ pass self.set_tracing(err.write) + def register(self, plugin, name=None, conftest=False): + ret = super(PytestPluginManager, self).register(plugin, name) + if ret and not conftest: + self._globalplugins.append(plugin) + return ret + + def unregister(self, plugin): + super(PytestPluginManager, self).unregister(plugin) + try: + self._globalplugins.remove(plugin) + except ValueError: + pass + def getplugin(self, name): if name is None: return name @@ -787,7 +801,7 @@ setattr(self.option, opt.dest, opt.default) def _getmatchingplugins(self, fspath): - return self.pluginmanager._plugins + \ + return self.pluginmanager._globalplugins + \ self._conftest.getconftestmodules(fspath) def pytest_load_initial_conftests(self, early_config): diff -r d9e0c52761aba632a3e48daed1688f1fe103f1ad -r 158322cbc5cc6b9a9864ab851201f8d71a43d328 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -140,7 +140,6 @@ self._excludefunc = excludefunc self._name2plugin = {} self._plugins = [] - self._conftestplugins = [] self._plugin2hookcallers = {} self.trace = TagTracer().get("pluginmanage") self._shutdown = [] @@ -205,7 +204,7 @@ ", ".join(hook.argnames)) yield hook - def register(self, plugin, name=None, conftest=False): + def register(self, plugin, name=None): if self._name2plugin.get(name, None) == -1: return name = name or getattr(plugin, '__name__', str(id(plugin))) @@ -219,20 +218,14 @@ hookcallers = list(self._scan_plugin(plugin)) self._plugin2hookcallers[plugin] = hookcallers self._name2plugin[name] = plugin - if conftest: - self._conftestplugins.append(plugin) - else: - self._plugins.append(plugin) + self._plugins.append(plugin) # finally make sure that the methods of the new plugin take part for hookcaller in hookcallers: hookcaller.scan_methods() return True def unregister(self, plugin): - try: - self._plugins.remove(plugin) - except KeyError: - self._conftestplugins.remove(plugin) + self._plugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] @@ -247,13 +240,13 @@ while self._shutdown: func = self._shutdown.pop() func() - self._plugins = self._conftestplugins = [] + self._plugins = [] self._name2plugin.clear() def isregistered(self, plugin, name=None): if self.getplugin(name) is not None: return True - return plugin in self._plugins or plugin in self._conftestplugins + return plugin in self._plugins def addhooks(self, module_or_class): isclass = int(inspect.isclass(module_or_class)) @@ -271,7 +264,7 @@ %(self._prefix, module_or_class)) def getplugins(self): - return self._plugins + self._conftestplugins + return self._plugins def hasplugin(self, name): return bool(self.getplugin(name)) @@ -281,7 +274,7 @@ def listattr(self, attrname, plugins=None): if plugins is None: - plugins = self._plugins + self._conftestplugins + plugins = self._plugins l = [] last = [] wrappers = [] https://bitbucket.org/pytest-dev/pytest/commits/885fb42ad526/ Changeset: 885fb42ad526 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 11:37:42+00:00 Summary: simplify exception capturing Affected #: 1 file diff -r 158322cbc5cc6b9a9864ab851201f8d71a43d328 -r 885fb42ad526081e7c7a85f1c161e692f72a78ba _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -207,14 +207,11 @@ return try: mod = importplugin(modname) - except KeyboardInterrupt: - raise except ImportError: if modname.startswith("pytest_"): return self.import_plugin(modname[7:]) raise - except: - e = sys.exc_info()[1] + except Exception as e: import pytest if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): raise https://bitbucket.org/pytest-dev/pytest/commits/9595f9a127fc/ Changeset: 9595f9a127fc Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 11:44:37+00:00 Summary: avoid undocumented special casing of "pytest_" prefix Affected #: 4 files diff -r 885fb42ad526081e7c7a85f1c161e692f72a78ba -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -208,8 +208,6 @@ try: mod = importplugin(modname) except ImportError: - if modname.startswith("pytest_"): - return self.import_plugin(modname[7:]) raise except Exception as e: import pytest diff -r 885fb42ad526081e7c7a85f1c161e692f72a78ba -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -64,7 +64,7 @@ def test_testdir_runs_with_plugin(testdir): testdir.makepyfile(""" - pytest_plugins = "pytest_pytester" + pytest_plugins = "pytester" def test_hello(testdir): assert 1 """) diff -r 885fb42ad526081e7c7a85f1c161e692f72a78ba -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 testing/test_recwarn.py --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -22,7 +22,6 @@ def test_recwarn_functional(testdir): reprec = testdir.inline_runsource(""" - pytest_plugins = 'pytest_recwarn', import warnings oldwarn = warnings.showwarning def test_method(recwarn): diff -r 885fb42ad526081e7c7a85f1c161e692f72a78ba -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 testing/test_unittest.py --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -3,7 +3,6 @@ def test_simple_unittest(testdir): testpath = testdir.makepyfile(""" import unittest - pytest_plugins = "pytest_unittest" class MyTestCase(unittest.TestCase): def testpassing(self): self.assertEquals('foo', 'foo') @@ -17,7 +16,6 @@ def test_runTest_method(testdir): testdir.makepyfile(""" import unittest - pytest_plugins = "pytest_unittest" class MyTestCaseWithRunTest(unittest.TestCase): def runTest(self): self.assertEquals('foo', 'foo') https://bitbucket.org/pytest-dev/pytest/commits/582c7e123d81/ Changeset: 582c7e123d81 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 12:15:42+00:00 Summary: merge conftest management into PytestPluginManager Affected #: 5 files diff -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 -r 582c7e123d8101de97d377a389ce71baddbe6451 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -98,6 +98,12 @@ self._warnings = [] self._plugin_distinfo = [] self._globalplugins = [] + + # state related to local conftest plugins + self._path2confmods = {} + self._conftestpath2mod = {} + self._confcutdir = None + self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): @@ -141,6 +147,89 @@ config.warn(code="I1", message=warning) # + # internal API for local conftest plugin handling + # + def _set_initial_conftests(self, namespace): + """ load initial conftest files given a preparsed "namespace". + As conftest files may add their own command line options + which have arguments ('--my-opt somepath') we might get some + false positives. All builtin and 3rd party plugins will have + been loaded, however, so common options will not confuse our logic + here. + """ + current = py.path.local() + self._confcutdir = current.join(namespace.confcutdir, abs=True) \ + if namespace.confcutdir else None + testpaths = namespace.file_or_dir + foundanchor = False + for path in testpaths: + path = str(path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = current.join(path, abs=1) + if exists(anchor): # we found some file object + self._try_load_conftest(anchor) + foundanchor = True + if not foundanchor: + self._try_load_conftest(current) + + def _try_load_conftest(self, anchor): + self._getconftestmodules(anchor) + # let's also consider test* subdirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): + self._getconftestmodules(x) + + def _getconftestmodules(self, path): + try: + return self._path2confmods[path] + except KeyError: + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.check(file=1): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._path2confmods[path] = clist + return clist + + def _rget_with_confmod(self, name, path): + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest(self, conftestpath): + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + try: + mod = conftestpath.pyimport() + except Exception: + raise ConftestImportFailure(conftestpath, sys.exc_info()) + self._conftestpath2mod[conftestpath] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._path2confmods: + for path, mods in self._path2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace("loaded conftestmodule %r" %(mod)) + self.consider_conftest(mod) + return mod + + # # API for bootstrapping plugin loading # # @@ -572,96 +661,6 @@ return action._formatted_action_invocation -class Conftest(object): - """ the single place for accessing values and interacting - towards conftest modules from pytest objects. - """ - def __init__(self, onimport=None): - self._path2confmods = {} - self._onimport = onimport - self._conftestpath2mod = {} - self._confcutdir = None - - def setinitial(self, namespace): - """ load initial conftest files given a preparsed "namespace". - As conftest files may add their own command line options - which have arguments ('--my-opt somepath') we might get some - false positives. All builtin and 3rd party plugins will have - been loaded, however, so common options will not confuse our logic - here. - """ - current = py.path.local() - self._confcutdir = current.join(namespace.confcutdir, abs=True) \ - if namespace.confcutdir else None - testpaths = namespace.file_or_dir - foundanchor = False - for path in testpaths: - path = str(path) - # remove node-id syntax - i = path.find("::") - if i != -1: - path = path[:i] - anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object - self._try_load_conftest(anchor) - foundanchor = True - if not foundanchor: - self._try_load_conftest(current) - - def _try_load_conftest(self, anchor): - self.getconftestmodules(anchor) - # let's also consider test* subdirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self.getconftestmodules(x) - - def getconftestmodules(self, path): - try: - return self._path2confmods[path] - except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self.importconftest(conftestpath) - clist.append(mod) - self._path2confmods[path] = clist - return clist - - def rget_with_confmod(self, name, path): - modules = self.getconftestmodules(path) - for mod in reversed(modules): - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def importconftest(self, conftestpath): - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - try: - mod = conftestpath.pyimport() - except Exception: - raise ConftestImportFailure(conftestpath, sys.exc_info()) - self._conftestpath2mod[conftestpath] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._path2confmods: - for path, mods in self._path2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - if self._onimport: - self._onimport(mod) - return mod - def _ensure_removed_sysmodule(modname): try: @@ -697,7 +696,6 @@ #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") - self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook self._inicache = {} self._opt2dest = {} @@ -783,10 +781,6 @@ config.pluginmanager.consider_pluginarg(x) return config - def _onimportconftest(self, conftestmodule): - self.trace("loaded conftestmodule %r" %(conftestmodule,)) - self.pluginmanager.consider_conftest(conftestmodule) - def _processopt(self, opt): for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest @@ -797,10 +791,10 @@ def _getmatchingplugins(self, fspath): return self.pluginmanager._globalplugins + \ - self._conftest.getconftestmodules(fspath) + self.pluginmanager._getconftestmodules(fspath) def pytest_load_initial_conftests(self, early_config): - self._conftest.setinitial(early_config.known_args_namespace) + self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) pytest_load_initial_conftests.trylast = True def _initini(self, args): @@ -907,7 +901,7 @@ def _getconftest_pathlist(self, name, path): try: - mod, relroots = self._conftest.rget_with_confmod(name, path) + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() diff -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 -r 582c7e123d8101de97d377a389ce71baddbe6451 _pytest/doctest.py --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -132,7 +132,7 @@ def collect(self): import doctest if self.fspath.basename == "conftest.py": - module = self.config._conftest.importconftest(self.fspath) + module = self.config._conftest._importconftest(self.fspath) else: try: module = self.fspath.pyimport() diff -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 -r 582c7e123d8101de97d377a389ce71baddbe6451 testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1487,7 +1487,7 @@ reprec = testdir.inline_run("-v","-s") reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config - l = config._conftest.getconftestmodules(p)[0].l + l = config.pluginmanager._getconftestmodules(p)[0].l assert l == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 def test_scope_ordering(self, testdir): diff -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 -r 582c7e123d8101de97d377a389ce71baddbe6451 testing/test_conftest.py --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,7 +1,6 @@ from textwrap import dedent import py, pytest -from _pytest.config import Conftest - +from _pytest.config import PytestPluginManager @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -16,7 +15,7 @@ return tmpdir def ConftestWithSetinitial(path): - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) return conftest @@ -25,51 +24,41 @@ def __init__(self): self.file_or_dir = args self.confcutdir = str(confcutdir) - conftest.setinitial(Namespace()) + conftest._set_initial_conftests(Namespace()) class TestConftestValueAccessGlobal: def test_basic_init(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() p = basedir.join("adir") - assert conftest.rget_with_confmod("a", p)[1] == 1 - - def test_onimport(self, basedir): - l = [] - conftest = Conftest(onimport=l.append) - adir = basedir.join("adir") - conftest_setinitial(conftest, [adir], confcutdir=basedir) - assert len(l) == 1 - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("b", adir.join("b"))[1] == 2 - assert len(l) == 2 + assert conftest._rget_with_confmod("a", p)[1] == 1 def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() len(conftest._path2confmods) - conftest.getconftestmodules(basedir) + conftest._getconftestmodules(basedir) snap1 = len(conftest._path2confmods) #assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('adir')) + conftest._getconftestmodules(basedir.join('adir')) assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('b')) + conftest._getconftestmodules(basedir.join('b')) assert len(conftest._path2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): - conftest.rget_with_confmod('a', basedir) + conftest._rget_with_confmod('a', basedir) def test_value_access_by_path(self, basedir): conftest = ConftestWithSetinitial(basedir) adir = basedir.join("adir") - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("a", adir.join("b"))[1] == 1.5 + assert conftest._rget_with_confmod("a", adir)[1] == 1 + assert conftest._rget_with_confmod("a", adir.join("b"))[1] == 1.5 def test_value_access_with_confmod(self, basedir): startdir = basedir.join("adir", "b") startdir.ensure("xx", dir=True) conftest = ConftestWithSetinitial(startdir) - mod, value = conftest.rget_with_confmod("a", startdir) + mod, value = conftest._rget_with_confmod("a", startdir) assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") @@ -85,9 +74,9 @@ def test_doubledash_considered(testdir): conf = testdir.mkdir("--option") conf.join("conftest.py").ensure() - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.basename, conf.basename]) - l = conftest.getconftestmodules(conf) + l = conftest._getconftestmodules(conf) assert len(l) == 1 def test_issue151_load_all_conftests(testdir): @@ -96,7 +85,7 @@ p = testdir.mkdir(name) p.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, names) d = list(conftest._conftestpath2mod.values()) assert len(d) == len(names) @@ -105,15 +94,15 @@ testdir.makeconftest("x=3") p = testdir.makepyfile(""" import py, pytest - from _pytest.config import Conftest - conf = Conftest() - mod = conf.importconftest(py.path.local("conftest.py")) + from _pytest.config import PytestPluginManager + conf = PytestPluginManager() + mod = conf._importconftest(py.path.local("conftest.py")) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) subconf = py.path.local().ensure("sub", "conftest.py") subconf.write("y=4") - mod2 = conf.importconftest(subconf) + mod2 = conf._importconftest(subconf) assert mod != mod2 assert mod2.y == 4 import conftest @@ -125,27 +114,27 @@ def test_conftestcutdir(testdir): conf = testdir.makeconftest("") p = testdir.mkdir("x") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p) - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 0 - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 0 assert conf not in conftest._conftestpath2mod # but we can still import a conftest directly - conftest.importconftest(conf) - l = conftest.getconftestmodules(conf.dirpath()) + conftest._importconftest(conf) + l = conftest._getconftestmodules(conf.dirpath()) assert l[0].__file__.startswith(str(conf)) # and all sub paths get updated properly - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) def test_conftestcutdir_inplace_considered(testdir): conf = testdir.makeconftest("") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) @@ -153,7 +142,7 @@ def test_setinitial_conftest_subdirs(testdir, name): sub = testdir.mkdir(name) subconftest = sub.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ('whatever', '.dotdir'): assert subconftest in conftest._conftestpath2mod @@ -199,9 +188,9 @@ ct2.write("") def impct(p): return p - conftest = Conftest() - monkeypatch.setattr(conftest, 'importconftest', impct) - assert conftest.getconftestmodules(sub) == [ct1, ct2] + conftest = PytestPluginManager() + monkeypatch.setattr(conftest, '_importconftest', impct) + assert conftest._getconftestmodules(sub) == [ct1, ct2] def test_fixture_dependency(testdir, monkeypatch): diff -r 9595f9a127fc87b3b4bb06b054fce9e5a2f21857 -r 582c7e123d8101de97d377a389ce71baddbe6451 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -94,7 +94,7 @@ return xyz + 1 """) config = get_plugin_manager().config - config._conftest.importconftest(conf) + config.pluginmanager._importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] @@ -143,7 +143,7 @@ parser.addoption('--test123', action="store_true", default=True) """) - config._conftest.importconftest(p) + config.pluginmanager._importconftest(p) assert config.option.test123 def test_configure(self, testdir): @@ -849,10 +849,6 @@ mod = pytestpm.getplugin("pkg.plug") assert mod.x == 3 - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - def test_consider_conftest_deps(self, testdir, pytestpm): mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() with pytest.raises(ImportError): https://bitbucket.org/pytest-dev/pytest/commits/02581ca33317/ Changeset: 02581ca33317 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 12:54:28+00:00 Summary: slight cleanup of plugin register() functionality Affected #: 2 files diff -r 582c7e123d8101de97d377a389ce71baddbe6451 -r 02581ca33317258e8412fe4187299b6c74837d8a _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -121,6 +121,12 @@ self._globalplugins.append(plugin) return ret + def _do_register(self, plugin, name): + # called from core PluginManager class + if hasattr(self, "config"): + self.config._register_plugin(plugin, name) + return super(PytestPluginManager, self)._do_register(plugin, name) + def unregister(self, plugin): super(PytestPluginManager, self).unregister(plugin) try: @@ -701,7 +707,6 @@ self._opt2dest = {} self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") - self.pluginmanager.set_register_callback(self._register_plugin) self._configured = False def _register_plugin(self, plugin, name): diff -r 582c7e123d8101de97d377a389ce71baddbe6451 -r 02581ca33317258e8412fe4187299b6c74837d8a _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -167,10 +167,6 @@ # backward compatibility config.do_configure() - def set_register_callback(self, callback): - assert not hasattr(self, "_registercallback") - self._registercallback = callback - def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) methods = self.listattr(name, plugins=plugins) @@ -204,22 +200,26 @@ ", ".join(hook.argnames)) yield hook + def _get_canonical_name(self, plugin): + return getattr(plugin, "__name__", None) or str(id(plugin)) + def register(self, plugin, name=None): + name = name or self._get_canonical_name(plugin) if self._name2plugin.get(name, None) == -1: return - name = name or getattr(plugin, '__name__', str(id(plugin))) - if self.isregistered(plugin, name): + if self.hasplugin(name): raise ValueError("Plugin already registered: %s=%s\n%s" %( name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) - reg = getattr(self, "_registercallback", None) - if reg is not None: - reg(plugin, name) # may call addhooks + # allow subclasses to intercept here by calling a helper + return self._do_register(plugin, name) + + def _do_register(self, plugin, name): hookcallers = list(self._scan_plugin(plugin)) self._plugin2hookcallers[plugin] = hookcallers self._name2plugin[name] = plugin self._plugins.append(plugin) - # finally make sure that the methods of the new plugin take part + # rescan all methods for the hookcallers we found for hookcaller in hookcallers: hookcaller.scan_methods() return True @@ -243,11 +243,6 @@ self._plugins = [] self._name2plugin.clear() - def isregistered(self, plugin, name=None): - if self.getplugin(name) is not None: - return True - return plugin in self._plugins - def addhooks(self, module_or_class): isclass = int(inspect.isclass(module_or_class)) names = [] @@ -266,8 +261,12 @@ def getplugins(self): return self._plugins + def isregistered(self, plugin): + return self.hasplugin(self._get_canonical_name(plugin)) or \ + plugin in self._plugins + def hasplugin(self, name): - return bool(self.getplugin(name)) + return name in self._name2plugin def getplugin(self, name): return self._name2plugin.get(name) https://bitbucket.org/pytest-dev/pytest/commits/300a44d73410/ Changeset: 300a44d73410 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 14:33:20+00:00 Summary: remove shutdown logic from PluginManager and add a add_cleanup() API for the already existing cleanup logic of the config object. This simplifies lifecycle management as we don't keep two layers of shutdown functions and also simplifies the pluginmanager interface. also add some docstrings. Affected #: 12 files diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,15 @@ from ``inline_run()`` to allow temporary modules to be reloaded. Thanks Eduardo Schettino. +- internally refactor pluginmanager API and code so that there + is a clear distinction between a pytest-agnostic rather simple + pluginmanager and the PytestPluginManager which adds a lot of + behaviour, among it handling of the local conftest files. + In terms of documented methods this is a backward compatible + change but it might still break 3rd party plugins which relied on + details like especially the pluginmanager.add_shutdown() API. + Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/assertion/__init__.py --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -70,12 +70,11 @@ config._assertstate = AssertionState(config, mode) config._assertstate.hook = hook config._assertstate.trace("configured with mode set to %r" % (mode,)) - - -def pytest_unconfigure(config): - hook = config._assertstate.hook - if hook is not None and hook in sys.meta_path: - sys.meta_path.remove(hook) + def undo(): + hook = config._assertstate.hook + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + config.add_cleanup(undo) def pytest_collection(session): diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -37,13 +37,13 @@ pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown - pluginmanager.add_shutdown(capman.reset_capturings) + early_config.add_cleanup(capman.reset_capturings) # make sure logging does not raise exceptions at the end def silence_logging_at_shutdown(): if "logging" in sys.modules: sys.modules["logging"].raiseExceptions = False - pluginmanager.add_shutdown(silence_logging_at_shutdown) + early_config.add_cleanup(silence_logging_at_shutdown) # finally trigger conftest loading but while capturing (issue93) capman.init_capturings() diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -77,20 +77,17 @@ raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) pluginmanager = get_plugin_manager() - try: - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - except Exception: - pluginmanager.ensure_shutdown() - raise + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) def exclude_pytest_names(name): return not name.startswith(name) or name == "pytest_plugins" or \ name.startswith("pytest_funcarg__") + class PytestPluginManager(PluginManager): def __init__(self): super(PytestPluginManager, self).__init__(prefix="pytest_", @@ -723,16 +720,23 @@ if self._configured: call_plugin(plugin, "pytest_configure", {'config': self}) - def do_configure(self): + def add_cleanup(self, func): + """ Add a function to be called when the config object gets out of + use (usually coninciding with pytest_unconfigure).""" + self._cleanup.append(func) + + def _do_configure(self): assert not self._configured self._configured = True self.hook.pytest_configure(config=self) - def do_unconfigure(self): - assert self._configured - self._configured = False - self.hook.pytest_unconfigure(config=self) - self.pluginmanager.ensure_shutdown() + def _ensure_unconfigure(self): + if self._configured: + self._configured = False + self.hook.pytest_unconfigure(config=self) + while self._cleanup: + fin = self._cleanup.pop() + fin() def warn(self, code, message): """ generate a warning for this test session. """ @@ -747,11 +751,6 @@ self.parse(args) return self - def pytest_unconfigure(config): - while config._cleanup: - fin = config._cleanup.pop() - fin() - def notify_exception(self, excinfo, option=None): if option and option.fulltrace: style = "long" diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -135,6 +135,21 @@ class PluginManager(object): + """ Core Pluginmanager class which manages registration + of plugin objects and 1:N hook calling. + + You can register new hooks by calling ``addhooks(module_or_class)``. + You can register plugin objects (which contain hooks) by calling + ``register(plugin)``. The Pluginmanager is initialized with a + prefix that is searched for in the names of the dict of registered + plugin objects. An optional excludefunc allows to blacklist names which + are not considered as hooks despite a matching prefix. + + For debugging purposes you can call ``set_tracing(writer)`` + which will subsequently send debug information to the specified + write function. + """ + def __init__(self, prefix, excludefunc=None): self._prefix = prefix self._excludefunc = excludefunc @@ -142,10 +157,11 @@ self._plugins = [] self._plugin2hookcallers = {} self.trace = TagTracer().get("pluginmanage") - self._shutdown = [] self.hook = HookRelay(pm=self) def set_tracing(self, writer): + """ turn on tracing to the given writer method and + return an undo function. """ self.trace.root.setwriter(writer) # reconfigure HookCalling to perform tracing assert not hasattr(self, "_wrapping") @@ -160,12 +176,7 @@ trace("finish", self.name, "-->", box.result) trace.root.indent -= 1 - undo = add_method_wrapper(HookCaller, _docall) - self.add_shutdown(undo) - - def do_configure(self, config): - # backward compatibility - config.do_configure() + return add_method_wrapper(HookCaller, _docall) def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) @@ -233,16 +244,6 @@ for hookcaller in hookcallers: hookcaller.scan_methods() - def add_shutdown(self, func): - self._shutdown.append(func) - - def ensure_shutdown(self): - while self._shutdown: - func = self._shutdown.pop() - func() - self._plugins = [] - self._name2plugin.clear() - def addhooks(self, module_or_class): isclass = int(inspect.isclass(module_or_class)) names = [] diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -28,24 +28,20 @@ config = outcome.get_result() if config.option.debug: path = os.path.abspath("pytestdebug.log") - f = open(path, 'w') - config._debugfile = f - f.write("versions pytest-%s, py-%s, " + debugfile = open(path, 'w') + debugfile.write("versions pytest-%s, py-%s, " "python-%s\ncwd=%s\nargs=%s\n\n" %( pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(f.write) + config.pluginmanager.set_tracing(debugfile.write) sys.stderr.write("writing pytestdebug information to %s\n" % path) - - at pytest.mark.trylast -def pytest_unconfigure(config): - if hasattr(config, '_debugfile'): - config._debugfile.close() - sys.stderr.write("wrote pytestdebug information to %s\n" % - config._debugfile.name) - config.trace.root.setwriter(None) - + def unset_tracing(): + debugfile.close() + sys.stderr.write("wrote pytestdebug information to %s\n" % + debugfile.name) + config.trace.root.setwriter(None) + config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): if config.option.version: @@ -58,9 +54,9 @@ sys.stderr.write(line + "\n") return 0 elif config.option.help: - config.do_configure() + config._do_configure() showhelp(config) - config.do_unconfigure() + config._ensure_unconfigure() return 0 def showhelp(config): diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -77,7 +77,7 @@ initstate = 0 try: try: - config.do_configure() + config._do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) initstate = 2 @@ -107,9 +107,7 @@ config.hook.pytest_sessionfinish( session=session, exitstatus=session.exitstatus) - if initstate >= 1: - config.do_unconfigure() - config.pluginmanager.ensure_shutdown() + config._ensure_unconfigure() return session.exitstatus def pytest_cmdline_main(config): diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/mark.py --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -44,14 +44,14 @@ def pytest_cmdline_main(config): if config.option.markers: - config.do_configure() + config._do_configure() tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() - config.do_unconfigure() + config._ensure_unconfigure() return 0 pytest_cmdline_main.tryfirst = True diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -65,7 +65,8 @@ self.calls.append(ParsedCall(hookcaller.name, kwargs)) yield self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - pluginmanager.add_shutdown(self._undo_wrapping) + #if hasattr(pluginmanager, "config"): + # pluginmanager.add_shutdown(self._undo_wrapping) def finish_recording(self): self._undo_wrapping() @@ -571,12 +572,7 @@ # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) - def ensure_unconfigure(): - if hasattr(config.pluginmanager, "_config"): - config.pluginmanager.do_unconfigure(config) - config.pluginmanager.ensure_shutdown() - - self.request.addfinalizer(ensure_unconfigure) + self.request.addfinalizer(config._ensure_unconfigure) return config def parseconfigure(self, *args): @@ -588,8 +584,8 @@ """ config = self.parseconfig(*args) - config.do_configure() - self.request.addfinalizer(config.do_unconfigure) + config._do_configure() + self.request.addfinalizer(config._ensure_unconfigure) return config def getitem(self, source, funcname="test_func"): diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 testing/conftest.py --- a/testing/conftest.py +++ b/testing/conftest.py @@ -66,6 +66,7 @@ error.append(error[0]) raise AssertionError("\n".join(error)) + at pytest.mark.trylast def pytest_runtest_teardown(item, __multicall__): item.config._basedir.chdir() if hasattr(item.config, '_openfiles'): diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -155,13 +155,13 @@ config.pluginmanager.register(A()) assert len(l) == 0 - config.do_configure() + config._do_configure() assert len(l) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] - config.do_unconfigure() + config._ensure_unconfigure() config.pluginmanager.register(A()) assert len(l) == 2 diff -r 02581ca33317258e8412fe4187299b6c74837d8a -r 300a44d73410ff26328f49953240c64e78189913 testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -214,8 +214,8 @@ def test_plugin_already_exists(testdir): config = testdir.parseconfig("-p", "terminal") assert config.option.plugins == ['terminal'] - config.do_configure() - config.do_unconfigure() + config._do_configure() + config._ensure_unconfigure() def test_exclude(testdir): hellodir = testdir.mkdir("hello") https://bitbucket.org/pytest-dev/pytest/commits/55a53e8c99c6/ Changeset: 55a53e8c99c6 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 14:34:42+00:00 Summary: shuffle PluginManager method order to first have the public API and then the internal. Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/98fdb4919f0b/ Changeset: 98fdb4919f0b Branch: plugin_no_pytest User: hpk42 Date: 2015-04-22 14:42:41+00:00 Summary: reshuffle pluginmanager methods and add some docstrings. Affected #: 1 file diff -r 55a53e8c99c6b4023341f1ee9185b4b6cdde8457 -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,7 +1,6 @@ """ PluginManager, basic initialization and tracing. """ -import os import sys import inspect import py @@ -186,6 +185,108 @@ argnames=caller.argnames, methods=methods) return caller + def register(self, plugin, name=None): + """ Register a plugin with the given name and ensure that all its + hook implementations are integrated. If the name is not specified + we use the ``__name__`` attribute of the plugin object or, if that + doesn't exist, the id of the plugin. This method will raise a + ValueError if the eventual name is already registered. """ + name = name or self._get_canonical_name(plugin) + if self._name2plugin.get(name, None) == -1: + return + if self.hasplugin(name): + raise ValueError("Plugin already registered: %s=%s\n%s" %( + name, plugin, self._name2plugin)) + #self.trace("registering", name, plugin) + # allow subclasses to intercept here by calling a helper + return self._do_register(plugin, name) + + def _do_register(self, plugin, name): + hookcallers = list(self._scan_plugin(plugin)) + self._plugin2hookcallers[plugin] = hookcallers + self._name2plugin[name] = plugin + self._plugins.append(plugin) + # rescan all methods for the hookcallers we found + for hookcaller in hookcallers: + hookcaller.scan_methods() + return True + + def unregister(self, plugin): + """ unregister the plugin object and all its contained hook implementations + from internal data structures. """ + self._plugins.remove(plugin) + for name, value in list(self._name2plugin.items()): + if value == plugin: + del self._name2plugin[name] + hookcallers = self._plugin2hookcallers.pop(plugin) + for hookcaller in hookcallers: + hookcaller.scan_methods() + + def addhooks(self, module_or_class): + """ add new hook definitions from the given module_or_class using + the prefix/excludefunc with which the PluginManager was initialized. """ + isclass = int(inspect.isclass(module_or_class)) + names = [] + for name in dir(module_or_class): + if name.startswith(self._prefix): + method = module_or_class.__dict__[name] + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self.hook, name, firstresult=firstresult, + argnames=varnames(method, startindex=isclass)) + setattr(self.hook, name, hc) + names.append(name) + if not names: + raise ValueError("did not find new %r hooks in %r" + %(self._prefix, module_or_class)) + + def getplugins(self): + """ return the complete list of registered plugins. NOTE that + you will get the internal list and need to make a copy if you + modify the list.""" + return self._plugins + + def isregistered(self, plugin): + """ Return True if the plugin is already registered under its + canonical name. """ + return self.hasplugin(self._get_canonical_name(plugin)) or \ + plugin in self._plugins + + def hasplugin(self, name): + """ Return True if there is a registered with the given name. """ + return name in self._name2plugin + + def getplugin(self, name): + """ Return a plugin or None for the given name. """ + return self._name2plugin.get(name) + + def listattr(self, attrname, plugins=None): + if plugins is None: + plugins = self._plugins + l = [] + last = [] + wrappers = [] + for plugin in plugins: + try: + meth = getattr(plugin, attrname) + except AttributeError: + continue + if hasattr(meth, 'hookwrapper'): + wrappers.append(meth) + elif hasattr(meth, 'tryfirst'): + last.append(meth) + elif hasattr(meth, 'trylast'): + l.insert(0, meth) + else: + l.append(meth) + l.extend(last) + l.extend(wrappers) + return l + + def call_plugin(self, plugin, methname, kwargs): + return MultiCall(methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() + + def _scan_plugin(self, plugin): def fail(msg, *args): name = getattr(plugin, '__name__', plugin) @@ -214,90 +315,6 @@ def _get_canonical_name(self, plugin): return getattr(plugin, "__name__", None) or str(id(plugin)) - def register(self, plugin, name=None): - name = name or self._get_canonical_name(plugin) - if self._name2plugin.get(name, None) == -1: - return - if self.hasplugin(name): - raise ValueError("Plugin already registered: %s=%s\n%s" %( - name, plugin, self._name2plugin)) - #self.trace("registering", name, plugin) - # allow subclasses to intercept here by calling a helper - return self._do_register(plugin, name) - - def _do_register(self, plugin, name): - hookcallers = list(self._scan_plugin(plugin)) - self._plugin2hookcallers[plugin] = hookcallers - self._name2plugin[name] = plugin - self._plugins.append(plugin) - # rescan all methods for the hookcallers we found - for hookcaller in hookcallers: - hookcaller.scan_methods() - return True - - def unregister(self, plugin): - self._plugins.remove(plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - hookcallers = self._plugin2hookcallers.pop(plugin) - for hookcaller in hookcallers: - hookcaller.scan_methods() - - def addhooks(self, module_or_class): - isclass = int(inspect.isclass(module_or_class)) - names = [] - for name in dir(module_or_class): - if name.startswith(self._prefix): - method = module_or_class.__dict__[name] - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self.hook, name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self.hook, name, hc) - names.append(name) - if not names: - raise ValueError("did not find new %r hooks in %r" - %(self._prefix, module_or_class)) - - def getplugins(self): - return self._plugins - - def isregistered(self, plugin): - return self.hasplugin(self._get_canonical_name(plugin)) or \ - plugin in self._plugins - - def hasplugin(self, name): - return name in self._name2plugin - - def getplugin(self, name): - return self._name2plugin.get(name) - - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - last = [] - wrappers = [] - for plugin in plugins: - try: - meth = getattr(plugin, attrname) - except AttributeError: - continue - if hasattr(meth, 'hookwrapper'): - wrappers.append(meth) - elif hasattr(meth, 'tryfirst'): - last.append(meth) - elif hasattr(meth, 'trylast'): - l.insert(0, meth) - else: - l.append(meth) - l.extend(last) - l.extend(wrappers) - return l - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() class MultiCall: https://bitbucket.org/pytest-dev/pytest/commits/5dd0adc5efd0/ Changeset: 5dd0adc5efd0 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-23 10:35:15+00:00 Summary: merge default Affected #: 6 files diff -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e .hgignore --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,8 @@ doc/*/_build build/ dist/ +testing/cx_freeze/build +testing/cx_freeze/cx_freeze_source *.egg-info issue/ env/ diff -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,8 @@ - fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing when tests raised SystemExit. Thanks Holger Krekel. +- reintroduced _pytest fixture of the pytester plugin which is used + at least by pytest-xdist. 2.7.0 (compared to 2.6.4) ----------------------------- diff -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e README.rst --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -.. image:: https://drone.io/bitbucket.org/pytest-dev/pytest/status.png - :target: https://drone.io/bitbucket.org/pytest-dev/pytest/latest .. image:: https://pypip.in/v/pytest/badge.png :target: https://pypi.python.org/pypi/pytest diff -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -15,6 +15,24 @@ from _pytest.main import Session, EXIT_OK +# used at least by pytest-xdist plugin + at pytest.fixture +def _pytest(request): + """ Return a helper which offers a gethookrecorder(hook) + method which returns a HookRecorder instance which helps + to make assertions about called hooks. + """ + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + def get_public_names(l): """Only return names from iterator l without a leading underscore.""" diff -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e testing/cx_freeze/install_cx_freeze.py --- /dev/null +++ b/testing/cx_freeze/install_cx_freeze.py @@ -0,0 +1,66 @@ +""" +Installs cx_freeze from source, but first patching +setup.py as described here: + +http://stackoverflow.com/questions/25107697/compiling-cx-freeze-under-ubuntu +""" +import glob +import shutil +import tarfile +import os +import sys +import platform + +if __name__ == '__main__': + if 'ubuntu' not in platform.version().lower(): + + print('Not Ubuntu, installing using pip. (platform.version() is %r)' % + platform.version()) + res = os.system('pip install cx_freeze') + if res != 0: + sys.exit(res) + sys.exit(0) + + if os.path.isdir('cx_freeze_source'): + shutil.rmtree('cx_freeze_source') + os.mkdir('cx_freeze_source') + + res = os.system('pip install --download cx_freeze_source --no-use-wheel ' + 'cx_freeze') + if res != 0: + sys.exit(res) + + packages = glob.glob('cx_freeze_source/*.tar.gz') + assert len(packages) == 1 + tar_filename = packages[0] + + tar_file = tarfile.open(tar_filename) + try: + tar_file.extractall(path='cx_freeze_source') + finally: + tar_file.close() + + basename = os.path.basename(tar_filename).replace('.tar.gz', '') + setup_py_filename = 'cx_freeze_source/%s/setup.py' % basename + with open(setup_py_filename) as f: + lines = f.readlines() + + line_to_patch = 'if not vars.get("Py_ENABLE_SHARED", 0):' + for index, line in enumerate(lines): + if line_to_patch in line: + indent = line[:line.index(line_to_patch)] + lines[index] = indent + 'if True:\n' + print('Patched line %d' % (index + 1)) + break + else: + sys.exit('Could not find line in setup.py to patch!') + + with open(setup_py_filename, 'w') as f: + f.writelines(lines) + + os.chdir('cx_freeze_source/%s' % basename) + res = os.system('python setup.py install') + if res != 0: + sys.exit(res) + + sys.exit(0) \ No newline at end of file diff -r 98fdb4919f0b358f7b73c3db86029d80dca7e148 -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e tox.ini --- a/tox.ini +++ b/tox.ini @@ -125,10 +125,10 @@ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [] [testenv:py27-cxfreeze] -deps=cx_freeze changedir=testing/cx_freeze basepython=python2.7 commands= + {envpython} install_cx_freeze.py {envpython} runtests_setup.py build --build-exe build {envpython} tox_run.py https://bitbucket.org/pytest-dev/pytest/commits/b66bb638a5ac/ Changeset: b66bb638a5ac Branch: plugin_no_pytest User: hpk42 Date: 2015-04-23 10:39:11+00:00 Summary: streamline and document handling of builtin module special casing. Affected #: 2 files diff -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e -r b66bb638a5ac20659f54feba2b4cfa2ff73411df _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -53,6 +53,10 @@ "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml resultlog doctest").split() +builtin_plugins = set(default_plugins) +builtin_plugins.add("pytester") + + def _preloadplugins(): assert not _preinit _preinit.append(get_plugin_manager()) @@ -131,14 +135,6 @@ except ValueError: pass - def getplugin(self, name): - if name is None: - return name - plugin = super(PytestPluginManager, self).getplugin(name) - if plugin is None: - plugin = super(PytestPluginManager, self).getplugin("_pytest." + name) - return plugin - def pytest_configure(self, config): config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " @@ -294,11 +290,19 @@ self.import_plugin(spec) def import_plugin(self, modname): + # most often modname refers to builtin modules, e.g. "pytester", + # "terminal" or "capture". Those plugins are registered under their + # basename for historic purposes but must be imported with the + # _pytest prefix. assert isinstance(modname, str) if self.getplugin(modname) is not None: return + if modname in builtin_plugins: + importspec = "_pytest." + modname + else: + importspec = modname try: - mod = importplugin(modname) + __import__(importspec) except ImportError: raise except Exception as e: @@ -307,6 +311,7 @@ raise self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) else: + mod = sys.modules[importspec] self.register(mod, modname) self.consider_module(mod) @@ -1040,14 +1045,3 @@ # pytest.__all__.append(name) setattr(pytest, name, value) - -def importplugin(importspec): - name = importspec - try: - mod = "_pytest." + name - __import__(mod) - return sys.modules[mod] - except ImportError: - __import__(importspec) - return sys.modules[importspec] - diff -r 5dd0adc5efd0533b71fa68ce2465ed62a9aba49e -r b66bb638a5ac20659f54feba2b4cfa2ff73411df testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,6 +1,6 @@ import pytest, py, os from _pytest.core import * # noqa -from _pytest.config import get_plugin_manager, importplugin +from _pytest.config import get_plugin_manager @pytest.fixture @@ -590,10 +590,11 @@ "*trylast*last*", ]) -def test_importplugin_issue375(testdir): +def test_importplugin_issue375(testdir, pytestpm): testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe="import aaaa") - excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) + with pytest.raises(ImportError) as excinfo: + pytestpm.import_plugin("qwe") assert "qwe" not in str(excinfo.value) assert "aaaa" in str(excinfo.value) https://bitbucket.org/pytest-dev/pytest/commits/b26c40205235/ Changeset: b26c40205235 Branch: plugin_no_pytest User: hpk42 Date: 2015-04-23 11:15:34+00:00 Summary: remove some redundancy when parsing import spec Affected #: 1 file diff -r b66bb638a5ac20659f54feba2b4cfa2ff73411df -r b26c402052354f344adee76a5a2209a39fbb3b1f _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -232,15 +232,6 @@ # API for bootstrapping plugin loading # # - def _envlist(self, varname): - val = os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) def consider_setuptools_entrypoints(self): try: @@ -281,13 +272,18 @@ conftest=True): self.consider_module(conftestmodule) + def consider_env(self): + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) + self._import_plugin_specs(getattr(mod, "pytest_plugins", None)) + + def _import_plugin_specs(self, spec): + if spec: + if isinstance(spec, str): + spec = spec.split(",") + for import_spec in spec: + self.import_plugin(import_spec) def import_plugin(self, modname): # most often modname refers to builtin modules, e.g. "pytester", https://bitbucket.org/pytest-dev/pytest/commits/1ecf40e29f0f/ Changeset: 1ecf40e29f0f Branch: plugin_no_pytest User: hpk42 Date: 2015-04-24 11:02:49+00:00 Summary: minimize HookCaller attributes: avoid passing in hookrelay to HookCallers Affected #: 1 file diff -r b26c402052354f344adee76a5a2209a39fbb3b1f -r 1ecf40e29f0f56d107192f3eea9b26b963e853e3 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -166,14 +166,15 @@ assert not hasattr(self, "_wrapping") self._wrapping = True + hooktrace = self.hook.trace + def _docall(self, methods, kwargs): - trace = self.hookrelay.trace - trace.root.indent += 1 - trace(self.name, kwargs) + hooktrace.root.indent += 1 + hooktrace(self.name, kwargs) box = yield if box.excinfo is None: - trace("finish", self.name, "-->", box.result) - trace.root.indent -= 1 + hooktrace("finish", self.name, "-->", box.result) + hooktrace.root.indent -= 1 return add_method_wrapper(HookCaller, _docall) @@ -181,7 +182,7 @@ caller = getattr(self.hook, name) methods = self.listattr(name, plugins=plugins) if methods: - return HookCaller(self.hook, caller.name, caller.firstresult, + return HookCaller(caller.name, caller.firstresult, argnames=caller.argnames, methods=methods) return caller @@ -208,7 +209,7 @@ self._plugins.append(plugin) # rescan all methods for the hookcallers we found for hookcaller in hookcallers: - hookcaller.scan_methods() + self._scan_methods(hookcaller) return True def unregister(self, plugin): @@ -220,7 +221,7 @@ del self._name2plugin[name] hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: - hookcaller.scan_methods() + self._scan_methods(hookcaller) def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using @@ -231,7 +232,7 @@ if name.startswith(self._prefix): method = module_or_class.__dict__[name] firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self.hook, name, firstresult=firstresult, + hc = HookCaller(name, firstresult=firstresult, argnames=varnames(method, startindex=isclass)) setattr(self.hook, name, hc) names.append(name) @@ -282,6 +283,9 @@ l.extend(wrappers) return l + def _scan_methods(self, hookcaller): + hookcaller.methods = self.listattr(hookcaller.name) + def call_plugin(self, plugin, methname, kwargs): return MultiCall(methods=self.listattr(methname, plugins=[plugin]), kwargs=kwargs, firstresult=True).execute() @@ -394,8 +398,7 @@ class HookCaller: - def __init__(self, hookrelay, name, firstresult, argnames, methods=()): - self.hookrelay = hookrelay + def __init__(self, name, firstresult, argnames, methods=()): self.name = name self.firstresult = firstresult self.argnames = ["__multicall__"] @@ -406,9 +409,6 @@ def __repr__(self): return "" %(self.name,) - def scan_methods(self): - self.methods = self.hookrelay._pm.listattr(self.name) - def __call__(self, **kwargs): return self._docall(self.methods, kwargs) https://bitbucket.org/pytest-dev/pytest/commits/49c0ea47afbf/ Changeset: 49c0ea47afbf Branch: plugin_no_pytest User: hpk42 Date: 2015-04-24 12:09:57+00:00 Summary: remove useless check Affected #: 1 file diff -r 1ecf40e29f0f56d107192f3eea9b26b963e853e3 -r 49c0ea47afbfd0a04b70fbf7de57531f53cd3f9e _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -181,10 +181,8 @@ def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) methods = self.listattr(name, plugins=plugins) - if methods: - return HookCaller(caller.name, caller.firstresult, - argnames=caller.argnames, methods=methods) - return caller + return HookCaller(caller.name, caller.firstresult, + argnames=caller.argnames, methods=methods) def register(self, plugin, name=None): """ Register a plugin with the given name and ensure that all its https://bitbucket.org/pytest-dev/pytest/commits/90f9b67b555f/ Changeset: 90f9b67b555f User: hpk42 Date: 2015-04-25 07:08:21+00:00 Summary: Merged in hpk42/pytest-patches/plugin_no_pytest (pull request #278) Refactor pluginmanagement Affected #: 20 files diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,15 @@ from ``inline_run()`` to allow temporary modules to be reloaded. Thanks Eduardo Schettino. +- internally refactor pluginmanager API and code so that there + is a clear distinction between a pytest-agnostic rather simple + pluginmanager and the PytestPluginManager which adds a lot of + behaviour, among it handling of the local conftest files. + In terms of documented methods this is a backward compatible + change but it might still break 3rd party plugins which relied on + details like especially the pluginmanager.add_shutdown() API. + Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/assertion/__init__.py --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -70,12 +70,11 @@ config._assertstate = AssertionState(config, mode) config._assertstate.hook = hook config._assertstate.trace("configured with mode set to %r" % (mode,)) - - -def pytest_unconfigure(config): - hook = config._assertstate.hook - if hook is not None and hook in sys.meta_path: - sys.meta_path.remove(hook) + def undo(): + hook = config._assertstate.hook + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + config.add_cleanup(undo) def pytest_collection(session): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -37,13 +37,13 @@ pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown - pluginmanager.add_shutdown(capman.reset_capturings) + early_config.add_cleanup(capman.reset_capturings) # make sure logging does not raise exceptions at the end def silence_logging_at_shutdown(): if "logging" in sys.modules: sys.modules["logging"].raiseExceptions = False - pluginmanager.add_shutdown(silence_logging_at_shutdown) + early_config.add_cleanup(silence_logging_at_shutdown) # finally trigger conftest loading but while capturing (issue93) capman.init_capturings() diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -53,6 +53,10 @@ "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml resultlog doctest").split() +builtin_plugins = set(default_plugins) +builtin_plugins.add("pytester") + + def _preloadplugins(): assert not _preinit _preinit.append(get_plugin_manager()) @@ -77,19 +81,31 @@ raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) pluginmanager = get_plugin_manager() - try: - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - except Exception: - pluginmanager.ensure_shutdown() - raise + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + +def exclude_pytest_names(name): + return not name.startswith(name) or name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + class PytestPluginManager(PluginManager): - def __init__(self, hookspecs=[hookspec]): - super(PytestPluginManager, self).__init__(hookspecs=hookspecs) + def __init__(self): + super(PytestPluginManager, self).__init__(prefix="pytest_", + excludefunc=exclude_pytest_names) + self._warnings = [] + self._plugin_distinfo = [] + self._globalplugins = [] + + # state related to local conftest plugins + self._path2confmods = {} + self._conftestpath2mod = {} + self._confcutdir = None + + self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): err = sys.stderr @@ -100,6 +116,25 @@ pass self.set_tracing(err.write) + def register(self, plugin, name=None, conftest=False): + ret = super(PytestPluginManager, self).register(plugin, name) + if ret and not conftest: + self._globalplugins.append(plugin) + return ret + + def _do_register(self, plugin, name): + # called from core PluginManager class + if hasattr(self, "config"): + self.config._register_plugin(plugin, name) + return super(PytestPluginManager, self)._do_register(plugin, name) + + def unregister(self, plugin): + super(PytestPluginManager, self).unregister(plugin) + try: + self._globalplugins.remove(plugin) + except ValueError: + pass + def pytest_configure(self, config): config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " @@ -110,6 +145,172 @@ for warning in self._warnings: config.warn(code="I1", message=warning) + # + # internal API for local conftest plugin handling + # + def _set_initial_conftests(self, namespace): + """ load initial conftest files given a preparsed "namespace". + As conftest files may add their own command line options + which have arguments ('--my-opt somepath') we might get some + false positives. All builtin and 3rd party plugins will have + been loaded, however, so common options will not confuse our logic + here. + """ + current = py.path.local() + self._confcutdir = current.join(namespace.confcutdir, abs=True) \ + if namespace.confcutdir else None + testpaths = namespace.file_or_dir + foundanchor = False + for path in testpaths: + path = str(path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = current.join(path, abs=1) + if exists(anchor): # we found some file object + self._try_load_conftest(anchor) + foundanchor = True + if not foundanchor: + self._try_load_conftest(current) + + def _try_load_conftest(self, anchor): + self._getconftestmodules(anchor) + # let's also consider test* subdirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): + self._getconftestmodules(x) + + def _getconftestmodules(self, path): + try: + return self._path2confmods[path] + except KeyError: + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.check(file=1): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._path2confmods[path] = clist + return clist + + def _rget_with_confmod(self, name, path): + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest(self, conftestpath): + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + try: + mod = conftestpath.pyimport() + except Exception: + raise ConftestImportFailure(conftestpath, sys.exc_info()) + self._conftestpath2mod[conftestpath] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._path2confmods: + for path, mods in self._path2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace("loaded conftestmodule %r" %(mod)) + self.consider_conftest(mod) + return mod + + # + # API for bootstrapping plugin loading + # + # + + def consider_setuptools_entrypoints(self): + try: + from pkg_resources import iter_entry_points, DistributionNotFound + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = ep.name + if name.startswith("pytest_"): + name = name[7:] + if ep.name in self._name2plugin or name in self._name2plugin: + continue + try: + plugin = ep.load() + except DistributionNotFound: + continue + self._plugin_distinfo.append((ep.dist, plugin)) + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.consider_pluginarg(opt2) + + def consider_pluginarg(self, arg): + if arg.startswith("no:"): + name = arg[3:] + plugin = self.getplugin(name) + if plugin is not None: + self.unregister(plugin) + self._name2plugin[name] = -1 + else: + if self.getplugin(arg) is None: + self.import_plugin(arg) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__, + conftest=True): + self.consider_module(conftestmodule) + + def consider_env(self): + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + + def consider_module(self, mod): + self._import_plugin_specs(getattr(mod, "pytest_plugins", None)) + + def _import_plugin_specs(self, spec): + if spec: + if isinstance(spec, str): + spec = spec.split(",") + for import_spec in spec: + self.import_plugin(import_spec) + + def import_plugin(self, modname): + # most often modname refers to builtin modules, e.g. "pytester", + # "terminal" or "capture". Those plugins are registered under their + # basename for historic purposes but must be imported with the + # _pytest prefix. + assert isinstance(modname, str) + if self.getplugin(modname) is not None: + return + if modname in builtin_plugins: + importspec = "_pytest." + modname + else: + importspec = modname + try: + __import__(importspec) + except ImportError: + raise + except Exception as e: + import pytest + if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): + raise + self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + mod = sys.modules[importspec] + self.register(mod, modname) + self.consider_module(mod) + class Parser: """ Parser for command line arguments and ini-file values. """ @@ -464,96 +665,6 @@ return action._formatted_action_invocation -class Conftest(object): - """ the single place for accessing values and interacting - towards conftest modules from pytest objects. - """ - def __init__(self, onimport=None): - self._path2confmods = {} - self._onimport = onimport - self._conftestpath2mod = {} - self._confcutdir = None - - def setinitial(self, namespace): - """ load initial conftest files given a preparsed "namespace". - As conftest files may add their own command line options - which have arguments ('--my-opt somepath') we might get some - false positives. All builtin and 3rd party plugins will have - been loaded, however, so common options will not confuse our logic - here. - """ - current = py.path.local() - self._confcutdir = current.join(namespace.confcutdir, abs=True) \ - if namespace.confcutdir else None - testpaths = namespace.file_or_dir - foundanchor = False - for path in testpaths: - path = str(path) - # remove node-id syntax - i = path.find("::") - if i != -1: - path = path[:i] - anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object - self._try_load_conftest(anchor) - foundanchor = True - if not foundanchor: - self._try_load_conftest(current) - - def _try_load_conftest(self, anchor): - self.getconftestmodules(anchor) - # let's also consider test* subdirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self.getconftestmodules(x) - - def getconftestmodules(self, path): - try: - return self._path2confmods[path] - except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self.importconftest(conftestpath) - clist.append(mod) - self._path2confmods[path] = clist - return clist - - def rget_with_confmod(self, name, path): - modules = self.getconftestmodules(path) - for mod in reversed(modules): - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def importconftest(self, conftestpath): - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - try: - mod = conftestpath.pyimport() - except Exception: - raise ConftestImportFailure(conftestpath, sys.exc_info()) - self._conftestpath2mod[conftestpath] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._path2confmods: - for path, mods in self._path2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - if self._onimport: - self._onimport(mod) - return mod - def _ensure_removed_sysmodule(modname): try: @@ -589,13 +700,11 @@ #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") - self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook self._inicache = {} self._opt2dest = {} self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") - self.pluginmanager.set_register_callback(self._register_plugin) self._configured = False def _register_plugin(self, plugin, name): @@ -612,16 +721,23 @@ if self._configured: call_plugin(plugin, "pytest_configure", {'config': self}) - def do_configure(self): + def add_cleanup(self, func): + """ Add a function to be called when the config object gets out of + use (usually coninciding with pytest_unconfigure).""" + self._cleanup.append(func) + + def _do_configure(self): assert not self._configured self._configured = True self.hook.pytest_configure(config=self) - def do_unconfigure(self): - assert self._configured - self._configured = False - self.hook.pytest_unconfigure(config=self) - self.pluginmanager.ensure_shutdown() + def _ensure_unconfigure(self): + if self._configured: + self._configured = False + self.hook.pytest_unconfigure(config=self) + while self._cleanup: + fin = self._cleanup.pop() + fin() def warn(self, code, message): """ generate a warning for this test session. """ @@ -636,11 +752,6 @@ self.parse(args) return self - def pytest_unconfigure(config): - while config._cleanup: - fin = config._cleanup.pop() - fin() - def notify_exception(self, excinfo, option=None): if option and option.fulltrace: style = "long" @@ -675,10 +786,6 @@ config.pluginmanager.consider_pluginarg(x) return config - def _onimportconftest(self, conftestmodule): - self.trace("loaded conftestmodule %r" %(conftestmodule,)) - self.pluginmanager.consider_conftest(conftestmodule) - def _processopt(self, opt): for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest @@ -688,11 +795,11 @@ setattr(self.option, opt.dest, opt.default) def _getmatchingplugins(self, fspath): - return self.pluginmanager._plugins + \ - self._conftest.getconftestmodules(fspath) + return self.pluginmanager._globalplugins + \ + self.pluginmanager._getconftestmodules(fspath) def pytest_load_initial_conftests(self, early_config): - self._conftest.setinitial(early_config.known_args_namespace) + self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) pytest_load_initial_conftests.trylast = True def _initini(self, args): @@ -799,7 +906,7 @@ def _getconftest_pathlist(self, name, path): try: - mod, relroots = self._conftest.rget_with_confmod(name, path) + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() @@ -933,3 +1040,4 @@ #if obj != pytest: # pytest.__all__.append(name) setattr(pytest, name, value) + diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,14 +1,9 @@ """ -pytest PluginManager, basic initialization and tracing. +PluginManager, basic initialization and tracing. """ -import os import sys import inspect import py -# don't import pytest to avoid circular imports - -assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) py3 = sys.version_info > (3,0) @@ -139,202 +134,133 @@ class PluginManager(object): - def __init__(self, hookspecs=None, prefix="pytest_"): + """ Core Pluginmanager class which manages registration + of plugin objects and 1:N hook calling. + + You can register new hooks by calling ``addhooks(module_or_class)``. + You can register plugin objects (which contain hooks) by calling + ``register(plugin)``. The Pluginmanager is initialized with a + prefix that is searched for in the names of the dict of registered + plugin objects. An optional excludefunc allows to blacklist names which + are not considered as hooks despite a matching prefix. + + For debugging purposes you can call ``set_tracing(writer)`` + which will subsequently send debug information to the specified + write function. + """ + + def __init__(self, prefix, excludefunc=None): + self._prefix = prefix + self._excludefunc = excludefunc self._name2plugin = {} self._plugins = [] - self._conftestplugins = [] self._plugin2hookcallers = {} - self._warnings = [] self.trace = TagTracer().get("pluginmanage") - self._plugin_distinfo = [] - self._shutdown = [] - self.hook = HookRelay(hookspecs or [], pm=self, prefix=prefix) + self.hook = HookRelay(pm=self) def set_tracing(self, writer): + """ turn on tracing to the given writer method and + return an undo function. """ self.trace.root.setwriter(writer) # reconfigure HookCalling to perform tracing assert not hasattr(self, "_wrapping") self._wrapping = True + hooktrace = self.hook.trace + def _docall(self, methods, kwargs): - trace = self.hookrelay.trace - trace.root.indent += 1 - trace(self.name, kwargs) + hooktrace.root.indent += 1 + hooktrace(self.name, kwargs) box = yield if box.excinfo is None: - trace("finish", self.name, "-->", box.result) - trace.root.indent -= 1 + hooktrace("finish", self.name, "-->", box.result) + hooktrace.root.indent -= 1 - undo = add_method_wrapper(HookCaller, _docall) - self.add_shutdown(undo) + return add_method_wrapper(HookCaller, _docall) - def do_configure(self, config): - # backward compatibility - config.do_configure() + def make_hook_caller(self, name, plugins): + caller = getattr(self.hook, name) + methods = self.listattr(name, plugins=plugins) + return HookCaller(caller.name, caller.firstresult, + argnames=caller.argnames, methods=methods) - def set_register_callback(self, callback): - assert not hasattr(self, "_registercallback") - self._registercallback = callback - - def register(self, plugin, name=None, prepend=False, conftest=False): + def register(self, plugin, name=None): + """ Register a plugin with the given name and ensure that all its + hook implementations are integrated. If the name is not specified + we use the ``__name__`` attribute of the plugin object or, if that + doesn't exist, the id of the plugin. This method will raise a + ValueError if the eventual name is already registered. """ + name = name or self._get_canonical_name(plugin) if self._name2plugin.get(name, None) == -1: return - name = name or getattr(plugin, '__name__', str(id(plugin))) - if self.isregistered(plugin, name): + if self.hasplugin(name): raise ValueError("Plugin already registered: %s=%s\n%s" %( name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) - reg = getattr(self, "_registercallback", None) - if reg is not None: - reg(plugin, name) # may call addhooks - hookcallers = list(self.hook._scan_plugin(plugin)) + # allow subclasses to intercept here by calling a helper + return self._do_register(plugin, name) + + def _do_register(self, plugin, name): + hookcallers = list(self._scan_plugin(plugin)) self._plugin2hookcallers[plugin] = hookcallers self._name2plugin[name] = plugin - if conftest: - self._conftestplugins.append(plugin) - else: - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - # finally make sure that the methods of the new plugin take part + self._plugins.append(plugin) + # rescan all methods for the hookcallers we found for hookcaller in hookcallers: - hookcaller.scan_methods() + self._scan_methods(hookcaller) return True def unregister(self, plugin): - try: - self._plugins.remove(plugin) - except KeyError: - self._conftestplugins.remove(plugin) + """ unregister the plugin object and all its contained hook implementations + from internal data structures. """ + self._plugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: - hookcaller.scan_methods() + self._scan_methods(hookcaller) - def add_shutdown(self, func): - self._shutdown.append(func) - - def ensure_shutdown(self): - while self._shutdown: - func = self._shutdown.pop() - func() - self._plugins = self._conftestplugins = [] - self._name2plugin.clear() - - def isregistered(self, plugin, name=None): - if self.getplugin(name) is not None: - return True - return plugin in self._plugins or plugin in self._conftestplugins - - def addhooks(self, spec, prefix="pytest_"): - self.hook._addhooks(spec, prefix=prefix) + def addhooks(self, module_or_class): + """ add new hook definitions from the given module_or_class using + the prefix/excludefunc with which the PluginManager was initialized. """ + isclass = int(inspect.isclass(module_or_class)) + names = [] + for name in dir(module_or_class): + if name.startswith(self._prefix): + method = module_or_class.__dict__[name] + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(name, firstresult=firstresult, + argnames=varnames(method, startindex=isclass)) + setattr(self.hook, name, hc) + names.append(name) + if not names: + raise ValueError("did not find new %r hooks in %r" + %(self._prefix, module_or_class)) def getplugins(self): - return self._plugins + self._conftestplugins + """ return the complete list of registered plugins. NOTE that + you will get the internal list and need to make a copy if you + modify the list.""" + return self._plugins - def skipifmissing(self, name): - if not self.hasplugin(name): - import pytest - pytest.skip("plugin %r is missing" % name) + def isregistered(self, plugin): + """ Return True if the plugin is already registered under its + canonical name. """ + return self.hasplugin(self._get_canonical_name(plugin)) or \ + plugin in self._plugins def hasplugin(self, name): - return bool(self.getplugin(name)) + """ Return True if there is a registered with the given name. """ + return name in self._name2plugin def getplugin(self, name): - if name is None: - return None - try: - return self._name2plugin[name] - except KeyError: - return self._name2plugin.get("_pytest." + name, None) - - # API for bootstrapping - # - def _envlist(self, varname): - val = os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) - - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.consider_pluginarg(opt2) - - def consider_pluginarg(self, arg): - if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 - else: - if self.getplugin(arg) is None: - self.import_plugin(arg) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): - self.consider_module(conftestmodule) - - def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) - - def import_plugin(self, modname): - assert isinstance(modname, str) - if self.getplugin(modname) is not None: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except ImportError: - if modname.startswith("pytest_"): - return self.import_plugin(modname[7:]) - raise - except: - e = sys.exc_info()[1] - import pytest - if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): - raise - self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) + """ Return a plugin or None for the given name. """ + return self._name2plugin.get(name) def listattr(self, attrname, plugins=None): if plugins is None: - plugins = self._plugins + self._conftestplugins + plugins = self._plugins l = [] last = [] wrappers = [] @@ -355,20 +281,43 @@ l.extend(wrappers) return l + def _scan_methods(self, hookcaller): + hookcaller.methods = self.listattr(hookcaller.name) + def call_plugin(self, plugin, methname, kwargs): return MultiCall(methods=self.listattr(methname, plugins=[plugin]), kwargs=kwargs, firstresult=True).execute() -def importplugin(importspec): - name = importspec - try: - mod = "_pytest." + name - __import__(mod) - return sys.modules[mod] - except ImportError: - __import__(importspec) - return sys.modules[importspec] + def _scan_plugin(self, plugin): + def fail(msg, *args): + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) + + for name in dir(plugin): + if name[0] == "_" or not name.startswith(self._prefix): + continue + hook = getattr(self.hook, name, None) + method = getattr(plugin, name) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + if getattr(method, 'optionalhook', False): + continue + fail("found unknown hook: %r", name) + for arg in varnames(method): + if arg not in hook.argnames: + fail("argument %r not available\n" + "actual definition: %s\n" + "available hookargs: %s", + arg, formatdef(method), + ", ".join(hook.argnames)) + yield hook + + def _get_canonical_name(self, plugin): + return getattr(plugin, "__name__", None) or str(id(plugin)) + + class MultiCall: """ execute a call into multiple python functions/methods. """ @@ -441,65 +390,13 @@ class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] + def __init__(self, pm): self._pm = pm self.trace = pm.trace.root.get("hook") - self.prefix = prefix - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspec, prefix): - added = False - isclass = int(inspect.isclass(hookspec)) - for name, method in vars(hookspec).items(): - if name.startswith(prefix): - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self, name, hc) - added = True - #print ("setting new hook", name) - if not added: - raise ValueError("did not find new %r hooks in %r" %( - prefix, hookspec,)) - - def _getcaller(self, name, plugins): - caller = getattr(self, name) - methods = self._pm.listattr(name, plugins=plugins) - if methods: - return caller.new_cached_caller(methods) - return caller - - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - - for name in dir(plugin): - if not name.startswith(self.prefix): - continue - hook = getattr(self, name, None) - method = getattr(plugin, name) - if hook is None: - is_optional = getattr(method, 'optionalhook', False) - if not isgenerichook(name) and not is_optional: - fail("found unknown hook: %r", name) - continue - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook class HookCaller: - def __init__(self, hookrelay, name, firstresult, argnames, methods=()): - self.hookrelay = hookrelay + def __init__(self, name, firstresult, argnames, methods=()): self.name = name self.firstresult = firstresult self.argnames = ["__multicall__"] @@ -507,16 +404,9 @@ assert "self" not in argnames # sanity check self.methods = methods - def new_cached_caller(self, methods): - return HookCaller(self.hookrelay, self.name, self.firstresult, - argnames=self.argnames, methods=methods) - def __repr__(self): return "" %(self.name,) - def scan_methods(self): - self.methods = self.hookrelay._pm.listattr(self.name) - def __call__(self, **kwargs): return self._docall(self.methods, kwargs) @@ -531,13 +421,9 @@ class PluginValidationError(Exception): """ plugin failed validation. """ -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") def formatdef(func): return "%s%s" % ( func.__name__, inspect.formatargspec(*inspect.getargspec(func)) ) - diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/doctest.py --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -132,7 +132,7 @@ def collect(self): import doctest if self.fspath.basename == "conftest.py": - module = self.config._conftest.importconftest(self.fspath) + module = self.config._conftest._importconftest(self.fspath) else: try: module = self.fspath.pyimport() diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -28,24 +28,20 @@ config = outcome.get_result() if config.option.debug: path = os.path.abspath("pytestdebug.log") - f = open(path, 'w') - config._debugfile = f - f.write("versions pytest-%s, py-%s, " + debugfile = open(path, 'w') + debugfile.write("versions pytest-%s, py-%s, " "python-%s\ncwd=%s\nargs=%s\n\n" %( pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(f.write) + config.pluginmanager.set_tracing(debugfile.write) sys.stderr.write("writing pytestdebug information to %s\n" % path) - - at pytest.mark.trylast -def pytest_unconfigure(config): - if hasattr(config, '_debugfile'): - config._debugfile.close() - sys.stderr.write("wrote pytestdebug information to %s\n" % - config._debugfile.name) - config.trace.root.setwriter(None) - + def unset_tracing(): + debugfile.close() + sys.stderr.write("wrote pytestdebug information to %s\n" % + debugfile.name) + config.trace.root.setwriter(None) + config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): if config.option.version: @@ -58,9 +54,9 @@ sys.stderr.write(line + "\n") return 0 elif config.option.help: - config.do_configure() + config._do_configure() showhelp(config) - config.do_unconfigure() + config._ensure_unconfigure() return 0 def showhelp(config): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -6,7 +6,7 @@ def pytest_addhooks(pluginmanager): """called at plugin load time to allow adding new hooks via a call to - pluginmanager.registerhooks(module).""" + pluginmanager.addhooks(module_or_class, prefix).""" def pytest_namespace(): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -77,7 +77,7 @@ initstate = 0 try: try: - config.do_configure() + config._do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) initstate = 2 @@ -107,9 +107,7 @@ config.hook.pytest_sessionfinish( session=session, exitstatus=session.exitstatus) - if initstate >= 1: - config.do_unconfigure() - config.pluginmanager.ensure_shutdown() + config._ensure_unconfigure() return session.exitstatus def pytest_cmdline_main(config): @@ -160,7 +158,7 @@ def __getattr__(self, name): plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.hook._getcaller(name, plugins) + x = self.config.pluginmanager.make_hook_caller(name, plugins) self.__dict__[name] = x return x @@ -510,7 +508,7 @@ def __init__(self, config): FSCollector.__init__(self, config.rootdir, parent=None, config=config, session=self) - self.config.pluginmanager.register(self, name="session", prepend=True) + self.config.pluginmanager.register(self, name="session") self._testsfailed = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") @@ -521,10 +519,12 @@ def _makeid(self): return "" + @pytest.mark.tryfirst def pytest_collectstart(self): if self.shouldstop: raise self.Interrupted(self.shouldstop) + @pytest.mark.tryfirst def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): self._testsfailed += 1 diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/mark.py --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -44,14 +44,14 @@ def pytest_cmdline_main(config): if config.option.markers: - config.do_configure() + config._do_configure() tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() - config.do_unconfigure() + config._ensure_unconfigure() return 0 pytest_cmdline_main.tryfirst = True diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -83,7 +83,8 @@ self.calls.append(ParsedCall(hookcaller.name, kwargs)) yield self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - pluginmanager.add_shutdown(self._undo_wrapping) + #if hasattr(pluginmanager, "config"): + # pluginmanager.add_shutdown(self._undo_wrapping) def finish_recording(self): self._undo_wrapping() @@ -589,12 +590,7 @@ # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) - def ensure_unconfigure(): - if hasattr(config.pluginmanager, "_config"): - config.pluginmanager.do_unconfigure(config) - config.pluginmanager.ensure_shutdown() - - self.request.addfinalizer(ensure_unconfigure) + self.request.addfinalizer(config._ensure_unconfigure) return config def parseconfigure(self, *args): @@ -606,8 +602,8 @@ """ config = self.parseconfig(*args) - config.do_configure() - self.request.addfinalizer(config.do_unconfigure) + config._do_configure() + self.request.addfinalizer(config._ensure_unconfigure) return config def getitem(self, source, funcname="test_func"): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/conftest.py --- a/testing/conftest.py +++ b/testing/conftest.py @@ -66,6 +66,7 @@ error.append(error[0]) raise AssertionError("\n".join(error)) + at pytest.mark.trylast def pytest_runtest_teardown(item, __multicall__): item.config._basedir.chdir() if hasattr(item.config, '_openfiles'): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1487,7 +1487,7 @@ reprec = testdir.inline_run("-v","-s") reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config - l = config._conftest.getconftestmodules(p)[0].l + l = config.pluginmanager._getconftestmodules(p)[0].l assert l == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 def test_scope_ordering(self, testdir): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/test_conftest.py --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,7 +1,6 @@ from textwrap import dedent import py, pytest -from _pytest.config import Conftest - +from _pytest.config import PytestPluginManager @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -16,7 +15,7 @@ return tmpdir def ConftestWithSetinitial(path): - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) return conftest @@ -25,51 +24,41 @@ def __init__(self): self.file_or_dir = args self.confcutdir = str(confcutdir) - conftest.setinitial(Namespace()) + conftest._set_initial_conftests(Namespace()) class TestConftestValueAccessGlobal: def test_basic_init(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() p = basedir.join("adir") - assert conftest.rget_with_confmod("a", p)[1] == 1 - - def test_onimport(self, basedir): - l = [] - conftest = Conftest(onimport=l.append) - adir = basedir.join("adir") - conftest_setinitial(conftest, [adir], confcutdir=basedir) - assert len(l) == 1 - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("b", adir.join("b"))[1] == 2 - assert len(l) == 2 + assert conftest._rget_with_confmod("a", p)[1] == 1 def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() len(conftest._path2confmods) - conftest.getconftestmodules(basedir) + conftest._getconftestmodules(basedir) snap1 = len(conftest._path2confmods) #assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('adir')) + conftest._getconftestmodules(basedir.join('adir')) assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('b')) + conftest._getconftestmodules(basedir.join('b')) assert len(conftest._path2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): - conftest.rget_with_confmod('a', basedir) + conftest._rget_with_confmod('a', basedir) def test_value_access_by_path(self, basedir): conftest = ConftestWithSetinitial(basedir) adir = basedir.join("adir") - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("a", adir.join("b"))[1] == 1.5 + assert conftest._rget_with_confmod("a", adir)[1] == 1 + assert conftest._rget_with_confmod("a", adir.join("b"))[1] == 1.5 def test_value_access_with_confmod(self, basedir): startdir = basedir.join("adir", "b") startdir.ensure("xx", dir=True) conftest = ConftestWithSetinitial(startdir) - mod, value = conftest.rget_with_confmod("a", startdir) + mod, value = conftest._rget_with_confmod("a", startdir) assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") @@ -85,9 +74,9 @@ def test_doubledash_considered(testdir): conf = testdir.mkdir("--option") conf.join("conftest.py").ensure() - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.basename, conf.basename]) - l = conftest.getconftestmodules(conf) + l = conftest._getconftestmodules(conf) assert len(l) == 1 def test_issue151_load_all_conftests(testdir): @@ -96,7 +85,7 @@ p = testdir.mkdir(name) p.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, names) d = list(conftest._conftestpath2mod.values()) assert len(d) == len(names) @@ -105,15 +94,15 @@ testdir.makeconftest("x=3") p = testdir.makepyfile(""" import py, pytest - from _pytest.config import Conftest - conf = Conftest() - mod = conf.importconftest(py.path.local("conftest.py")) + from _pytest.config import PytestPluginManager + conf = PytestPluginManager() + mod = conf._importconftest(py.path.local("conftest.py")) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) subconf = py.path.local().ensure("sub", "conftest.py") subconf.write("y=4") - mod2 = conf.importconftest(subconf) + mod2 = conf._importconftest(subconf) assert mod != mod2 assert mod2.y == 4 import conftest @@ -125,27 +114,27 @@ def test_conftestcutdir(testdir): conf = testdir.makeconftest("") p = testdir.mkdir("x") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p) - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 0 - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 0 assert conf not in conftest._conftestpath2mod # but we can still import a conftest directly - conftest.importconftest(conf) - l = conftest.getconftestmodules(conf.dirpath()) + conftest._importconftest(conf) + l = conftest._getconftestmodules(conf.dirpath()) assert l[0].__file__.startswith(str(conf)) # and all sub paths get updated properly - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) def test_conftestcutdir_inplace_considered(testdir): conf = testdir.makeconftest("") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) @@ -153,7 +142,7 @@ def test_setinitial_conftest_subdirs(testdir, name): sub = testdir.mkdir(name) subconftest = sub.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ('whatever', '.dotdir'): assert subconftest in conftest._conftestpath2mod @@ -199,9 +188,9 @@ ct2.write("") def impct(p): return p - conftest = Conftest() - monkeypatch.setattr(conftest, 'importconftest', impct) - assert conftest.getconftestmodules(sub) == [ct1, ct2] + conftest = PytestPluginManager() + monkeypatch.setattr(conftest, '_importconftest', impct) + assert conftest._getconftestmodules(sub) == [ct1, ct2] def test_fixture_dependency(testdir, monkeypatch): diff -r 34ec01b366b95afec4c4928b21e2020389a35bee -r 90f9b67b555f24f26798193206f58930f2ea1306 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -3,234 +3,48 @@ from _pytest.config import get_plugin_manager -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - pytest.raises(ImportError, lambda: pluginmanager.consider_env()) + at pytest.fixture +def pm(): + return PluginManager("he") - def test_preparse_args(self): - pluginmanager = PluginManager() - pytest.raises(ImportError, lambda: - pluginmanager.consider_preparse(["xyz", "-p", "hello123"])) + at pytest.fixture +def pytestpm(): + return PytestPluginManager() - def test_plugin_prevent_register(self): - pluginmanager = PluginManager() - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l1 = pluginmanager.getplugins() - pluginmanager.register(42, name="abc") - l2 = pluginmanager.getplugins() - assert len(l2) == len(l1) - def test_plugin_prevent_register_unregistered_alredy_registered(self): - pluginmanager = PluginManager() - pluginmanager.register(42, name="abc") - l1 = pluginmanager.getplugins() - assert 42 in l1 - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l2 = pluginmanager.getplugins() - assert 42 not in l2 +class TestPluginManager: + def test_plugin_double_register(self, pm): + pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="abc") - def test_plugin_double_register(self): - pm = PluginManager() - pm.register(42, name="abc") - pytest.raises(ValueError, lambda: pm.register(42, name="abc")) - - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(skipping1=""" - import pytest - pytest.skip("hello") - """) - p.copy(p.dirpath("skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "WI1*skipped plugin*skipping1*hello*", - "WI1*skipped plugin*skipping2*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(xy123="#") - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pluginmanager.getplugins()) - pluginmanager.consider_env() - l2 = len(pluginmanager.getplugins()) - assert l2 == l1 + 1 - assert pluginmanager.getplugin('xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = pytest.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "pytest_mytestplugin" - dist = None - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - plugin = pluginmanager.getplugin("mytestplugin") - assert plugin.x == 42 - - def test_consider_setuptools_not_installed(self, monkeypatch): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - # ok, we did not explode - - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import pytest - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') - assert plugin is not None - """) - monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) - - def test_import_plugin_importname(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("pytest_hello") - len1 = len(pluginmanager.getplugins()) - pluginmanager.import_plugin("pytest_hello") - len2 = len(pluginmanager.getplugins()) - assert len1 == len2 - plugin1 = pluginmanager.getplugin("pytest_hello") - assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("pytest_hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") - pluginname = "pkg.plug" - pluginmanager.import_plugin(pluginname) - mod = pluginmanager.getplugin("pkg.plug") - assert mod.x == 3 - - def test_consider_module(self, testdir): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_p1="#") - testdir.makepyfile(pytest_p2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_p1", "pytest_p2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("pytest_p1").__name__ == "pytest_p1" - assert pluginmanager.getplugin("pytest_p2").__name__ == "pytest_p2" - - def test_consider_module_import_module(self, testdir): - mod = py.std.types.ModuleType("x") - mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = get_plugin_manager() - reprec = testdir.make_hook_recorder(pluginmanager) - #syspath.prepend(aplugin.dirpath()) - py.std.sys.path.insert(0, str(aplugin.dirpath())) - pluginmanager.consider_module(mod) - call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__name__ == "pytest_a" - - # check that it is not registered twice - pluginmanager.consider_module(mod) - l = reprec.getcalls("pytest_plugin_registered") - assert len(l) == 1 - - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - - def test_consider_conftest_deps(self, testdir): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() - pp = PluginManager() - pytest.raises(ImportError, lambda: pp.consider_conftest(mod)) - - def test_pm(self): - pp = PluginManager() + def test_pm(self, pm): class A: pass a1, a2 = A(), A() - pp.register(a1) - assert pp.isregistered(a1) - pp.register(a2, "hello") - assert pp.isregistered(a2) - l = pp.getplugins() + pm.register(a1) + assert pm.isregistered(a1) + pm.register(a2, "hello") + assert pm.isregistered(a2) + l = pm.getplugins() assert a1 in l assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) - - def test_pm_ordering(self): - pp = PluginManager() - class A: pass - a1, a2 = A(), A() - pp.register(a1) - pp.register(a2, "hello") - l = pp.getplugins() - assert l.index(a1) < l.index(a2) - a3 = A() - pp.register(a3, prepend=True) - l = pp.getplugins() - assert l.index(a3) == 0 - - def test_register_imported_modules(self): - pp = PluginManager() - mod = py.std.types.ModuleType("x.y.pytest_hello") - pp.register(mod) - assert pp.isregistered(mod) - l = pp.getplugins() - assert mod in l - pytest.raises(ValueError, "pp.register(mod)") - pytest.raises(ValueError, lambda: pp.register(mod)) - #assert not pp.isregistered(mod2) - assert pp.getplugins() == l - - def test_canonical_import(self, monkeypatch): - mod = py.std.types.ModuleType("pytest_xyz") - monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) - pp = PluginManager() - pp.import_plugin('pytest_xyz') - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) + assert pm.getplugin('hello') == a2 + pm.unregister(a1) + assert not pm.isregistered(a1) def test_register_mismatch_method(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_gurgel(self): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register_mismatch_arg(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_configure(self, asd): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register(self): pm = get_plugin_manager() @@ -250,7 +64,7 @@ assert pm.getplugins()[-1:] == [my2] def test_listattr(self): - plugins = PluginManager() + plugins = PluginManager("xyz") class api1: x = 41 class api2: @@ -263,27 +77,6 @@ l = list(plugins.listattr('x')) assert l == [41, 42, 43] - def test_hook_tracing(self): - pm = get_plugin_manager() - saveindent = [] - class api1: - x = 41 - def pytest_plugin_registered(self, plugin): - saveindent.append(pm.trace.root.indent) - raise ValueError(42) - l = [] - pm.set_tracing(l.append) - indent = pm.trace.root.indent - p = api1() - pm.register(p) - - assert pm.trace.root.indent == indent - assert len(l) == 2 - assert 'pytest_plugin_registered' in l[0] - assert 'finish' in l[1] - pytest.raises(ValueError, lambda: pm.register(api1())) - assert pm.trace.root.indent == indent - assert saveindent[0] > indent class TestPytestPluginInteractions: @@ -301,7 +94,7 @@ return xyz + 1 """) config = get_plugin_manager().config - config._conftest.importconftest(conf) + config.pluginmanager._importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] @@ -350,7 +143,7 @@ parser.addoption('--test123', action="store_true", default=True) """) - config._conftest.importconftest(p) + config.pluginmanager._importconftest(p) assert config.option.test123 def test_configure(self, testdir): @@ -362,20 +155,43 @@ config.pluginmanager.register(A()) assert len(l) == 0 - config.do_configure() + config._do_configure() assert len(l) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] - config.do_unconfigure() + config._ensure_unconfigure() config.pluginmanager.register(A()) assert len(l) == 2 + def test_hook_tracing(self): + pytestpm = get_plugin_manager() # fully initialized with plugins + saveindent = [] + class api1: + x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pytestpm.trace.root.indent) + raise ValueError(42) + l = [] + pytestpm.set_tracing(l.append) + indent = pytestpm.trace.root.indent + p = api1() + pytestpm.register(p) + + assert pytestpm.trace.root.indent == indent + assert len(l) == 2 + assert 'pytest_plugin_registered' in l[0] + assert 'finish' in l[1] + with pytest.raises(ValueError): + pytestpm.register(api1()) + assert pytestpm.trace.root.indent == indent + assert saveindent[0] > indent + # lower level API def test_listattr(self): - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") class My2: x = 42 pluginmanager.register(My2()) @@ -395,7 +211,7 @@ def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -572,7 +388,7 @@ def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -624,11 +440,12 @@ class TestHookRelay: - def test_happypath(self): + def test_hapmypath(self): class Api: def hello(self, arg): "api hook 1" - pm = PluginManager([Api], prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) hook = pm.hook assert hasattr(hook, 'hello') assert repr(hook.hello).find("hello") != -1 @@ -647,7 +464,8 @@ class Api: def hello(self, arg): "api hook 1" - pm = PluginManager(Api, prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, argwrong): return arg + 1 @@ -656,19 +474,20 @@ assert "argwrong" in str(exc.value) def test_only_kwargs(self): - pm = PluginManager() + pm = PluginManager("he") class Api: def hello(self, arg): "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - pytest.raises(TypeError, lambda: mcm.hello(3)) + pm.addhooks(Api) + pytest.raises(TypeError, lambda: pm.hook.hello(3)) def test_firstresult_definition(self): class Api: def hello(self, arg): "api hook 1" hello.firstresult = True - pm = PluginManager([Api], "he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, arg): return arg + 1 @@ -771,15 +590,16 @@ "*trylast*last*", ]) -def test_importplugin_issue375(testdir): +def test_importplugin_issue375(testdir, pytestpm): testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe="import aaaa") - excinfo = pytest.raises(ImportError, lambda: importplugin("qwe")) + with pytest.raises(ImportError) as excinfo: + pytestpm.import_plugin("qwe") assert "qwe" not in str(excinfo.value) assert "aaaa" in str(excinfo.value) class TestWrapMethod: - def test_basic_happypath(self): + def test_basic_hapmypath(self): class A: def f(self): return "A.f" @@ -880,3 +700,178 @@ with pytest.raises(ValueError): A().error() assert l == [1] + + +### to be shifted to own test file +from _pytest.config import PytestPluginManager + +class TestPytestPluginManager: + def test_register_imported_modules(self): + pm = PytestPluginManager() + mod = py.std.types.ModuleType("x.y.pytest_hello") + pm.register(mod) + assert pm.isregistered(mod) + l = pm.getplugins() + assert mod in l + pytest.raises(ValueError, "pm.register(mod)") + pytest.raises(ValueError, lambda: pm.register(mod)) + #assert not pm.isregistered(mod2) + assert pm.getplugins() == l + + def test_canonical_import(self, monkeypatch): + mod = py.std.types.ModuleType("pytest_xyz") + monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) + pm = PytestPluginManager() + pm.import_plugin('pytest_xyz') + assert pm.getplugin('pytest_xyz') == mod + assert pm.isregistered(mod) + + def test_consider_module(self, testdir, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(pytest_p1="#") + testdir.makepyfile(pytest_p2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_p1", "pytest_p2"] + pytestpm.consider_module(mod) + assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1" + assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" + + def test_consider_module_import_module(self, testdir): + pytestpm = get_plugin_manager() + mod = py.std.types.ModuleType("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="#") + reprec = testdir.make_hook_recorder(pytestpm) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + pytestpm.consider_module(mod) + call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name) + assert call.plugin.__name__ == "pytest_a" + + # check that it is not registered twice + pytestpm.consider_module(mod) + l = reprec.getcalls("pytest_plugin_registered") + assert len(l) == 1 + + def test_consider_env_fails_to_import(self, monkeypatch, pytestpm): + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + with pytest.raises(ImportError): + pytestpm.consider_env() + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(skipping1=""" + import pytest + pytest.skip("hello") + """) + p.copy(p.dirpath("skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "WI1*skipped plugin*skipping1*hello*", + "WI1*skipped plugin*skipping2*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(xy123="#") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(pytestpm.getplugins()) + pytestpm.consider_env() + l2 = len(pytestpm.getplugins()) + assert l2 == l1 + 1 + assert pytestpm.getplugin('xy123') + pytestpm.consider_env() + l3 = len(pytestpm.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm): + pkg_resources = pytest.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "pytest_mytestplugin" + dist = None + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pytestpm.consider_setuptools_entrypoints() + plugin = pytestpm.getplugin("mytestplugin") + assert plugin.x == 42 + + def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + pytestpm.consider_setuptools_entrypoints() + # ok, we did not explode + + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): + testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import pytest + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') + assert plugin is not None + """) + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") + result = testdir.runpytest(p) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwx.y")') + + testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pytestpm.import_plugin("pytest_hello") + len1 = len(pytestpm.getplugins()) + pytestpm.import_plugin("pytest_hello") + len2 = len(pytestpm.getplugins()) + assert len1 == len2 + plugin1 = pytestpm.getplugin("pytest_hello") + assert plugin1.__name__.endswith('pytest_hello') + plugin2 = pytestpm.getplugin("pytest_hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwex.y")') + + testdir.syspathinsert() + testdir.mkpydir("pkg").join("plug.py").write("x=3") + pluginname = "pkg.plug" + pytestpm.import_plugin(pluginname) + mod = pytestpm.getplugin("pkg.plug") + assert mod.x == 3 + + def test_consider_conftest_deps(self, testdir, pytestpm): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + with pytest.raises(ImportError): + pytestpm.consider_conftest(mod) + + +class TestPytestPluginManagerBootstrapming: + def test_preparse_args(self, pytestpm): + pytest.raises(ImportError, lambda: + pytestpm.consider_preparse(["xyz", "-p", "hello123"])) + + def test_plugin_prevent_register(self, pytestpm): + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l1 = pytestpm.getplugins() + pytestpm.register(42, name="abc") + l2 = pytestpm.getplugins() + assert len(l2) == len(l1) + + def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): + pytestpm.register(42, name="abc") + l1 = pytestpm.getplugins() + assert 42 in l1 + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l2 = pytestpm.getplugins() + assert 42 not in l2 This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Mon Apr 27 14:17:46 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 27 Apr 2015 12:17:46 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merged in hpk42/pytest-patches/more_plugin (pull request #282) Message-ID: <20150427121746.18238.65755@app06.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/24f4d48abeeb/ Changeset: 24f4d48abeeb User: flub Date: 2015-04-27 12:17:40+00:00 Summary: Merged in hpk42/pytest-patches/more_plugin (pull request #282) another major pluginmanager refactor and docs Affected #: 27 files diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,23 @@ change but it might still break 3rd party plugins which relied on details like especially the pluginmanager.add_shutdown() API. Thanks Holger Krekel. + +- pluginmanagement: introduce ``pytest.hookimpl_opts`` and + ``pytest.hookspec_opts`` decorators for setting impl/spec + specific parameters. This substitutes the previous + now deprecated use of ``pytest.mark`` which is meant to + contain markers for test functions only. + +- write/refine docs for "writing plugins" which now have their + own page and are separate from the "using/installing plugins`` page. + +- fix issue732: properly unregister plugins from any hook calling + sites allowing to have temporary plugins during test execution. + +- deprecate and warn about ``__multicall__`` argument in hook + implementations. Use the ``hookwrapper`` mechanism instead already + introduced with pytest-2.7. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -29,7 +29,7 @@ help="shortcut for --capture=no.") - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_load_initial_conftests(early_config, parser, args): ns = early_config.known_args_namespace pluginmanager = early_config.pluginmanager @@ -101,7 +101,7 @@ if capfuncarg is not None: capfuncarg.close() - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_make_collect_report(self, collector): if isinstance(collector, pytest.File): self.resumecapture() @@ -115,13 +115,13 @@ else: yield - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_setup(self, item): self.resumecapture() yield self.suspendcapture_item(item, "setup") - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_call(self, item): self.resumecapture() self.activate_funcargs(item) @@ -129,17 +129,17 @@ #self.deactivate_funcargs() called from suspendcapture() self.suspendcapture_item(item, "call") - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_teardown(self, item): self.resumecapture() yield self.suspendcapture_item(item, "teardown") - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_keyboard_interrupt(self, excinfo): self.reset_capturings() - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_internalerror(self, excinfo): self.reset_capturings() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -9,7 +9,7 @@ # DON't import pytest here because it causes import cycle troubles import sys, os from _pytest import hookspec # the extension point definitions -from _pytest.core import PluginManager +from _pytest.core import PluginManager, hookimpl_opts, varnames # pytest startup # @@ -38,6 +38,7 @@ tw.line("ERROR: could not load %s\n" % (e.path), red=True) return 4 else: + config.pluginmanager.check_pending() return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace @@ -59,17 +60,17 @@ def _preloadplugins(): assert not _preinit - _preinit.append(get_plugin_manager()) + _preinit.append(get_config()) -def get_plugin_manager(): +def get_config(): if _preinit: return _preinit.pop(0) # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - pluginmanager.config = Config(pluginmanager) # XXX attr needed? + config = Config(pluginmanager) for spec in default_plugins: pluginmanager.import_plugin(spec) - return pluginmanager + return config def _prepareconfig(args=None, plugins=None): if args is None: @@ -80,7 +81,7 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_plugin_manager() + pluginmanager = get_config().pluginmanager if plugins: for plugin in plugins: pluginmanager.register(plugin) @@ -97,8 +98,7 @@ super(PytestPluginManager, self).__init__(prefix="pytest_", excludefunc=exclude_pytest_names) self._warnings = [] - self._plugin_distinfo = [] - self._globalplugins = [] + self._conftest_plugins = set() # state related to local conftest plugins self._path2confmods = {} @@ -114,28 +114,35 @@ err = py.io.dupfile(err, encoding=encoding) except Exception: pass - self.set_tracing(err.write) + self.trace.root.setwriter(err.write) + self.enable_tracing() - def register(self, plugin, name=None, conftest=False): + + def _verify_hook(self, hook, plugin): + super(PytestPluginManager, self)._verify_hook(hook, plugin) + method = getattr(plugin, hook.name) + if "__multicall__" in varnames(method): + fslineno = py.code.getfslineno(method) + warning = dict(code="I1", + fslocation=fslineno, + message="%r hook uses deprecated __multicall__ " + "argument" % (hook.name)) + self._warnings.append(warning) + + def register(self, plugin, name=None): ret = super(PytestPluginManager, self).register(plugin, name) - if ret and not conftest: - self._globalplugins.append(plugin) + if ret: + self.hook.pytest_plugin_registered.call_historic( + kwargs=dict(plugin=plugin, manager=self)) return ret - def _do_register(self, plugin, name): - # called from core PluginManager class - if hasattr(self, "config"): - self.config._register_plugin(plugin, name) - return super(PytestPluginManager, self)._do_register(plugin, name) - - def unregister(self, plugin): - super(PytestPluginManager, self).unregister(plugin) - try: - self._globalplugins.remove(plugin) - except ValueError: - pass + def getplugin(self, name): + # support deprecated naming because plugins (xdist e.g.) use it + return self.get_plugin(name) def pytest_configure(self, config): + # XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...) + # we should remove tryfirst/trylast as markers config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " "plugin machinery will try to call it first/as early as possible.") @@ -143,7 +150,10 @@ "trylast: mark a hook implementation function such that the " "plugin machinery will try to call it last/as late as possible.") for warning in self._warnings: - config.warn(code="I1", message=warning) + if isinstance(warning, dict): + config.warn(**warning) + else: + config.warn(code="I1", message=warning) # # internal API for local conftest plugin handling @@ -186,14 +196,21 @@ try: return self._path2confmods[path] except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self._importconftest(conftestpath) - clist.append(mod) + if path.isfile(): + clist = self._getconftestmodules(path.dirpath()) + else: + # XXX these days we may rather want to use config.rootdir + # and allow users to opt into looking into the rootdir parent + # directories instead of requiring to specify confcutdir + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._path2confmods[path] = clist return clist @@ -217,6 +234,8 @@ mod = conftestpath.pyimport() except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) + + self._conftest_plugins.add(mod) self._conftestpath2mod[conftestpath] = mod dirpath = conftestpath.dirpath() if dirpath in self._path2confmods: @@ -233,24 +252,6 @@ # # - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) - def consider_preparse(self, args): for opt1,opt2 in zip(args, args[1:]): if opt1 == "-p": @@ -258,18 +259,12 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 + self.set_blocked(arg[3:]) else: - if self.getplugin(arg) is None: - self.import_plugin(arg) + self.import_plugin(arg) def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): + if self.register(conftestmodule, name=conftestmodule.__file__): self.consider_module(conftestmodule) def consider_env(self): @@ -291,7 +286,7 @@ # basename for historic purposes but must be imported with the # _pytest prefix. assert isinstance(modname, str) - if self.getplugin(modname) is not None: + if self.get_plugin(modname) is not None: return if modname in builtin_plugins: importspec = "_pytest." + modname @@ -685,6 +680,7 @@ notset = Notset() FILE_OR_DIR = 'file_or_dir' + class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ @@ -706,20 +702,11 @@ self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - - def _register_plugin(self, plugin, name): - call_plugin = self.pluginmanager.call_plugin - call_plugin(plugin, "pytest_addhooks", - {'pluginmanager': self.pluginmanager}) - self.hook.pytest_plugin_registered(plugin=plugin, - manager=self.pluginmanager) - dic = call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: + def do_setns(dic): import pytest setns(pytest, dic) - call_plugin(plugin, "pytest_addoption", {'parser': self._parser}) - if self._configured: - call_plugin(plugin, "pytest_configure", {'config': self}) + self.hook.pytest_namespace.call_historic(do_setns, {}) + self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) def add_cleanup(self, func): """ Add a function to be called when the config object gets out of @@ -729,26 +716,27 @@ def _do_configure(self): assert not self._configured self._configured = True - self.hook.pytest_configure(config=self) + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) def _ensure_unconfigure(self): if self._configured: self._configured = False self.hook.pytest_unconfigure(config=self) + self.hook.pytest_configure._call_history = [] while self._cleanup: fin = self._cleanup.pop() fin() - def warn(self, code, message): + def warn(self, code, message, fslocation=None): """ generate a warning for this test session. """ self.hook.pytest_logwarning(code=code, message=message, - fslocation=None, nodeid=None) + fslocation=fslocation, nodeid=None) def get_terminal_writer(self): - return self.pluginmanager.getplugin("terminalreporter")._tw + return self.pluginmanager.get_plugin("terminalreporter")._tw def pytest_cmdline_parse(self, pluginmanager, args): - assert self == pluginmanager.config, (self, pluginmanager.config) + # REF1 assert self == pluginmanager.config, (self, pluginmanager.config) self.parse(args) return self @@ -778,8 +766,7 @@ @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ - pluginmanager = get_plugin_manager() - config = pluginmanager.config + config = get_config() config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) for x in config.option.plugins: @@ -794,13 +781,9 @@ if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) - def _getmatchingplugins(self, fspath): - return self.pluginmanager._globalplugins + \ - self.pluginmanager._getconftestmodules(fspath) - + @hookimpl_opts(trylast=True) def pytest_load_initial_conftests(self, early_config): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - pytest_load_initial_conftests.trylast = True def _initini(self, args): parsed_args = self._parser.parse_known_args(args) @@ -817,7 +800,10 @@ args[:] = self.getini("addopts") + args self._checkversion() self.pluginmanager.consider_preparse(args) - self.pluginmanager.consider_setuptools_entrypoints() + try: + self.pluginmanager.load_setuptools_entrypoints("pytest11") + 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) try: @@ -850,6 +836,8 @@ 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) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -2,11 +2,65 @@ PluginManager, basic initialization and tracing. """ import sys -import inspect +from inspect import isfunction, ismethod, isclass, formatargspec, getargspec import py py3 = sys.version_info > (3,0) +def hookspec_opts(firstresult=False, historic=False): + """ returns a decorator which will define a function as a hook specfication. + + If firstresult is True the 1:N hook call (N being the number of registered + hook implementation functions) will stop at I<=N when the I'th function + returns a non-None result. + + If historic is True calls to a hook will be memorized and replayed + on later registered plugins. + """ + def setattr_hookspec_opts(func): + if historic and firstresult: + raise ValueError("cannot have a historic firstresult hook") + if firstresult: + func.firstresult = firstresult + if historic: + func.historic = historic + return func + return setattr_hookspec_opts + + +def hookimpl_opts(hookwrapper=False, optionalhook=False, + tryfirst=False, trylast=False): + """ Return a decorator which marks a function as a hook implementation. + + If optionalhook is True a missing matching hook specification will not result + in an error (by default it is an error if no matching spec is found). + + If tryfirst is True this hook implementation will run as early as possible + in the chain of N hook implementations for a specfication. + + If trylast is True this hook implementation will run as late as possible + in the chain of N hook implementations. + + If hookwrapper is True the hook implementations needs to execute exactly + one "yield". The code before the yield is run early before any non-hookwrapper + function is run. The code after the yield is run after all non-hookwrapper + function have run. The yield receives an ``CallOutcome`` object representing + the exception or result outcome of the inner calls (including other hookwrapper + calls). + """ + def setattr_hookimpl_opts(func): + if hookwrapper: + func.hookwrapper = True + if optionalhook: + func.optionalhook = True + if tryfirst: + func.tryfirst = True + if trylast: + func.trylast = True + return func + return setattr_hookimpl_opts + + class TagTracer: def __init__(self): self._tag2proc = {} @@ -53,42 +107,28 @@ assert isinstance(tags, tuple) self._tag2proc[tags] = processor + class TagTracerSub: def __init__(self, root, tags): self.root = root self.tags = tags + def __call__(self, *args): self.root.processmessage(self.tags, args) + def setmyprocessor(self, processor): self.root.setprocessor(self.tags, processor) + def get(self, name): return self.__class__(self.root, self.tags + (name,)) -def add_method_wrapper(cls, wrapper_func): - """ Substitute the function named "wrapperfunc.__name__" at class - "cls" with a function that wraps the call to the original function. - Return an undo function which can be called to reset the class to use - the old method again. - - wrapper_func is called with the same arguments as the method - it wraps and its result is used as a wrap_controller for - calling the original function. - """ - name = wrapper_func.__name__ - oldcall = getattr(cls, name) - def wrap_exec(*args, **kwargs): - gen = wrapper_func(*args, **kwargs) - return wrapped_call(gen, lambda: oldcall(*args, **kwargs)) - - setattr(cls, name, wrap_exec) - return lambda: setattr(cls, name, oldcall) - def raise_wrapfail(wrap_controller, msg): co = wrap_controller.gi_code raise RuntimeError("wrap_controller at %r %s:%d %s" % (co.co_name, co.co_filename, co.co_firstlineno, msg)) + def wrapped_call(wrap_controller, func): """ Wrap calling to a function with a generator which needs to yield exactly once. The yield point will trigger calling the wrapped function @@ -133,6 +173,25 @@ py.builtin._reraise(*ex) +class TracedHookExecution: + def __init__(self, pluginmanager, before, after): + self.pluginmanager = pluginmanager + self.before = before + self.after = after + self.oldcall = pluginmanager._inner_hookexec + assert not isinstance(self.oldcall, TracedHookExecution) + self.pluginmanager._inner_hookexec = self + + def __call__(self, hook, methods, kwargs): + self.before(hook, methods, kwargs) + outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs)) + self.after(outcome, hook, methods, kwargs) + return outcome.get_result() + + def undo(self): + self.pluginmanager._inner_hookexec = self.oldcall + + class PluginManager(object): """ Core Pluginmanager class which manages registration of plugin objects and 1:N hook calling. @@ -144,197 +203,228 @@ plugin objects. An optional excludefunc allows to blacklist names which are not considered as hooks despite a matching prefix. - For debugging purposes you can call ``set_tracing(writer)`` - which will subsequently send debug information to the specified - write function. + For debugging purposes you can call ``enable_tracing()`` + which will subsequently send debug information to the trace helper. """ def __init__(self, prefix, excludefunc=None): self._prefix = prefix self._excludefunc = excludefunc self._name2plugin = {} - self._plugins = [] self._plugin2hookcallers = {} + self._plugin_distinfo = [] self.trace = TagTracer().get("pluginmanage") - self.hook = HookRelay(pm=self) + self.hook = HookRelay(self.trace.root.get("hook")) + self._inner_hookexec = lambda hook, methods, kwargs: \ + MultiCall(methods, kwargs, hook.firstresult).execute() - def set_tracing(self, writer): - """ turn on tracing to the given writer method and - return an undo function. """ - self.trace.root.setwriter(writer) - # reconfigure HookCalling to perform tracing - assert not hasattr(self, "_wrapping") - self._wrapping = True + def _hookexec(self, hook, methods, kwargs): + # called from all hookcaller instances. + # enable_tracing will set its own wrapping function at self._inner_hookexec + return self._inner_hookexec(hook, methods, kwargs) - hooktrace = self.hook.trace + def enable_tracing(self): + """ enable tracing of hook calls and return an undo function. """ + hooktrace = self.hook._trace - def _docall(self, methods, kwargs): + def before(hook, methods, kwargs): hooktrace.root.indent += 1 - hooktrace(self.name, kwargs) - box = yield - if box.excinfo is None: - hooktrace("finish", self.name, "-->", box.result) + hooktrace(hook.name, kwargs) + + def after(outcome, hook, methods, kwargs): + if outcome.excinfo is None: + hooktrace("finish", hook.name, "-->", outcome.result) hooktrace.root.indent -= 1 - return add_method_wrapper(HookCaller, _docall) + return TracedHookExecution(self, before, after).undo - def make_hook_caller(self, name, plugins): - caller = getattr(self.hook, name) - methods = self.listattr(name, plugins=plugins) - return HookCaller(caller.name, caller.firstresult, - argnames=caller.argnames, methods=methods) + def subset_hook_caller(self, name, remove_plugins): + """ Return a new HookCaller instance for the named method + which manages calls to all registered plugins except the + ones from remove_plugins. """ + orig = getattr(self.hook, name) + plugins_to_remove = [plugin for plugin in remove_plugins + if hasattr(plugin, name)] + if plugins_to_remove: + hc = HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class) + for plugin in orig._plugins: + if plugin not in plugins_to_remove: + hc._add_plugin(plugin) + # we also keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._plugin2hookcallers.setdefault(plugin, []).append(hc) + return hc + return orig def register(self, plugin, name=None): - """ Register a plugin with the given name and ensure that all its - hook implementations are integrated. If the name is not specified - we use the ``__name__`` attribute of the plugin object or, if that - doesn't exist, the id of the plugin. This method will raise a - ValueError if the eventual name is already registered. """ - name = name or self._get_canonical_name(plugin) - if self._name2plugin.get(name, None) == -1: - return - if self.hasplugin(name): + """ Register a plugin and return its canonical name or None if the name + is blocked from registering. Raise a ValueError if the plugin is already + registered. """ + plugin_name = name or self.get_canonical_name(plugin) + + if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: + if self._name2plugin.get(plugin_name, -1) is None: + return # blocked plugin, return None to indicate no registration raise ValueError("Plugin already registered: %s=%s\n%s" %( - name, plugin, self._name2plugin)) - #self.trace("registering", name, plugin) - # allow subclasses to intercept here by calling a helper - return self._do_register(plugin, name) + plugin_name, plugin, self._name2plugin)) - def _do_register(self, plugin, name): - hookcallers = list(self._scan_plugin(plugin)) - self._plugin2hookcallers[plugin] = hookcallers - self._name2plugin[name] = plugin - self._plugins.append(plugin) - # rescan all methods for the hookcallers we found - for hookcaller in hookcallers: - self._scan_methods(hookcaller) - return True + self._name2plugin[plugin_name] = plugin - def unregister(self, plugin): - """ unregister the plugin object and all its contained hook implementations + # register prefix-matching hook specs of the plugin + self._plugin2hookcallers[plugin] = hookcallers = [] + for name in dir(plugin): + if name.startswith(self._prefix): + hook = getattr(self.hook, name, None) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + hook = HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) + elif hook.has_spec(): + self._verify_hook(hook, plugin) + hook._maybe_apply_history(getattr(plugin, name)) + hookcallers.append(hook) + hook._add_plugin(plugin) + return plugin_name + + def unregister(self, plugin=None, name=None): + """ unregister a plugin object and all its contained hook implementations from internal data structures. """ - self._plugins.remove(plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - hookcallers = self._plugin2hookcallers.pop(plugin) - for hookcaller in hookcallers: - self._scan_methods(hookcaller) + if name is None: + assert plugin is not None, "one of name or plugin needs to be specified" + name = self.get_name(plugin) + + if plugin is None: + plugin = self.get_plugin(name) + + # if self._name2plugin[name] == None registration was blocked: ignore + if self._name2plugin.get(name): + del self._name2plugin[name] + + for hookcaller in self._plugin2hookcallers.pop(plugin, []): + hookcaller._remove_plugin(plugin) + + return plugin + + def set_blocked(self, name): + """ block registrations of the given name, unregister if already registered. """ + self.unregister(name=name) + self._name2plugin[name] = None def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using the prefix/excludefunc with which the PluginManager was initialized. """ - isclass = int(inspect.isclass(module_or_class)) names = [] for name in dir(module_or_class): if name.startswith(self._prefix): - method = module_or_class.__dict__[name] - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self.hook, name, hc) + hc = getattr(self.hook, name, None) + if hc is None: + hc = HookCaller(name, self._hookexec, module_or_class) + setattr(self.hook, name, hc) + else: + # plugins registered this hook without knowing the spec + hc.set_specification(module_or_class) + for plugin in hc._plugins: + self._verify_hook(hc, plugin) names.append(name) + if not names: raise ValueError("did not find new %r hooks in %r" %(self._prefix, module_or_class)) - def getplugins(self): - """ return the complete list of registered plugins. NOTE that - you will get the internal list and need to make a copy if you - modify the list.""" - return self._plugins + def get_plugins(self): + """ return the set of registered plugins. """ + return set(self._plugin2hookcallers) - def isregistered(self, plugin): - """ Return True if the plugin is already registered under its - canonical name. """ - return self.hasplugin(self._get_canonical_name(plugin)) or \ - plugin in self._plugins + def is_registered(self, plugin): + """ Return True if the plugin is already registered. """ + return plugin in self._plugin2hookcallers - def hasplugin(self, name): - """ Return True if there is a registered with the given name. """ - return name in self._name2plugin + def get_canonical_name(self, plugin): + """ Return canonical name for a plugin object. Note that a plugin + may be registered under a different name which was specified + by the caller of register(plugin, name). To obtain the name + of an registered plugin use ``get_name(plugin)`` instead.""" + return getattr(plugin, "__name__", None) or str(id(plugin)) - def getplugin(self, name): + def get_plugin(self, name): """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - last = [] - wrappers = [] - for plugin in plugins: + def get_name(self, plugin): + """ Return name for registered plugin or None if not registered. """ + for name, val in self._name2plugin.items(): + if plugin == val: + return name + + def _verify_hook(self, hook, plugin): + method = getattr(plugin, hook.name) + pluginname = self.get_name(plugin) + + if hook.is_historic() and hasattr(method, "hookwrapper"): + raise PluginValidationError( + "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %( + pluginname, hook.name)) + + for arg in varnames(method): + if arg not in hook.argnames: + raise PluginValidationError( + "Plugin %r\nhook %r\nargument %r not available\n" + "plugin definition: %s\n" + "available hookargs: %s" %( + pluginname, hook.name, arg, formatdef(method), + ", ".join(hook.argnames))) + + def check_pending(self): + """ Verify that all hooks which have not been verified against + a hook specification are optional, otherwise raise PluginValidationError""" + for name in self.hook.__dict__: + if name.startswith(self._prefix): + hook = getattr(self.hook, name) + if not hook.has_spec(): + for plugin in hook._plugins: + method = getattr(plugin, hook.name) + if not getattr(method, "optionalhook", False): + raise PluginValidationError( + "unknown hook %r in plugin %r" %(name, plugin)) + + def load_setuptools_entrypoints(self, entrypoint_name): + """ Load modules from querying the specified setuptools entrypoint name. + Return the number of loaded plugins. """ + from pkg_resources import iter_entry_points, DistributionNotFound + for ep in iter_entry_points(entrypoint_name): + # is the plugin registered or blocked? + if self.get_plugin(ep.name) or ep.name in self._name2plugin: + continue try: - meth = getattr(plugin, attrname) - except AttributeError: + plugin = ep.load() + except DistributionNotFound: continue - if hasattr(meth, 'hookwrapper'): - wrappers.append(meth) - elif hasattr(meth, 'tryfirst'): - last.append(meth) - elif hasattr(meth, 'trylast'): - l.insert(0, meth) - else: - l.append(meth) - l.extend(last) - l.extend(wrappers) - return l - - def _scan_methods(self, hookcaller): - hookcaller.methods = self.listattr(hookcaller.name) - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() - - - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - - for name in dir(plugin): - if name[0] == "_" or not name.startswith(self._prefix): - continue - hook = getattr(self.hook, name, None) - method = getattr(plugin, name) - if hook is None: - if self._excludefunc is not None and self._excludefunc(name): - continue - if getattr(method, 'optionalhook', False): - continue - fail("found unknown hook: %r", name) - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook - - def _get_canonical_name(self, plugin): - return getattr(plugin, "__name__", None) or str(id(plugin)) - + self.register(plugin, name=ep.name) + self._plugin_distinfo.append((ep.dist, plugin)) + return len(self._plugin_distinfo) class MultiCall: """ execute a call into multiple python functions/methods. """ + # XXX note that the __multicall__ argument is supported only + # for pytest compatibility reasons. It was never officially + # supported there and is explicitely deprecated since 2.8 + # so we can remove it soon, allowing to avoid the below recursion + # in execute() and simplify/speed up the execute loop. + def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) + self.methods = methods self.kwargs = kwargs self.kwargs["__multicall__"] = self - self.results = [] self.firstresult = firstresult - def __repr__(self): - status = "%d results, %d meths" % (len(self.results), len(self.methods)) - return "" %(status, self.kwargs) - def execute(self): all_kwargs = self.kwargs + self.results = results = [] + firstresult = self.firstresult + while self.methods: method = self.methods.pop() args = [all_kwargs[argname] for argname in varnames(method)] @@ -342,11 +432,19 @@ return wrapped_call(method(*args), self.execute) res = method(*args) if res is not None: - self.results.append(res) - if self.firstresult: + if firstresult: return res - if not self.firstresult: - return self.results + results.append(res) + + if not firstresult: + return results + + def __repr__(self): + status = "%d meths" % (len(self.methods),) + if hasattr(self, "results"): + status = ("%d results, " % len(self.results)) + status + return "" %(status, self.kwargs) + def varnames(func, startindex=None): @@ -361,17 +459,17 @@ return cache["_varnames"] except KeyError: pass - if inspect.isclass(func): + if isclass(func): try: func = func.__init__ except AttributeError: return () startindex = 1 else: - if not inspect.isfunction(func) and not inspect.ismethod(func): + if not isfunction(func) and not ismethod(func): func = getattr(func, '__call__', func) if startindex is None: - startindex = int(inspect.ismethod(func)) + startindex = int(ismethod(func)) rawcode = py.code.getrawcode(func) try: @@ -390,32 +488,95 @@ class HookRelay: - def __init__(self, pm): - self._pm = pm - self.trace = pm.trace.root.get("hook") + def __init__(self, trace): + self._trace = trace -class HookCaller: - def __init__(self, name, firstresult, argnames, methods=()): +class HookCaller(object): + def __init__(self, name, hook_execute, specmodule_or_class=None): self.name = name - self.firstresult = firstresult - self.argnames = ["__multicall__"] - self.argnames.extend(argnames) + self._plugins = [] + self._wrappers = [] + self._nonwrappers = [] + self._hookexec = hook_execute + if specmodule_or_class is not None: + self.set_specification(specmodule_or_class) + + def has_spec(self): + return hasattr(self, "_specmodule_or_class") + + def set_specification(self, specmodule_or_class): + assert not self.has_spec() + self._specmodule_or_class = specmodule_or_class + specfunc = getattr(specmodule_or_class, self.name) + argnames = varnames(specfunc, startindex=isclass(specmodule_or_class)) assert "self" not in argnames # sanity check - self.methods = methods + self.argnames = ["__multicall__"] + list(argnames) + self.firstresult = getattr(specfunc, 'firstresult', False) + if hasattr(specfunc, "historic"): + self._call_history = [] + + def is_historic(self): + return hasattr(self, "_call_history") + + def _remove_plugin(self, plugin): + self._plugins.remove(plugin) + meth = getattr(plugin, self.name) + try: + self._nonwrappers.remove(meth) + except ValueError: + self._wrappers.remove(meth) + + def _add_plugin(self, plugin): + self._plugins.append(plugin) + self._add_method(getattr(plugin, self.name)) + + def _add_method(self, meth): + if hasattr(meth, 'hookwrapper'): + methods = self._wrappers + else: + methods = self._nonwrappers + + if hasattr(meth, 'trylast'): + methods.insert(0, meth) + elif hasattr(meth, 'tryfirst'): + methods.append(meth) + else: + # find last non-tryfirst method + i = len(methods) - 1 + while i >= 0 and hasattr(methods[i], "tryfirst"): + i -= 1 + methods.insert(i + 1, meth) def __repr__(self): return "" %(self.name,) def __call__(self, **kwargs): - return self._docall(self.methods, kwargs) + assert not self.is_historic() + return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) - def callextra(self, methods, **kwargs): - return self._docall(self.methods + methods, kwargs) + def call_historic(self, proc=None, kwargs=None): + self._call_history.append((kwargs or {}, proc)) + # historizing hooks don't return results + self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) - def _docall(self, methods, kwargs): - return MultiCall(methods, kwargs, - firstresult=self.firstresult).execute() + def call_extra(self, methods, kwargs): + """ Call the hook with some additional temporarily participating + methods using the specified kwargs as call parameters. """ + old = list(self._nonwrappers), list(self._wrappers) + for method in methods: + self._add_method(method) + try: + return self(**kwargs) + finally: + self._nonwrappers, self._wrappers = old + + def _maybe_apply_history(self, method): + if self.is_historic(): + for kwargs, proc in self._call_history: + res = self._hookexec(self, [method], kwargs) + if res and proc is not None: + proc(res[0]) class PluginValidationError(Exception): @@ -425,5 +586,5 @@ def formatdef(func): return "%s%s" % ( func.__name__, - inspect.formatargspec(*inspect.getargspec(func)) + formatargspec(*getargspec(func)) ) diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -22,7 +22,7 @@ help="store internal tracing debug information in 'pytestdebug.log'.") - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_cmdline_parse(): outcome = yield config = outcome.get_result() @@ -34,13 +34,15 @@ pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(debugfile.write) + config.trace.root.setwriter(debugfile.write) + undo_tracing = config.pluginmanager.enable_tracing() sys.stderr.write("writing pytestdebug information to %s\n" % path) def unset_tracing(): debugfile.close() sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) config.trace.root.setwriter(None) + undo_tracing() config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -1,27 +1,30 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ +from _pytest.core import hookspec_opts + # ------------------------------------------------------------------------- -# Initialization +# Initialization hooks called for every plugin # ------------------------------------------------------------------------- + at hookspec_opts(historic=True) def pytest_addhooks(pluginmanager): - """called at plugin load time to allow adding new hooks via a call to + """called at plugin registration time to allow adding new hooks via a call to pluginmanager.addhooks(module_or_class, prefix).""" + at hookspec_opts(historic=True) def pytest_namespace(): """return dict of name->object to be made globally available in - the pytest namespace. This hook is called before command line options - are parsed. + the pytest namespace. This hook is called at plugin registration + time. """ -def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ -pytest_cmdline_parse.firstresult = True + at hookspec_opts(historic=True) +def pytest_plugin_registered(plugin, manager): + """ a new pytest plugin got registered. """ -def pytest_cmdline_preparse(config, args): - """(deprecated) modify command line arguments before option parsing. """ + at hookspec_opts(historic=True) def pytest_addoption(parser): """register argparse-style options and ini-style config values. @@ -47,35 +50,43 @@ via (deprecated) ``pytest.config``. """ + at hookspec_opts(historic=True) +def pytest_configure(config): + """ called after command line options have been parsed + and all plugins and initial conftest files been loaded. + This hook is called for every plugin. + """ + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins as well as directly +# discoverable conftest.py local plugins. +# ------------------------------------------------------------------------- + + at hookspec_opts(firstresult=True) +def pytest_cmdline_parse(pluginmanager, args): + """return initialized config object, parsing the specified args. """ + +def pytest_cmdline_preparse(config, args): + """(deprecated) modify command line arguments before option parsing. """ + + at hookspec_opts(firstresult=True) def pytest_cmdline_main(config): """ called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. """ -pytest_cmdline_main.firstresult = True def pytest_load_initial_conftests(args, early_config, parser): """ implements the loading of initial conftest files ahead of command line option parsing. """ -def pytest_configure(config): - """ called after command line options have been parsed - and all plugins and initial conftest files been loaded. - """ - -def pytest_unconfigure(config): - """ called before test process is exited. """ - -def pytest_runtestloop(session): - """ called for performing the main runtest loop - (after collection finished). """ -pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_collection(session): """ perform the collection protocol for the given session. """ -pytest_collection.firstresult = True def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order @@ -84,16 +95,16 @@ def pytest_collection_finish(session): """ called after collection has been performed and modified. """ + at hookspec_opts(firstresult=True) def pytest_ignore_collect(path, config): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. """ -pytest_ignore_collect.firstresult = True + at hookspec_opts(firstresult=True) def pytest_collect_directory(path, parent): """ called before traversing a directory for collection files. """ -pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): """ return collection Node or None for the given path. Any new node @@ -112,29 +123,29 @@ def pytest_deselected(items): """ called for test items deselected by keyword. """ + at hookspec_opts(firstresult=True) def pytest_make_collect_report(collector): """ perform ``collector.collect()`` and return a CollectReport. """ -pytest_make_collect_report.firstresult = True # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_pycollect_makemodule(path, parent): """ return a Module collector or None for the given path. This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to create test modules for files that do not match as a test module. """ -pytest_pycollect_makemodule.firstresult = True + at hookspec_opts(firstresult=True) def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ -pytest_pycollect_makeitem.firstresult = True + at hookspec_opts(firstresult=True) def pytest_pyfunc_call(pyfuncitem): """ call underlying test function. """ -pytest_pyfunc_call.firstresult = True def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" @@ -142,9 +153,16 @@ # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- + + at hookspec_opts(firstresult=True) +def pytest_runtestloop(session): + """ called for performing the main runtest loop + (after collection finished). """ + def pytest_itemstart(item, node): """ (deprecated, use pytest_runtest_logstart). """ + at hookspec_opts(firstresult=True) def pytest_runtest_protocol(item, nextitem): """ implements the runtest_setup/call/teardown protocol for the given test item, including capturing exceptions and calling @@ -158,7 +176,6 @@ :return boolean: True if no further hook implementations should be invoked. """ -pytest_runtest_protocol.firstresult = True def pytest_runtest_logstart(nodeid, location): """ signal the start of running a single test item. """ @@ -178,12 +195,12 @@ so that nextitem only needs to call setup-functions. """ + at hookspec_opts(firstresult=True) def pytest_runtest_makereport(item, call): """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item` and :py:class:`_pytest.runner.CallInfo`. """ -pytest_runtest_makereport.firstresult = True def pytest_runtest_logreport(report): """ process a test setup/call/teardown report relating to @@ -199,6 +216,9 @@ def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ +def pytest_unconfigure(config): + """ called before test process is exited. """ + # ------------------------------------------------------------------------- # hooks for customising the assert methods @@ -220,9 +240,9 @@ def pytest_report_header(config, startdir): """ return a string to be displayed as header info for terminal reporting.""" + at hookspec_opts(firstresult=True) def pytest_report_teststatus(report): """ return result-category, shortletter and verbose word for reporting.""" -pytest_report_teststatus.firstresult = True def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ @@ -236,17 +256,14 @@ # doctest hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_doctest_prepare_content(content): """ return processed content for a given doctest""" -pytest_doctest_prepare_content.firstresult = True # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- -def pytest_plugin_registered(plugin, manager): - """ a new pytest plugin got registered. """ - def pytest_internalerror(excrepr, excinfo): """ called for internal errors. """ diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -151,18 +151,17 @@ ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -class FSHookProxy(object): - def __init__(self, fspath, config): +class FSHookProxy: + def __init__(self, fspath, pm, remove_mods): self.fspath = fspath - self.config = config + self.pm = pm + self.remove_mods = remove_mods def __getattr__(self, name): - plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.pluginmanager.make_hook_caller(name, plugins) + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) self.__dict__[name] = x return x - def compatproperty(name): def fget(self): # deprecated - use pytest.name @@ -362,9 +361,6 @@ def listnames(self): return [x.name for x in self.listchain()] - def getplugins(self): - return self.config._getmatchingplugins(self.fspath) - def addfinalizer(self, fin): """ register a function to be called when this node is finalized. @@ -519,12 +515,12 @@ def _makeid(self): return "" - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_collectstart(self): if self.shouldstop: raise self.Interrupted(self.shouldstop) - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): self._testsfailed += 1 @@ -541,8 +537,20 @@ try: return self._fs2hookproxy[fspath] except KeyError: - self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config) - return x + # check if we have the common case of running + # hooks with all conftest.py filesall conftest.py + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules(fspath) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # one or more conftests are not in use at this fspath + proxy = FSHookProxy(fspath, pm, remove_mods) + else: + # all plugis are active for this fspath + proxy = self.config.hook + + self._fs2hookproxy[fspath] = proxy + return proxy def perform_collect(self, args=None, genitems=True): hook = self.config.hook diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/nose.py --- a/_pytest/nose.py +++ b/_pytest/nose.py @@ -24,7 +24,7 @@ call.excinfo = call2.excinfo - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_runtest_setup(item): if is_potential_nosetest(item): if isinstance(item.parent, pytest.Generator): diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pastebin.py --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -11,7 +11,7 @@ choices=['failed', 'all'], help="send failed|all info to bpaste.net pastebin service.") - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_configure(config): if config.option.pastebin == "all": tr = config.pluginmanager.getplugin('terminalreporter') diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -11,7 +11,7 @@ import py import pytest from py.builtin import print_ -from _pytest.core import HookCaller, add_method_wrapper +from _pytest.core import TracedHookExecution from _pytest.main import Session, EXIT_OK @@ -79,12 +79,12 @@ self._pluginmanager = pluginmanager self.calls = [] - def _docall(hookcaller, methods, kwargs): - self.calls.append(ParsedCall(hookcaller.name, kwargs)) - yield - self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - #if hasattr(pluginmanager, "config"): - # pluginmanager.add_shutdown(self._undo_wrapping) + def before(hook, method, kwargs): + self.calls.append(ParsedCall(hook.name, kwargs)) + def after(outcome, hook, method, kwargs): + pass + executor = TracedHookExecution(pluginmanager, before, after) + self._undo_wrapping = executor.undo def finish_recording(self): self._undo_wrapping() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -172,7 +172,7 @@ def pytest_sessionstart(session): session._fixturemanager = FixtureManager(session) - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_namespace(): raises.Exception = pytest.fail.Exception return { @@ -191,7 +191,7 @@ return request.config - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): @@ -219,7 +219,7 @@ def pytest_pycollect_makemodule(path, parent): return Module(path, parent) - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() @@ -375,13 +375,16 @@ fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) metafunc = Metafunc(funcobj, fixtureinfo, self.config, cls=cls, module=module) - try: - methods = [module.pytest_generate_tests] - except AttributeError: - methods = [] + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) - self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) + if methods: + self.ihook.pytest_generate_tests.call_extra(methods, + dict(metafunc=metafunc)) + else: + self.ihook.pytest_generate_tests(metafunc=metafunc) Function = self._getcustomclass("Function") if not metafunc._calls: @@ -1621,7 +1624,6 @@ self.session = session self.config = session.config self._arg2fixturedefs = {} - self._seenplugins = set() self._holderobjseen = set() self._arg2finish = {} self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] @@ -1646,11 +1648,7 @@ node) return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs) - ### XXX this hook should be called for historic events like pytest_configure - ### so that we don't have to do the below pytest_configure hook def pytest_plugin_registered(self, plugin): - if plugin in self._seenplugins: - return nodeid = None try: p = py.path.local(plugin.__file__) @@ -1665,13 +1663,6 @@ if p.sep != "/": nodeid = nodeid.replace(p.sep, "/") self.parsefactories(plugin, nodeid) - self._seenplugins.add(plugin) - - @pytest.mark.tryfirst - def pytest_configure(self, config): - plugins = config.pluginmanager.getplugins() - for plugin in plugins: - self.pytest_plugin_registered(plugin) def _getautousenames(self, nodeid): """ return a tuple of fixture names to be used. """ diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/skipping.py --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -133,7 +133,7 @@ return expl - at pytest.mark.tryfirst + at pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_setup(item): evalskip = MarkEvaluator(item, 'skipif') if evalskip.istrue(): @@ -151,7 +151,7 @@ if not evalxfail.get('run', True): pytest.xfail("[NOTRUN] " + evalxfail.getexplanation()) - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -164,6 +164,8 @@ def pytest_logwarning(self, code, fslocation, message, nodeid): warnings = self.stats.setdefault("warnings", []) + if isinstance(fslocation, tuple): + fslocation = "%s:%d" % fslocation warning = WarningReport(code=code, fslocation=fslocation, message=message, nodeid=nodeid) warnings.append(warning) @@ -265,7 +267,7 @@ def pytest_collection_modifyitems(self): self.report_collect(True) - @pytest.mark.trylast + @pytest.hookimpl_opts(trylast=True) def pytest_sessionstart(self, session): self._sessionstarttime = time.time() if not self.showheader: @@ -350,7 +352,7 @@ indent = (len(stack) - 1) * " " self._tw.line("%s%s" % (indent, col)) - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): outcome = yield outcome.get_result() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/unittest.py --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -140,7 +140,7 @@ if traceback: excinfo.traceback = traceback - at pytest.mark.tryfirst + at pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call): if isinstance(item, TestCaseFunction): if item._excinfo: @@ -152,7 +152,7 @@ # twisted trial support - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_protocol(item): if isinstance(item, TestCaseFunction) and \ 'twisted.trial.unittest' in sys.modules: diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/example/markers.txt --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -201,9 +201,9 @@ @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. For an example on how to add and work with markers from a plugin, see @@ -375,9 +375,9 @@ @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. Reading markers which were set from multiple places diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/example/simple.txt --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.txt @@ -534,7 +534,7 @@ import pytest import os.path - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call, __multicall__): # execute all other hooks to obtain the report object rep = __multicall__.execute() @@ -607,7 +607,7 @@ import pytest - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call, __multicall__): # execute all other hooks to obtain the report object rep = __multicall__.execute() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -56,6 +56,7 @@ - all collection, reporting, running aspects are delegated to hook functions - customizations can be per-directory, per-project or per PyPI released plugin - it is easy to add command line options or customize existing behaviour + - :ref:`easy to write your own plugins ` .. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Mon Apr 27 14:18:53 2015 From: builds at drone.io (Drone.io Build) Date: Mon, 27 Apr 2015 12:18:53 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 97 Message-ID: <20150427121851.102145.78852@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/97 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4203:24f4d48abeeb Author : Floris Bruynooghe Branch : default Message: Merged in hpk42/pytest-patches/more_plugin (pull request #282) -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Mon Apr 27 14:17:46 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 27 Apr 2015 12:17:46 -0000 Subject: [Pytest-commit] commit/pytest: 25 new changesets Message-ID: <20150427121746.23547.59061@app05.ash-private.bitbucket.org> 25 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/b58169b19784/ Changeset: b58169b19784 Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: allow to register plugins with hooks that are only added later Affected #: 3 files diff -r 90f9b67b555f24f26798193206f58930f2ea1306 -r b58169b197846955a1edaaaf2f944ea0f489c7b0 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -38,6 +38,7 @@ tw.line("ERROR: could not load %s\n" % (e.path), red=True) return 4 else: + config.pluginmanager.check_pending() return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace diff -r 90f9b67b555f24f26798193206f58930f2ea1306 -r b58169b197846955a1edaaaf2f944ea0f489c7b0 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -181,7 +181,7 @@ def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) methods = self.listattr(name, plugins=plugins) - return HookCaller(caller.name, caller.firstresult, + return HookCaller(caller.name, [plugins], firstresult=caller.firstresult, argnames=caller.argnames, methods=methods) def register(self, plugin, name=None): @@ -201,13 +201,9 @@ return self._do_register(plugin, name) def _do_register(self, plugin, name): - hookcallers = list(self._scan_plugin(plugin)) - self._plugin2hookcallers[plugin] = hookcallers + self._plugin2hookcallers[plugin] = self._scan_plugin(plugin) self._name2plugin[name] = plugin self._plugins.append(plugin) - # rescan all methods for the hookcallers we found - for hookcaller in hookcallers: - self._scan_methods(hookcaller) return True def unregister(self, plugin): @@ -219,6 +215,7 @@ del self._name2plugin[name] hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: + hookcaller.plugins.remove(plugin) self._scan_methods(hookcaller) def addhooks(self, module_or_class): @@ -228,11 +225,20 @@ names = [] for name in dir(module_or_class): if name.startswith(self._prefix): - method = module_or_class.__dict__[name] - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self.hook, name, hc) + specfunc = module_or_class.__dict__[name] + firstresult = getattr(specfunc, 'firstresult', False) + hc = getattr(self.hook, name, None) + argnames = varnames(specfunc, startindex=isclass) + if hc is None: + hc = HookCaller(name, [], firstresult=firstresult, + argnames=argnames, methods=[]) + setattr(self.hook, name, hc) + else: + # plugins registered this hook without knowing the spec + hc.setspec(firstresult=firstresult, argnames=argnames) + self._scan_methods(hc) + for plugin in hc.plugins: + self._verify_hook(hc, specfunc, plugin) names.append(name) if not names: raise ValueError("did not find new %r hooks in %r" @@ -282,18 +288,14 @@ return l def _scan_methods(self, hookcaller): - hookcaller.methods = self.listattr(hookcaller.name) + hookcaller.methods = self.listattr(hookcaller.name, hookcaller.plugins) def call_plugin(self, plugin, methname, kwargs): return MultiCall(methods=self.listattr(methname, plugins=[plugin]), kwargs=kwargs, firstresult=True).execute() - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - + hookcallers = [] for name in dir(plugin): if name[0] == "_" or not name.startswith(self._prefix): continue @@ -302,17 +304,40 @@ if hook is None: if self._excludefunc is not None and self._excludefunc(name): continue - if getattr(method, 'optionalhook', False): - continue - fail("found unknown hook: %r", name) - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook + hook = HookCaller(name, [plugin]) + setattr(self.hook, name, hook) + elif hook.pre: + # there is only a pre non-specced stub + hook.plugins.append(plugin) + else: + # we have a hook spec, can verify early + self._verify_hook(hook, method, plugin) + hook.plugins.append(plugin) + self._scan_methods(hook) + hookcallers.append(hook) + return hookcallers + + def _verify_hook(self, hook, method, plugin): + for arg in varnames(method): + if arg not in hook.argnames: + pluginname = self._get_canonical_name(plugin) + raise PluginValidationError( + "Plugin %r\nhook %r\nargument %r not available\n" + "plugin definition: %s\n" + "available hookargs: %s" %( + pluginname, hook.name, arg, formatdef(method), + ", ".join(hook.argnames))) + + def check_pending(self): + for name in self.hook.__dict__: + if name.startswith(self._prefix): + hook = getattr(self.hook, name) + if hook.pre: + for plugin in hook.plugins: + method = getattr(plugin, hook.name) + if not getattr(method, "optionalhook", False): + raise PluginValidationError( + "unknown hook %r in plugin %r" %(name, plugin)) def _get_canonical_name(self, plugin): return getattr(plugin, "__name__", None) or str(id(plugin)) @@ -396,13 +421,24 @@ class HookCaller: - def __init__(self, name, firstresult, argnames, methods=()): + def __init__(self, name, plugins, argnames=None, firstresult=None, methods=None): self.name = name + self.plugins = plugins + self.methods = methods + if argnames is not None: + argnames = ["__multicall__"] + list(argnames) + self.argnames = argnames self.firstresult = firstresult - self.argnames = ["__multicall__"] - self.argnames.extend(argnames) + + @property + def pre(self): + return self.argnames is None + + def setspec(self, argnames, firstresult): + assert self.pre assert "self" not in argnames # sanity check - self.methods = methods + self.argnames = ["__multicall__"] + list(argnames) + self.firstresult = firstresult def __repr__(self): return "" %(self.name,) @@ -414,6 +450,7 @@ return self._docall(self.methods + methods, kwargs) def _docall(self, methods, kwargs): + assert not self.pre, self.name return MultiCall(methods, kwargs, firstresult=self.firstresult).execute() diff -r 90f9b67b555f24f26798193206f58930f2ea1306 -r b58169b197846955a1edaaaf2f944ea0f489c7b0 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -32,12 +32,13 @@ pm.unregister(a1) assert not pm.isregistered(a1) - def test_register_mismatch_method(self): - pm = get_plugin_manager() + def test_register_mismatch_method(self, pytestpm): class hello: def pytest_gurgel(self): pass - pytest.raises(Exception, lambda: pm.register(hello())) + pytestpm.register(hello()) + with pytest.raises(PluginValidationError): + pytestpm.check_pending() def test_register_mismatch_arg(self): pm = get_plugin_manager() @@ -77,6 +78,18 @@ l = list(plugins.listattr('x')) assert l == [41, 42, 43] + def test_register_unknown_hooks(self, pm): + class Plugin1: + def he_method1(self, arg): + return arg + 1 + + pm.register(Plugin1()) + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + #assert not pm._unverified_hooks + assert pm.hook.he_method1(arg=1) == [2] class TestPytestPluginInteractions: https://bitbucket.org/pytest-dev/pytest/commits/6b9672b337a8/ Changeset: 6b9672b337a8 Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: incrementally update hook call lists instead of regenerating the whole list on each registered plugin Affected #: 3 files diff -r b58169b197846955a1edaaaf2f944ea0f489c7b0 -r 6b9672b337a806cdfc16655f9552f91a50eb62be _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -180,9 +180,13 @@ def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) - methods = self.listattr(name, plugins=plugins) - return HookCaller(caller.name, [plugins], firstresult=caller.firstresult, - argnames=caller.argnames, methods=methods) + hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult, + argnames=caller.argnames) + for plugin in hc.plugins: + meth = getattr(plugin, name, None) + if meth is not None: + hc._add_method(meth) + return hc def register(self, plugin, name=None): """ Register a plugin with the given name and ensure that all its @@ -216,7 +220,7 @@ hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: hookcaller.plugins.remove(plugin) - self._scan_methods(hookcaller) + hookcaller._scan_methods() def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using @@ -231,14 +235,14 @@ argnames = varnames(specfunc, startindex=isclass) if hc is None: hc = HookCaller(name, [], firstresult=firstresult, - argnames=argnames, methods=[]) + argnames=argnames) setattr(self.hook, name, hc) else: # plugins registered this hook without knowing the spec hc.setspec(firstresult=firstresult, argnames=argnames) - self._scan_methods(hc) for plugin in hc.plugins: self._verify_hook(hc, specfunc, plugin) + hc._add_method(getattr(plugin, name)) names.append(name) if not names: raise ValueError("did not find new %r hooks in %r" @@ -264,35 +268,10 @@ """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - last = [] - wrappers = [] - for plugin in plugins: - try: - meth = getattr(plugin, attrname) - except AttributeError: - continue - if hasattr(meth, 'hookwrapper'): - wrappers.append(meth) - elif hasattr(meth, 'tryfirst'): - last.append(meth) - elif hasattr(meth, 'trylast'): - l.insert(0, meth) - else: - l.append(meth) - l.extend(last) - l.extend(wrappers) - return l - - def _scan_methods(self, hookcaller): - hookcaller.methods = self.listattr(hookcaller.name, hookcaller.plugins) - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() + meth = getattr(plugin, methname, None) + if meth is not None: + return MultiCall(methods=[meth], kwargs=kwargs, firstresult=True).execute() def _scan_plugin(self, plugin): hookcallers = [] @@ -313,7 +292,7 @@ # we have a hook spec, can verify early self._verify_hook(hook, method, plugin) hook.plugins.append(plugin) - self._scan_methods(hook) + hook._add_method(method) hookcallers.append(hook) return hookcallers @@ -348,7 +327,7 @@ """ execute a call into multiple python functions/methods. """ def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) + self.methods = methods self.kwargs = kwargs self.kwargs["__multicall__"] = self self.results = [] @@ -421,14 +400,15 @@ class HookCaller: - def __init__(self, name, plugins, argnames=None, firstresult=None, methods=None): + def __init__(self, name, plugins, argnames=None, firstresult=None): self.name = name self.plugins = plugins - self.methods = methods if argnames is not None: argnames = ["__multicall__"] + list(argnames) self.argnames = argnames self.firstresult = firstresult + self.wrappers = [] + self.nonwrappers = [] @property def pre(self): @@ -440,14 +420,41 @@ self.argnames = ["__multicall__"] + list(argnames) self.firstresult = firstresult + def _scan_methods(self): + self.wrappers[:] = [] + self.nonwrappers[:] = [] + for plugin in self.plugins: + self._add_method(getattr(plugin, self.name)) + + def _add_method(self, meth): + assert not self.pre + if hasattr(meth, 'hookwrapper'): + self.wrappers.append(meth) + elif hasattr(meth, 'trylast'): + self.nonwrappers.insert(0, meth) + elif hasattr(meth, 'tryfirst'): + self.nonwrappers.append(meth) + else: + if not self.nonwrappers or not hasattr(self.nonwrappers[-1], "tryfirst"): + self.nonwrappers.append(meth) + else: + for i in reversed(range(len(self.nonwrappers)-1)): + if hasattr(self.nonwrappers[i], "tryfirst"): + continue + self.nonwrappers.insert(i+1, meth) + break + else: + self.nonwrappers.insert(0, meth) + def __repr__(self): return "" %(self.name,) def __call__(self, **kwargs): - return self._docall(self.methods, kwargs) + return self._docall(self.nonwrappers + self.wrappers, kwargs) def callextra(self, methods, **kwargs): - return self._docall(self.methods + methods, kwargs) + return self._docall(self.nonwrappers + methods + self.wrappers, + kwargs) def _docall(self, methods, kwargs): assert not self.pre, self.name diff -r b58169b197846955a1edaaaf2f944ea0f489c7b0 -r 6b9672b337a806cdfc16655f9552f91a50eb62be testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -355,7 +355,8 @@ pass m = My() pm.register(m) - l = pm.listattr("pytest_load_initial_conftests") + hc = pm.hook.pytest_load_initial_conftests + l = hc.nonwrappers + hc.wrappers assert l[-1].__module__ == "_pytest.capture" assert l[-2] == m.pytest_load_initial_conftests assert l[-3].__module__ == "_pytest.config" diff -r b58169b197846955a1edaaaf2f944ea0f489c7b0 -r 6b9672b337a806cdfc16655f9552f91a50eb62be testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -64,20 +64,6 @@ assert not pm.isregistered(my) assert pm.getplugins()[-1:] == [my2] - def test_listattr(self): - plugins = PluginManager("xyz") - class api1: - x = 41 - class api2: - x = 42 - class api3: - x = 43 - plugins.register(api1()) - plugins.register(api2()) - plugins.register(api3()) - l = list(plugins.listattr('x')) - assert l == [41, 42, 43] - def test_register_unknown_hooks(self, pm): class Plugin1: def he_method1(self, arg): @@ -91,6 +77,121 @@ #assert not pm._unverified_hooks assert pm.hook.he_method1(arg=1) == [2] +class TestAddMethodOrdering: + @pytest.fixture + def hc(self, pm): + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + return pm.hook.he_method1 + + @pytest.fixture + def addmeth(self, hc): + def addmeth(tryfirst=False, trylast=False, hookwrapper=False): + def wrap(func): + if tryfirst: + func.tryfirst = True + if trylast: + func.trylast = True + if hookwrapper: + func.hookwrapper = True + hc._add_method(func) + return func + return wrap + return addmeth + + def test_adding_nonwrappers(self, hc, addmeth): + @addmeth() + def he_method1(): + pass + + @addmeth() + def he_method2(): + pass + + @addmeth() + def he_method3(): + pass + assert hc.nonwrappers == [he_method1, he_method2, he_method3] + + def test_adding_nonwrappers_trylast(self, hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + + @addmeth() + def he_method1_b(): + pass + assert hc.nonwrappers == [he_method1, he_method1_middle, he_method1_b] + + def test_adding_nonwrappers_trylast2(self, hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + assert hc.nonwrappers == [he_method1, he_method1_middle, he_method1_b] + + def test_adding_nonwrappers_tryfirst(self, hc, addmeth): + @addmeth(tryfirst=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + assert hc.nonwrappers == [he_method1_middle, he_method1_b, he_method1] + + def test_adding_nonwrappers_trylast(self, hc, addmeth): + @addmeth() + def he_method1_a(): + pass + + @addmeth(trylast=True) + def he_method1_b(): + pass + + @addmeth() + def he_method1_c(): + pass + + @addmeth(trylast=True) + def he_method1_d(): + pass + assert hc.nonwrappers == [he_method1_d, he_method1_b, he_method1_a, he_method1_c] + + def test_adding_wrappers_ordering(self, hc, addmeth): + @addmeth(hookwrapper=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth(hookwrapper=True) + def he_method3(): + pass + + assert hc.nonwrappers == [he_method1_middle] + assert hc.wrappers == [he_method1, he_method3] + + class TestPytestPluginInteractions: def test_addhooks_conftestplugin(self, testdir): @@ -201,43 +302,6 @@ assert pytestpm.trace.root.indent == indent assert saveindent[0] > indent - # lower level API - - def test_listattr(self): - pluginmanager = PluginManager("xyz") - class My2: - x = 42 - pluginmanager.register(My2()) - assert not pluginmanager.listattr("hello") - assert pluginmanager.listattr("x") == [42] - - def test_listattr_tryfirst(self): - class P1: - @pytest.mark.tryfirst - def m(self): - return 17 - - class P2: - def m(self): - return 23 - class P3: - def m(self): - return 19 - - pluginmanager = PluginManager("xyz") - p1 = P1() - p2 = P2() - p3 = P3() - pluginmanager.register(p1) - pluginmanager.register(p2) - pluginmanager.register(p3) - methods = pluginmanager.listattr('m') - assert methods == [p2.m, p3.m, p1.m] - del P1.m.__dict__['tryfirst'] - pytest.mark.trylast(getattr(P2.m, 'im_func', P2.m)) - methods = pluginmanager.listattr('m') - assert methods == [p2.m, p1.m, p3.m] - def test_namespace_has_default_and_env_plugins(testdir): p = testdir.makepyfile(""" @@ -386,35 +450,6 @@ assert res == [] assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] - def test_listattr_hookwrapper_ordering(self): - class P1: - @pytest.mark.hookwrapper - def m(self): - return 17 - - class P2: - def m(self): - return 23 - - class P3: - @pytest.mark.tryfirst - def m(self): - return 19 - - pluginmanager = PluginManager("xyz") - p1 = P1() - p2 = P2() - p3 = P3() - pluginmanager.register(p1) - pluginmanager.register(p2) - pluginmanager.register(p3) - methods = pluginmanager.listattr('m') - assert methods == [p2.m, p3.m, p1.m] - ## listattr keeps a cache and deleting - ## a function attribute requires clearing it - #pluginmanager._listattrcache.clear() - #del P1.m.__dict__['tryfirst'] - def test_hookwrapper_not_yield(self): def m1(): pass https://bitbucket.org/pytest-dev/pytest/commits/65d700534b5a/ Changeset: 65d700534b5a Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: also incrementally remove plugins from hook callers Affected #: 2 files diff -r 6b9672b337a806cdfc16655f9552f91a50eb62be -r 65d700534b5ab1b32f56a20b8addf60da1e3f54d _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -185,7 +185,7 @@ for plugin in hc.plugins: meth = getattr(plugin, name, None) if meth is not None: - hc._add_method(meth) + hc.add_method(meth) return hc def register(self, plugin, name=None): @@ -219,8 +219,7 @@ del self._name2plugin[name] hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: - hookcaller.plugins.remove(plugin) - hookcaller._scan_methods() + hookcaller.remove_plugin(plugin) def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using @@ -242,7 +241,7 @@ hc.setspec(firstresult=firstresult, argnames=argnames) for plugin in hc.plugins: self._verify_hook(hc, specfunc, plugin) - hc._add_method(getattr(plugin, name)) + hc.add_method(getattr(plugin, name)) names.append(name) if not names: raise ValueError("did not find new %r hooks in %r" @@ -292,7 +291,7 @@ # we have a hook spec, can verify early self._verify_hook(hook, method, plugin) hook.plugins.append(plugin) - hook._add_method(method) + hook.add_method(method) hookcallers.append(hook) return hookcallers @@ -420,13 +419,15 @@ self.argnames = ["__multicall__"] + list(argnames) self.firstresult = firstresult - def _scan_methods(self): - self.wrappers[:] = [] - self.nonwrappers[:] = [] - for plugin in self.plugins: - self._add_method(getattr(plugin, self.name)) + def remove_plugin(self, plugin): + self.plugins.remove(plugin) + meth = getattr(plugin, self.name) + try: + self.nonwrappers.remove(meth) + except ValueError: + self.wrappers.remove(meth) - def _add_method(self, meth): + def add_method(self, meth): assert not self.pre if hasattr(meth, 'hookwrapper'): self.wrappers.append(meth) diff -r 6b9672b337a806cdfc16655f9552f91a50eb62be -r 65d700534b5ab1b32f56a20b8addf60da1e3f54d testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -96,7 +96,7 @@ func.trylast = True if hookwrapper: func.hookwrapper = True - hc._add_method(func) + hc.add_method(func) return func return wrap return addmeth https://bitbucket.org/pytest-dev/pytest/commits/188dd86b0564/ Changeset: 188dd86b0564 Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: remove _do_register indirection between PluginManager and PytestPluginManager Affected #: 3 files diff -r 65d700534b5ab1b32f56a20b8addf60da1e3f54d -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -119,16 +119,13 @@ def register(self, plugin, name=None, conftest=False): ret = super(PytestPluginManager, self).register(plugin, name) - if ret and not conftest: - self._globalplugins.append(plugin) + if ret: + if not conftest: + self._globalplugins.append(plugin) + if hasattr(self, "config"): + self.config._register_plugin(plugin, name) return ret - def _do_register(self, plugin, name): - # called from core PluginManager class - if hasattr(self, "config"): - self.config._register_plugin(plugin, name) - return super(PytestPluginManager, self)._do_register(plugin, name) - def unregister(self, plugin): super(PytestPluginManager, self).unregister(plugin) try: @@ -710,8 +707,7 @@ def _register_plugin(self, plugin, name): call_plugin = self.pluginmanager.call_plugin - call_plugin(plugin, "pytest_addhooks", - {'pluginmanager': self.pluginmanager}) + call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self.pluginmanager}) self.hook.pytest_plugin_registered(plugin=plugin, manager=self.pluginmanager) dic = call_plugin(plugin, "pytest_namespace", {}) or {} diff -r 65d700534b5ab1b32f56a20b8addf60da1e3f54d -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -201,10 +201,6 @@ raise ValueError("Plugin already registered: %s=%s\n%s" %( name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) - # allow subclasses to intercept here by calling a helper - return self._do_register(plugin, name) - - def _do_register(self, plugin, name): self._plugin2hookcallers[plugin] = self._scan_plugin(plugin) self._name2plugin[name] = plugin self._plugins.append(plugin) diff -r 65d700534b5ab1b32f56a20b8addf60da1e3f54d -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -77,6 +77,7 @@ #assert not pm._unverified_hooks assert pm.hook.he_method1(arg=1) == [2] + class TestAddMethodOrdering: @pytest.fixture def hc(self, pm): @@ -283,24 +284,30 @@ pytestpm = get_plugin_manager() # fully initialized with plugins saveindent = [] class api1: - x = 41 def pytest_plugin_registered(self, plugin): saveindent.append(pytestpm.trace.root.indent) - raise ValueError(42) + class api2: + def pytest_plugin_registered(self, plugin): + saveindent.append(pytestpm.trace.root.indent) + raise ValueError() l = [] - pytestpm.set_tracing(l.append) - indent = pytestpm.trace.root.indent - p = api1() - pytestpm.register(p) + undo = pytestpm.set_tracing(l.append) + try: + indent = pytestpm.trace.root.indent + p = api1() + pytestpm.register(p) + assert pytestpm.trace.root.indent == indent + assert len(l) == 2 + assert 'pytest_plugin_registered' in l[0] + assert 'finish' in l[1] - assert pytestpm.trace.root.indent == indent - assert len(l) == 2 - assert 'pytest_plugin_registered' in l[0] - assert 'finish' in l[1] - with pytest.raises(ValueError): - pytestpm.register(api1()) - assert pytestpm.trace.root.indent == indent - assert saveindent[0] > indent + l[:] = [] + with pytest.raises(ValueError): + pytestpm.register(api2()) + assert pytestpm.trace.root.indent == indent + assert saveindent[0] > indent + finally: + undo() def test_namespace_has_default_and_env_plugins(testdir): https://bitbucket.org/pytest-dev/pytest/commits/c1729eda15e9/ Changeset: c1729eda15e9 Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: add documented hookimpl_opts and hookspec_opts decorators so that one doesn't have to use pytest.mark or function-attribute setting anymore Affected #: 21 files diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,13 @@ change but it might still break 3rd party plugins which relied on details like especially the pluginmanager.add_shutdown() API. Thanks Holger Krekel. + +- pluginmanagement: introduce ``pytest.hookimpl_opts`` and + ``pytest.hookspec_opts`` decorators for setting impl/spec + specific parameters. This substitutes the previous + now deprecated use of ``pytest.mark`` which is meant to + contain markers for test functions only. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -29,7 +29,7 @@ help="shortcut for --capture=no.") - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_load_initial_conftests(early_config, parser, args): ns = early_config.known_args_namespace pluginmanager = early_config.pluginmanager @@ -101,7 +101,7 @@ if capfuncarg is not None: capfuncarg.close() - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_make_collect_report(self, collector): if isinstance(collector, pytest.File): self.resumecapture() @@ -115,13 +115,13 @@ else: yield - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_setup(self, item): self.resumecapture() yield self.suspendcapture_item(item, "setup") - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_call(self, item): self.resumecapture() self.activate_funcargs(item) @@ -129,17 +129,17 @@ #self.deactivate_funcargs() called from suspendcapture() self.suspendcapture_item(item, "call") - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_teardown(self, item): self.resumecapture() yield self.suspendcapture_item(item, "teardown") - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_keyboard_interrupt(self, excinfo): self.reset_capturings() - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_internalerror(self, excinfo): self.reset_capturings() diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -7,6 +7,52 @@ py3 = sys.version_info > (3,0) +def hookspec_opts(firstresult=False): + """ returns a decorator which will define a function as a hook specfication. + + If firstresult is True the 1:N hook call (N being the number of registered + hook implementation functions) will stop at I<=N when the I'th function + returns a non-None result. + """ + def setattr_hookspec_opts(func): + if firstresult: + func.firstresult = firstresult + return func + return setattr_hookspec_opts + + +def hookimpl_opts(hookwrapper=False, optionalhook=False, + tryfirst=False, trylast=False): + """ Return a decorator which marks a function as a hook implementation. + + If optionalhook is True a missing matching hook specification will not result + in an error (by default it is an error if no matching spec is found). + + If tryfirst is True this hook implementation will run as early as possible + in the chain of N hook implementations for a specfication. + + If trylast is True this hook implementation will run as late as possible + in the chain of N hook implementations. + + If hookwrapper is True the hook implementations needs to execute exactly + one "yield". The code before the yield is run early before any non-hookwrapper + function is run. The code after the yield is run after all non-hookwrapper + function have run. The yield receives an ``CallOutcome`` object representing + the exception or result outcome of the inner calls (including other hookwrapper + calls). + """ + def setattr_hookimpl_opts(func): + if hookwrapper: + func.hookwrapper = True + if optionalhook: + func.optionalhook = True + if tryfirst: + func.tryfirst = True + if trylast: + func.trylast = True + return func + return setattr_hookimpl_opts + class TagTracer: def __init__(self): self._tag2proc = {} diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -22,7 +22,7 @@ help="store internal tracing debug information in 'pytestdebug.log'.") - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_cmdline_parse(): outcome = yield config = outcome.get_result() diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -1,5 +1,7 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ +from _pytest.core import hookspec_opts + # ------------------------------------------------------------------------- # Initialization # ------------------------------------------------------------------------- @@ -15,9 +17,9 @@ are parsed. """ + at hookspec_opts(firstresult=True) def pytest_cmdline_parse(pluginmanager, args): """return initialized config object, parsing the specified args. """ -pytest_cmdline_parse.firstresult = True def pytest_cmdline_preparse(config, args): """(deprecated) modify command line arguments before option parsing. """ @@ -47,10 +49,10 @@ via (deprecated) ``pytest.config``. """ + at hookspec_opts(firstresult=True) def pytest_cmdline_main(config): """ called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. """ -pytest_cmdline_main.firstresult = True def pytest_load_initial_conftests(args, early_config, parser): """ implements the loading of initial conftest files ahead @@ -64,18 +66,18 @@ def pytest_unconfigure(config): """ called before test process is exited. """ + at hookspec_opts(firstresult=True) def pytest_runtestloop(session): """ called for performing the main runtest loop (after collection finished). """ -pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_collection(session): """ perform the collection protocol for the given session. """ -pytest_collection.firstresult = True def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order @@ -84,16 +86,16 @@ def pytest_collection_finish(session): """ called after collection has been performed and modified. """ + at hookspec_opts(firstresult=True) def pytest_ignore_collect(path, config): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. """ -pytest_ignore_collect.firstresult = True + at hookspec_opts(firstresult=True) def pytest_collect_directory(path, parent): """ called before traversing a directory for collection files. """ -pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): """ return collection Node or None for the given path. Any new node @@ -112,29 +114,29 @@ def pytest_deselected(items): """ called for test items deselected by keyword. """ + at hookspec_opts(firstresult=True) def pytest_make_collect_report(collector): """ perform ``collector.collect()`` and return a CollectReport. """ -pytest_make_collect_report.firstresult = True # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_pycollect_makemodule(path, parent): """ return a Module collector or None for the given path. This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to create test modules for files that do not match as a test module. """ -pytest_pycollect_makemodule.firstresult = True + at hookspec_opts(firstresult=True) def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ -pytest_pycollect_makeitem.firstresult = True + at hookspec_opts(firstresult=True) def pytest_pyfunc_call(pyfuncitem): """ call underlying test function. """ -pytest_pyfunc_call.firstresult = True def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" @@ -145,6 +147,7 @@ def pytest_itemstart(item, node): """ (deprecated, use pytest_runtest_logstart). """ + at hookspec_opts(firstresult=True) def pytest_runtest_protocol(item, nextitem): """ implements the runtest_setup/call/teardown protocol for the given test item, including capturing exceptions and calling @@ -158,7 +161,6 @@ :return boolean: True if no further hook implementations should be invoked. """ -pytest_runtest_protocol.firstresult = True def pytest_runtest_logstart(nodeid, location): """ signal the start of running a single test item. """ @@ -178,12 +180,12 @@ so that nextitem only needs to call setup-functions. """ + at hookspec_opts(firstresult=True) def pytest_runtest_makereport(item, call): """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item` and :py:class:`_pytest.runner.CallInfo`. """ -pytest_runtest_makereport.firstresult = True def pytest_runtest_logreport(report): """ process a test setup/call/teardown report relating to @@ -220,9 +222,9 @@ def pytest_report_header(config, startdir): """ return a string to be displayed as header info for terminal reporting.""" + at hookspec_opts(firstresult=True) def pytest_report_teststatus(report): """ return result-category, shortletter and verbose word for reporting.""" -pytest_report_teststatus.firstresult = True def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ @@ -236,9 +238,9 @@ # doctest hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_doctest_prepare_content(content): """ return processed content for a given doctest""" -pytest_doctest_prepare_content.firstresult = True # ------------------------------------------------------------------------- # error handling and internal debugging hooks diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -519,12 +519,12 @@ def _makeid(self): return "" - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_collectstart(self): if self.shouldstop: raise self.Interrupted(self.shouldstop) - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): self._testsfailed += 1 diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/nose.py --- a/_pytest/nose.py +++ b/_pytest/nose.py @@ -24,7 +24,7 @@ call.excinfo = call2.excinfo - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_runtest_setup(item): if is_potential_nosetest(item): if isinstance(item.parent, pytest.Generator): diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/pastebin.py --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -11,7 +11,7 @@ choices=['failed', 'all'], help="send failed|all info to bpaste.net pastebin service.") - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_configure(config): if config.option.pastebin == "all": tr = config.pluginmanager.getplugin('terminalreporter') diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -172,7 +172,7 @@ def pytest_sessionstart(session): session._fixturemanager = FixtureManager(session) - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_namespace(): raises.Exception = pytest.fail.Exception return { @@ -191,7 +191,7 @@ return request.config - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): @@ -219,7 +219,7 @@ def pytest_pycollect_makemodule(path, parent): return Module(path, parent) - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() @@ -1667,7 +1667,7 @@ self.parsefactories(plugin, nodeid) self._seenplugins.add(plugin) - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_configure(self, config): plugins = config.pluginmanager.getplugins() for plugin in plugins: diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/skipping.py --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -133,7 +133,7 @@ return expl - at pytest.mark.tryfirst + at pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_setup(item): evalskip = MarkEvaluator(item, 'skipif') if evalskip.istrue(): @@ -151,7 +151,7 @@ if not evalxfail.get('run', True): pytest.xfail("[NOTRUN] " + evalxfail.getexplanation()) - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -265,7 +265,7 @@ def pytest_collection_modifyitems(self): self.report_collect(True) - @pytest.mark.trylast + @pytest.hookimpl_opts(trylast=True) def pytest_sessionstart(self, session): self._sessionstarttime = time.time() if not self.showheader: @@ -350,7 +350,7 @@ indent = (len(stack) - 1) * " " self._tw.line("%s%s" % (indent, col)) - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): outcome = yield outcome.get_result() diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b _pytest/unittest.py --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -140,7 +140,7 @@ if traceback: excinfo.traceback = traceback - at pytest.mark.tryfirst + at pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call): if isinstance(item, TestCaseFunction): if item._excinfo: @@ -152,7 +152,7 @@ # twisted trial support - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_protocol(item): if isinstance(item, TestCaseFunction) and \ 'twisted.trial.unittest' in sys.modules: diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b doc/en/example/markers.txt --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -201,9 +201,9 @@ @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. For an example on how to add and work with markers from a plugin, see @@ -375,9 +375,9 @@ @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. Reading markers which were set from multiple places diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b doc/en/example/simple.txt --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.txt @@ -534,7 +534,7 @@ import pytest import os.path - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call, __multicall__): # execute all other hooks to obtain the report object rep = __multicall__.execute() @@ -607,7 +607,7 @@ import pytest - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call, __multicall__): # execute all other hooks to obtain the report object rep = __multicall__.execute() diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b doc/en/plugins.txt --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -458,7 +458,7 @@ import pytest - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): # do whatever you want before the next hook executes outcome = yield diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b pytest.py --- a/pytest.py +++ b/pytest.py @@ -12,6 +12,7 @@ # else we are imported from _pytest.config import main, UsageError, _preloadplugins, cmdline +from _pytest.core import hookspec_opts, hookimpl_opts from _pytest import __version__ _preloadplugins() # to populate pytest.* namespace so help(pytest) works diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b testing/conftest.py --- a/testing/conftest.py +++ b/testing/conftest.py @@ -66,7 +66,7 @@ error.append(error[0]) raise AssertionError("\n".join(error)) - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_runtest_teardown(item, __multicall__): item.config._basedir.chdir() if hasattr(item.config, '_openfiles'): diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -563,7 +563,7 @@ b = testdir.mkdir("a").mkdir("b") b.join("conftest.py").write(py.code.Source(""" import pytest - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_pycollect_makeitem(): outcome = yield if outcome.excinfo is None: diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -192,6 +192,53 @@ assert hc.nonwrappers == [he_method1_middle] assert hc.wrappers == [he_method1, he_method3] + def test_hookspec_opts(self, pm): + class HookSpec: + @hookspec_opts() + def he_myhook1(self, arg1): + pass + + @hookspec_opts(firstresult=True) + def he_myhook2(self, arg1): + pass + + @hookspec_opts(firstresult=False) + def he_myhook3(self, arg1): + pass + + pm.addhooks(HookSpec) + assert not pm.hook.he_myhook1.firstresult + assert pm.hook.he_myhook2.firstresult + assert not pm.hook.he_myhook3.firstresult + + + def test_hookimpl_opts(self): + for name in ["hookwrapper", "optionalhook", "tryfirst", "trylast"]: + for val in [True, False]: + @hookimpl_opts(**{name: val}) + def he_myhook1(self, arg1): + pass + if val: + assert getattr(he_myhook1, name) + else: + assert not hasattr(he_myhook1, name) + + def test_decorator_functional(self, pm): + class HookSpec: + @hookspec_opts(firstresult=True) + def he_myhook(self, arg1): + """ add to arg1 """ + pm.addhooks(HookSpec) + + class Plugin: + @hookimpl_opts() + def he_myhook(self, arg1): + return arg1 + 1 + + pm.register(Plugin()) + results = pm.hook.he_myhook(arg1=17) + assert results == 18 + class TestPytestPluginInteractions: diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b testing/test_helpconfig.py --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -38,7 +38,7 @@ def test_hookvalidation_optional(testdir): testdir.makeconftest(""" import pytest - @pytest.mark.optionalhook + @pytest.hookimpl_opts(optionalhook=True) def pytest_hello(xyz): pass """) diff -r 188dd86b05649a47cfce5c1d908762bd6d5cc84f -r c1729eda15e9f93e674437977b8a724aabf34d9b testing/test_mark.py --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -510,7 +510,7 @@ """) testdir.makepyfile(conftest=""" import pytest - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_pycollect_makeitem(name): outcome = yield if name == "TestClass": https://bitbucket.org/pytest-dev/pytest/commits/34b1a2d8b232/ Changeset: 34b1a2d8b232 Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: split plugin documentation into "using" and "writing plugins", referencing each other. Also add tryfirst/trylast examples. Affected #: 4 files diff -r c1729eda15e9f93e674437977b8a724aabf34d9b -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,9 @@ now deprecated use of ``pytest.mark`` which is meant to contain markers for test functions only. +- write/refine docs for "writing plugins" which now have their + own page and are separate from the "using/installing plugins`` page. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r c1729eda15e9f93e674437977b8a724aabf34d9b -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -56,6 +56,7 @@ - all collection, reporting, running aspects are delegated to hook functions - customizations can be per-directory, per-project or per PyPI released plugin - it is easy to add command line options or customize existing behaviour + - :ref:`easy to write your own plugins ` .. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html diff -r c1729eda15e9f93e674437977b8a724aabf34d9b -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a doc/en/plugins.txt --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -1,64 +1,14 @@ -.. _plugins: - -Working with plugins and conftest files -======================================= - -``pytest`` implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic location types: - -* `builtin plugins`_: loaded from pytest's internal ``_pytest`` directory. -* `external plugins`_: modules discovered through `setuptools entry points`_ -* `conftest.py plugins`_: modules auto-discovered in test directories - -.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ -.. _`conftest.py plugins`: -.. _`conftest.py`: -.. _`localplugin`: -.. _`conftest`: - -conftest.py: local per-directory plugins ----------------------------------------- - -local ``conftest.py`` plugins contain directory-specific hook -implementations. Session and test running activities will -invoke all hooks defined in ``conftest.py`` files closer to the -root of the filesystem. Example: Assume the following layout -and content of files:: - - a/conftest.py: - def pytest_runtest_setup(item): - # called for running each test in 'a' directory - print ("setting up", item) - - a/test_sub.py: - def test_sub(): - pass - - test_flat.py: - def test_flat(): - pass - -Here is how you might run it:: - - py.test test_flat.py # will not show "setting up" - py.test a/test_sub.py # will show "setting up" - -.. Note:: - If you have ``conftest.py`` files which do not reside in a - python package directory (i.e. one containing an ``__init__.py``) then - "import conftest" can be ambiguous because there might be other - ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. - It is thus good practise for projects to either put ``conftest.py`` - under a package scope or to never import anything from a - conftest.py file. - .. _`external plugins`: .. _`extplugins`: +.. _`using plugins`: -Installing External Plugins / Searching ---------------------------------------- +Installing and Using plugins +============================ -Installing a plugin happens through any usual Python installation -tool, for example:: +This section talks about installing and using third party plugins. +For writing your own plugins, please refer to :ref:`writing-plugins`. + +Installing a third party plugin can be easily done with ``pip``:: pip install pytest-NAME pip uninstall pytest-NAME @@ -120,118 +70,20 @@ .. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search -Writing a plugin by looking at examples ---------------------------------------- - -.. _`setuptools`: http://pypi.python.org/pypi/setuptools - -If you want to write a plugin, there are many real-life examples -you can copy from: - -* a custom collection example plugin: :ref:`yaml plugin` -* around 20 `builtin plugins`_ which provide pytest's own functionality -* many `external plugins`_ providing additional features - -All of these plugins implement the documented `well specified hooks`_ -to extend and add functionality. - -You can also :ref:`contribute your plugin to pytest-dev` -once it has some happy users other than yourself. - - -.. _`setuptools entry points`: - -Making your plugin installable by others ----------------------------------------- - -If you want to make your plugin externally available, you -may define a so-called entry point for your distribution so -that ``pytest`` finds your plugin module. Entry points are -a feature that is provided by `setuptools`_. pytest looks up -the ``pytest11`` entrypoint to discover its -plugins and you can thus make your plugin available by defining -it in your setuptools-invocation: - -.. sourcecode:: python - - # sample ./setup.py file - from setuptools import setup - - setup( - name="myproject", - packages = ['myproject'] - - # the following makes a plugin available to pytest - entry_points = { - 'pytest11': [ - 'name_of_plugin = myproject.pluginmodule', - ] - }, - ) - -If a package is installed this way, ``pytest`` will load -``myproject.pluginmodule`` as a plugin which can define -`well specified hooks`_. - - -.. _`pluginorder`: - -Plugin discovery order at tool startup --------------------------------------- - -``pytest`` loads plugin modules at tool startup in the following way: - -* by loading all builtin plugins - -* by loading all plugins registered through `setuptools entry points`_. - -* by pre-scanning the command line for the ``-p name`` option - and loading the specified plugin before actual command line parsing. - -* by loading all :file:`conftest.py` files as inferred by the command line - invocation: - - - if no test paths are specified use current dir as a test path - - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative - to the directory part of the first test path. - - Note that pytest does not find ``conftest.py`` files in deeper nested - sub directories at tool startup. It is usually a good idea to keep - your conftest.py file in the top level test or project root directory. - -* by recursively loading all plugins specified by the - ``pytest_plugins`` variable in ``conftest.py`` files - - Requiring/Loading plugins in a test module or conftest file ----------------------------------------------------------- You can require plugins in a test module or a conftest file like this:: - pytest_plugins = "name1", "name2", + pytest_plugins = "myapp.testsupport.myplugin", When the test module or conftest plugin is loaded the specified plugins -will be loaded as well. You can also use dotted path like this:: +will be loaded as well. pytest_plugins = "myapp.testsupport.myplugin" which will import the specified module as a ``pytest`` plugin. - -Accessing another plugin by name --------------------------------- - -If a plugin wants to collaborate with code from -another plugin it can obtain a reference through -the plugin manager like this: - -.. sourcecode:: python - - plugin = config.pluginmanager.getplugin("name_of_plugin") - -If you want to look at the names of existing plugins, use -the ``--traceconfig`` option. - .. _`findpluginname`: Finding out which plugins are active @@ -293,223 +145,3 @@ _pytest.tmpdir _pytest.unittest -.. _`well specified hooks`: - -pytest hook reference -===================== - -Hook specification and validation ---------------------------------- - -``pytest`` calls hook functions to implement initialization, running, -test execution and reporting. When ``pytest`` loads a plugin it validates -that each hook function conforms to its respective hook specification. -Each hook function name and its argument names need to match a hook -specification. However, a hook function may accept *fewer* parameters -by simply not specifying them. If you mistype argument names or the -hook name itself you get an error showing the available arguments. - -Initialization, command line and configuration hooks ----------------------------------------------------- - -.. currentmodule:: _pytest.hookspec - -.. autofunction:: pytest_load_initial_conftests -.. autofunction:: pytest_cmdline_preparse -.. autofunction:: pytest_cmdline_parse -.. autofunction:: pytest_namespace -.. autofunction:: pytest_addoption -.. autofunction:: pytest_cmdline_main -.. autofunction:: pytest_configure -.. autofunction:: pytest_unconfigure - -Generic "runtest" hooks ------------------------ - -All runtest related hooks receive a :py:class:`pytest.Item` object. - -.. autofunction:: pytest_runtest_protocol -.. autofunction:: pytest_runtest_setup -.. autofunction:: pytest_runtest_call -.. autofunction:: pytest_runtest_teardown -.. autofunction:: pytest_runtest_makereport - -For deeper understanding you may look at the default implementation of -these hooks in :py:mod:`_pytest.runner` and maybe also -in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture` -and its input/output capturing in order to immediately drop -into interactive debugging when a test failure occurs. - -The :py:mod:`_pytest.terminal` reported specifically uses -the reporting hook to print information about a test run. - -Collection hooks ----------------- - -``pytest`` calls the following hooks for collecting files and directories: - -.. autofunction:: pytest_ignore_collect -.. autofunction:: pytest_collect_directory -.. autofunction:: pytest_collect_file - -For influencing the collection of objects in Python modules -you can use the following hook: - -.. autofunction:: pytest_pycollect_makeitem -.. autofunction:: pytest_generate_tests - -After collection is complete, you can modify the order of -items, delete or otherwise amend the test items: - -.. autofunction:: pytest_collection_modifyitems - -Reporting hooks ---------------- - -Session related reporting hooks: - -.. autofunction:: pytest_collectstart -.. autofunction:: pytest_itemcollected -.. autofunction:: pytest_collectreport -.. autofunction:: pytest_deselected - -And here is the central hook for reporting about -test execution: - -.. autofunction:: pytest_runtest_logreport - - -Debugging/Interaction hooks ---------------------------- - -There are few hooks which can be used for special -reporting or interaction with exceptions: - -.. autofunction:: pytest_internalerror -.. autofunction:: pytest_keyboard_interrupt -.. autofunction:: pytest_exception_interact - - -Declaring new hooks ------------------------- - -Plugins and ``conftest.py`` files may declare new hooks that can then be -implemented by other plugins in order to alter behaviour or interact with -the new plugin: - -.. autofunction:: pytest_addhooks - -Hooks are usually declared as do-nothing functions that contain only -documentation describing when the hook will be called and what return values -are expected. - -For an example, see `newhooks.py`_ from :ref:`xdist`. - -.. _`newhooks.py`: https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default - - -Using hooks from 3rd party plugins -------------------------------------- - -Using new hooks from plugins as explained above might be a little tricky -because the standard `Hook specification and validation`_ mechanism: -if you depend on a plugin that is not installed, -validation will fail and the error message will not make much sense to your users. - -One approach is to defer the hook implementation to a new plugin instead of -declaring the hook functions directly in your plugin module, for example:: - - # contents of myplugin.py - - class DeferPlugin(object): - """Simple plugin to defer pytest-xdist hook functions.""" - - def pytest_testnodedown(self, node, error): - """standard xdist hook function. - """ - - def pytest_configure(config): - if config.pluginmanager.hasplugin('xdist'): - config.pluginmanager.register(DeferPlugin()) - - -This has the added benefit of allowing you to conditionally install hooks -depending on which plugins are installed. - -hookwrapper: executing around other hooks -------------------------------------------------- - -.. currentmodule:: _pytest.core - -.. versionadded:: 2.7 (experimental) - -pytest plugins can implement hook wrappers which which wrap the execution -of other hook implementations. A hook wrapper is a generator function -which yields exactly once. When pytest invokes hooks it first executes -hook wrappers and passes the same arguments as to the regular hooks. - -At the yield point of the hook wrapper pytest will execute the next hook -implementations and return their result to the yield point in the form of -a :py:class:`CallOutcome` instance which encapsulates a result or -exception info. The yield point itself will thus typically not raise -exceptions (unless there are bugs). - -Here is an example definition of a hook wrapper:: - - import pytest - - @pytest.hookimpl_opts(hookwrapper=True) - def pytest_pyfunc_call(pyfuncitem): - # do whatever you want before the next hook executes - outcome = yield - # outcome.excinfo may be None or a (cls, val, tb) tuple - res = outcome.get_result() # will raise if outcome was exception - # postprocess result - -Note that hook wrappers don't return results themselves, they merely -perform tracing or other side effects around the actual hook implementations. -If the result of the underlying hook is a mutable object, they may modify -that result, however. - - -Reference of objects involved in hooks -====================================== - -.. autoclass:: _pytest.config.Config() - :members: - -.. autoclass:: _pytest.config.Parser() - :members: - -.. autoclass:: _pytest.main.Node() - :members: - -.. autoclass:: _pytest.main.Collector() - :members: - :show-inheritance: - -.. autoclass:: _pytest.main.Item() - :members: - :show-inheritance: - -.. autoclass:: _pytest.python.Module() - :members: - :show-inheritance: - -.. autoclass:: _pytest.python.Class() - :members: - :show-inheritance: - -.. autoclass:: _pytest.python.Function() - :members: - :show-inheritance: - -.. autoclass:: _pytest.runner.CallInfo() - :members: - -.. autoclass:: _pytest.runner.TestReport() - :members: - -.. autoclass:: _pytest.core.CallOutcome() - :members: - diff -r c1729eda15e9f93e674437977b8a724aabf34d9b -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a doc/en/writing_plugins.txt --- /dev/null +++ b/doc/en/writing_plugins.txt @@ -0,0 +1,469 @@ +.. _plugins: +.. _`writing-plugins`: + +Writing plugins +=============== + +It is easy to implement `local conftest plugins`_ for your own project +or `pip-installable plugins`_ that can be used throughout many projects, +including third party projects. Please refer to :ref:`using plugins` if you +only want to use but not write plugins. + +A plugin contains one or multiple hook functions. :ref:`Writing hooks ` +explains the basics and details of how you can write a hook function yourself. +``pytest`` implements all aspects of configuration, collection, running and +reporting by calling `well specified hooks`_ of the following plugins: + +* :ref:`builtin plugins`: loaded from pytest's internal ``_pytest`` directory. + +* :ref:`external plugins `: modules discovered through + `setuptools entry points`_ + +* `conftest.py plugins`_: modules auto-discovered in test directories + +In principle, each hook call is a ``1:N`` Python function call where ``N`` is the +number of registered implementation functions for a given specification. +All specifications and implementations following the ``pytest_`` prefix +naming convention, making them easy to distinguish and find. + +.. _`pluginorder`: + +Plugin discovery order at tool startup +-------------------------------------- + +``pytest`` loads plugin modules at tool startup in the following way: + +* by loading all builtin plugins + +* by loading all plugins registered through `setuptools entry points`_. + +* by pre-scanning the command line for the ``-p name`` option + and loading the specified plugin before actual command line parsing. + +* by loading all :file:`conftest.py` files as inferred by the command line + invocation: + + - if no test paths are specified use current dir as a test path + - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative + to the directory part of the first test path. + + Note that pytest does not find ``conftest.py`` files in deeper nested + sub directories at tool startup. It is usually a good idea to keep + your conftest.py file in the top level test or project root directory. + +* by recursively loading all plugins specified by the + ``pytest_plugins`` variable in ``conftest.py`` files + + +.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ +.. _`conftest.py plugins`: +.. _`conftest.py`: +.. _`localplugin`: +.. _`conftest`: +.. _`local conftest plugins`: + +conftest.py: local per-directory plugins +---------------------------------------- + +Local ``conftest.py`` plugins contain directory-specific hook +implementations. Hook Session and test running activities will +invoke all hooks defined in ``conftest.py`` files closer to the +root of the filesystem. Example of implementing the +``pytest_runtest_setup`` hook so that is called for tests in the ``a`` +sub directory but not for other directories:: + + a/conftest.py: + def pytest_runtest_setup(item): + # called for running each test in 'a' directory + print ("setting up", item) + + a/test_sub.py: + def test_sub(): + pass + + test_flat.py: + def test_flat(): + pass + +Here is how you might run it:: + + py.test test_flat.py # will not show "setting up" + py.test a/test_sub.py # will show "setting up" + +.. Note:: + If you have ``conftest.py`` files which do not reside in a + python package directory (i.e. one containing an ``__init__.py``) then + "import conftest" can be ambiguous because there might be other + ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. + It is thus good practise for projects to either put ``conftest.py`` + under a package scope or to never import anything from a + conftest.py file. + + +Writing a plugin by looking at examples +--------------------------------------- + +.. _`setuptools`: http://pypi.python.org/pypi/setuptools + +If you want to write a plugin, there are many real-life examples +you can copy from: + +* a custom collection example plugin: :ref:`yaml plugin` +* around 20 doc:`builtin plugins` which provide pytest's own functionality +* many :doc:`external plugins` providing additional features + +All of these plugins implement the documented `well specified hooks`_ +to extend and add functionality. + +You can also :ref:`contribute your plugin to pytest-dev` +once it has some happy users other than yourself. + + +.. _`setuptools entry points`: +.. _`pip-installable plugins`: + +Making your plugin installable by others +---------------------------------------- + +If you want to make your plugin externally available, you +may define a so-called entry point for your distribution so +that ``pytest`` finds your plugin module. Entry points are +a feature that is provided by `setuptools`_. pytest looks up +the ``pytest11`` entrypoint to discover its +plugins and you can thus make your plugin available by defining +it in your setuptools-invocation: + +.. sourcecode:: python + + # sample ./setup.py file + from setuptools import setup + + setup( + name="myproject", + packages = ['myproject'] + + # the following makes a plugin available to pytest + entry_points = { + 'pytest11': [ + 'name_of_plugin = myproject.pluginmodule', + ] + }, + ) + +If a package is installed this way, ``pytest`` will load +``myproject.pluginmodule`` as a plugin which can define +`well specified hooks`_. + + + + +Requiring/Loading plugins in a test module or conftest file +----------------------------------------------------------- + +You can require plugins in a test module or a conftest file like this:: + + pytest_plugins = "name1", "name2", + +When the test module or conftest plugin is loaded the specified plugins +will be loaded as well. You can also use dotted path like this:: + + pytest_plugins = "myapp.testsupport.myplugin" + +which will import the specified module as a ``pytest`` plugin. + + +Accessing another plugin by name +-------------------------------- + +If a plugin wants to collaborate with code from +another plugin it can obtain a reference through +the plugin manager like this: + +.. sourcecode:: python + + plugin = config.pluginmanager.getplugin("name_of_plugin") + +If you want to look at the names of existing plugins, use +the ``--traceconfig`` option. + + +.. _`writinghooks`: + +Writing hook functions +====================== + +.. _validation: + +hook function validation and execution +-------------------------------------- + +pytest calls hook functions from registered plugins for any +given hook specification. Let's look at a typical hook function +for the ``pytest_collection_modifyitems(session, config, +items)`` hook which pytest calls after collection of all test items is +completed. + +When we implement a ``pytest_collection_modifyitems`` function in our plugin +pytest will during registration verify that you use argument +names which match the specification and bail out if not. + +Let's look at a possible implementation:: + + def pytest_collection_modifyitems(config, items): + # called after collectin is completed + # you can modify the ``items`` list + +Here, ``pytest`` will pass in ``config`` (the pytest config object) +and ``items`` (the list of collected test items) but will not pass +in the ``session`` argument because we didn't list it in the function +signature. This dynamic "pruning" of arguments allows ``pytest`` to +be "future-compatible": we can introduce new hook named parameters without +breaking the signatures of existing hook implementations. It is one of +the reasons for the general long-lived compatibility of pytest plugins. + +Hook function results +--------------------- + +Most calls to ``pytest`` hooks result in a **list of results** which contains +all non-None results of the called hook functions. + +Some hooks are specified so that the hook call only executes until the +first function returned a non-None value which is then also the +result of the overall hook call. The remaining hook functions will +not be called in this case. + +Note that hook functions other than ``pytest_runtest_*`` are not +allowed to raise exceptions. Doing so will break the pytest run. + +Hook function ordering +---------------------- + +For any given hook there may be more than one implementation and we thus +generally view ``hook`` execution as a ``1:N`` function call where ``N`` +is the number of registered functions. There are ways to +influence if a hook implementation comes before or after others, i.e. +the position in the ``N``-sized list of functions:: + + @pytest.hookimpl_spec(tryfirst=True) + def pytest_collection_modifyitems(items): + # will execute as early as possible + + @pytest.hookimpl_spec(trylast=True) + def pytest_collection_modifyitems(items): + # will execute as late as possible + + +hookwrapper: executing around other hooks +------------------------------------------------- + +.. currentmodule:: _pytest.core + +.. versionadded:: 2.7 (experimental) + +pytest plugins can implement hook wrappers which wrap the execution +of other hook implementations. A hook wrapper is a generator function +which yields exactly once. When pytest invokes hooks it first executes +hook wrappers and passes the same arguments as to the regular hooks. + +At the yield point of the hook wrapper pytest will execute the next hook +implementations and return their result to the yield point in the form of +a :py:class:`CallOutcome` instance which encapsulates a result or +exception info. The yield point itself will thus typically not raise +exceptions (unless there are bugs). + +Here is an example definition of a hook wrapper:: + + import pytest + + @pytest.hookimpl_opts(hookwrapper=True) + def pytest_pyfunc_call(pyfuncitem): + # do whatever you want before the next hook executes + + outcome = yield + # outcome.excinfo may be None or a (cls, val, tb) tuple + + res = outcome.get_result() # will raise if outcome was exception + # postprocess result + +Note that hook wrappers don't return results themselves, they merely +perform tracing or other side effects around the actual hook implementations. +If the result of the underlying hook is a mutable object, they may modify +that result, however. + +Declaring new hooks +------------------------ + +.. currentmodule:: _pytest.hookspec + +Plugins and ``conftest.py`` files may declare new hooks that can then be +implemented by other plugins in order to alter behaviour or interact with +the new plugin: + +.. autofunction:: pytest_addhooks + +Hooks are usually declared as do-nothing functions that contain only +documentation describing when the hook will be called and what return values +are expected. + +For an example, see `newhooks.py`_ from :ref:`xdist`. + +.. _`newhooks.py`: https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default + + +Using hooks from 3rd party plugins +------------------------------------- + +Using new hooks from plugins as explained above might be a little tricky +because the standard :ref:`validation mechanism `: +if you depend on a plugin that is not installed, validation will fail and +the error message will not make much sense to your users. + +One approach is to defer the hook implementation to a new plugin instead of +declaring the hook functions directly in your plugin module, for example:: + + # contents of myplugin.py + + class DeferPlugin(object): + """Simple plugin to defer pytest-xdist hook functions.""" + + def pytest_testnodedown(self, node, error): + """standard xdist hook function. + """ + + def pytest_configure(config): + if config.pluginmanager.hasplugin('xdist'): + config.pluginmanager.register(DeferPlugin()) + +This has the added benefit of allowing you to conditionally install hooks +depending on which plugins are installed. + + +.. _`well specified hooks`: + +.. currentmodule:: _pytest.hookspec + +pytest hook reference +===================== + + +Initialization, command line and configuration hooks +---------------------------------------------------- + +.. autofunction:: pytest_load_initial_conftests +.. autofunction:: pytest_cmdline_preparse +.. autofunction:: pytest_cmdline_parse +.. autofunction:: pytest_namespace +.. autofunction:: pytest_addoption +.. autofunction:: pytest_cmdline_main +.. autofunction:: pytest_configure +.. autofunction:: pytest_unconfigure + +Generic "runtest" hooks +----------------------- + +All runtest related hooks receive a :py:class:`pytest.Item` object. + +.. autofunction:: pytest_runtest_protocol +.. autofunction:: pytest_runtest_setup +.. autofunction:: pytest_runtest_call +.. autofunction:: pytest_runtest_teardown +.. autofunction:: pytest_runtest_makereport + +For deeper understanding you may look at the default implementation of +these hooks in :py:mod:`_pytest.runner` and maybe also +in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture` +and its input/output capturing in order to immediately drop +into interactive debugging when a test failure occurs. + +The :py:mod:`_pytest.terminal` reported specifically uses +the reporting hook to print information about a test run. + +Collection hooks +---------------- + +``pytest`` calls the following hooks for collecting files and directories: + +.. autofunction:: pytest_ignore_collect +.. autofunction:: pytest_collect_directory +.. autofunction:: pytest_collect_file + +For influencing the collection of objects in Python modules +you can use the following hook: + +.. autofunction:: pytest_pycollect_makeitem +.. autofunction:: pytest_generate_tests + +After collection is complete, you can modify the order of +items, delete or otherwise amend the test items: + +.. autofunction:: pytest_collection_modifyitems + +Reporting hooks +--------------- + +Session related reporting hooks: + +.. autofunction:: pytest_collectstart +.. autofunction:: pytest_itemcollected +.. autofunction:: pytest_collectreport +.. autofunction:: pytest_deselected + +And here is the central hook for reporting about +test execution: + +.. autofunction:: pytest_runtest_logreport + + +Debugging/Interaction hooks +--------------------------- + +There are few hooks which can be used for special +reporting or interaction with exceptions: + +.. autofunction:: pytest_internalerror +.. autofunction:: pytest_keyboard_interrupt +.. autofunction:: pytest_exception_interact + + + +Reference of objects involved in hooks +====================================== + +.. autoclass:: _pytest.config.Config() + :members: + +.. autoclass:: _pytest.config.Parser() + :members: + +.. autoclass:: _pytest.main.Node() + :members: + +.. autoclass:: _pytest.main.Collector() + :members: + :show-inheritance: + +.. autoclass:: _pytest.main.Item() + :members: + :show-inheritance: + +.. autoclass:: _pytest.python.Module() + :members: + :show-inheritance: + +.. autoclass:: _pytest.python.Class() + :members: + :show-inheritance: + +.. autoclass:: _pytest.python.Function() + :members: + :show-inheritance: + +.. autoclass:: _pytest.runner.CallInfo() + :members: + +.. autoclass:: _pytest.runner.TestReport() + :members: + +.. autoclass:: _pytest.core.CallOutcome() + :members: + https://bitbucket.org/pytest-dev/pytest/commits/2df6e23821fb/ Changeset: 2df6e23821fb Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: introduce historic hook spec which will memorize calls to a hook in order to call them on later registered plugins Affected #: 4 files diff -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a -r 2df6e23821fb7973900905b8935e35e018df0f8f _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -122,8 +122,8 @@ if ret: if not conftest: self._globalplugins.append(plugin) - if hasattr(self, "config"): - self.config._register_plugin(plugin, name) + self.hook.pytest_plugin_registered(plugin=plugin, + manager=self) return ret def unregister(self, plugin): @@ -704,19 +704,11 @@ self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - - def _register_plugin(self, plugin, name): - call_plugin = self.pluginmanager.call_plugin - call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self.pluginmanager}) - self.hook.pytest_plugin_registered(plugin=plugin, - manager=self.pluginmanager) - dic = call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: + def do_setns(dic): import pytest setns(pytest, dic) - call_plugin(plugin, "pytest_addoption", {'parser': self._parser}) - if self._configured: - call_plugin(plugin, "pytest_configure", {'config': self}) + self.hook.pytest_namespace.call_historic({}, proc=do_setns) + self.hook.pytest_addoption.call_historic(dict(parser=self._parser)) def add_cleanup(self, func): """ Add a function to be called when the config object gets out of @@ -726,12 +718,13 @@ def _do_configure(self): assert not self._configured self._configured = True - self.hook.pytest_configure(config=self) + self.hook.pytest_configure.call_historic(dict(config=self)) def _ensure_unconfigure(self): if self._configured: self._configured = False self.hook.pytest_unconfigure(config=self) + self.hook.pytest_configure._call_history = [] while self._cleanup: fin = self._cleanup.pop() fin() @@ -847,6 +840,7 @@ 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(dict(pluginmanager=self.pluginmanager)) self._preparse(args) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) diff -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a -r 2df6e23821fb7973900905b8935e35e018df0f8f _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -7,16 +7,23 @@ py3 = sys.version_info > (3,0) -def hookspec_opts(firstresult=False): +def hookspec_opts(firstresult=False, historic=False): """ returns a decorator which will define a function as a hook specfication. If firstresult is True the 1:N hook call (N being the number of registered hook implementation functions) will stop at I<=N when the I'th function returns a non-None result. + + If historic is True calls to a hook will be memorized and replayed + on later registered plugins. """ def setattr_hookspec_opts(func): + if historic and firstresult: + raise ValueError("cannot have a historic firstresult hook") if firstresult: func.firstresult = firstresult + if historic: + func.historic = historic return func return setattr_hookspec_opts @@ -226,6 +233,7 @@ def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) + assert not caller.historic hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult, argnames=caller.argnames) for plugin in hc.plugins: @@ -272,15 +280,18 @@ if name.startswith(self._prefix): specfunc = module_or_class.__dict__[name] firstresult = getattr(specfunc, 'firstresult', False) + historic = getattr(specfunc, 'historic', False) hc = getattr(self.hook, name, None) argnames = varnames(specfunc, startindex=isclass) if hc is None: hc = HookCaller(name, [], firstresult=firstresult, + historic=historic, argnames=argnames) setattr(self.hook, name, hc) else: # plugins registered this hook without knowing the spec - hc.setspec(firstresult=firstresult, argnames=argnames) + hc.setspec(firstresult=firstresult, argnames=argnames, + historic=historic) for plugin in hc.plugins: self._verify_hook(hc, specfunc, plugin) hc.add_method(getattr(plugin, name)) @@ -309,11 +320,6 @@ """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) - def call_plugin(self, plugin, methname, kwargs): - meth = getattr(plugin, methname, None) - if meth is not None: - return MultiCall(methods=[meth], kwargs=kwargs, firstresult=True).execute() - def _scan_plugin(self, plugin): hookcallers = [] for name in dir(plugin): @@ -334,6 +340,7 @@ self._verify_hook(hook, method, plugin) hook.plugins.append(plugin) hook.add_method(method) + hook._apply_history(method) hookcallers.append(hook) return hookcallers @@ -441,25 +448,30 @@ class HookCaller: - def __init__(self, name, plugins, argnames=None, firstresult=None): + def __init__(self, name, plugins, argnames=None, firstresult=None, + historic=False): self.name = name self.plugins = plugins if argnames is not None: argnames = ["__multicall__"] + list(argnames) + self.historic = historic self.argnames = argnames self.firstresult = firstresult self.wrappers = [] self.nonwrappers = [] + if self.historic: + self._call_history = [] @property def pre(self): return self.argnames is None - def setspec(self, argnames, firstresult): + def setspec(self, argnames, firstresult, historic): assert self.pre assert "self" not in argnames # sanity check self.argnames = ["__multicall__"] + list(argnames) self.firstresult = firstresult + self.historic = historic def remove_plugin(self, plugin): self.plugins.remove(plugin) @@ -472,6 +484,7 @@ def add_method(self, meth): assert not self.pre if hasattr(meth, 'hookwrapper'): + assert not self.historic self.wrappers.append(meth) elif hasattr(meth, 'trylast'): self.nonwrappers.insert(0, meth) @@ -493,16 +506,27 @@ return "" %(self.name,) def __call__(self, **kwargs): + assert not self.historic return self._docall(self.nonwrappers + self.wrappers, kwargs) def callextra(self, methods, **kwargs): + assert not self.historic return self._docall(self.nonwrappers + methods + self.wrappers, kwargs) def _docall(self, methods, kwargs): - assert not self.pre, self.name - return MultiCall(methods, kwargs, - firstresult=self.firstresult).execute() + return MultiCall(methods, kwargs, firstresult=self.firstresult).execute() + + def call_historic(self, kwargs, proc=None): + self._call_history.append((kwargs, proc)) + self._docall(self.nonwrappers + self.wrappers, kwargs) + + def _apply_history(self, meth): + if hasattr(self, "_call_history"): + for kwargs, proc in self._call_history: + res = MultiCall([meth], kwargs, firstresult=True).execute() + if proc is not None: + proc(res) class PluginValidationError(Exception): diff -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a -r 2df6e23821fb7973900905b8935e35e018df0f8f _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -3,27 +3,23 @@ from _pytest.core import hookspec_opts # ------------------------------------------------------------------------- -# Initialization +# Initialization hooks called for every plugin # ------------------------------------------------------------------------- + at hookspec_opts(historic=True) def pytest_addhooks(pluginmanager): - """called at plugin load time to allow adding new hooks via a call to + """called at plugin registration time to allow adding new hooks via a call to pluginmanager.addhooks(module_or_class, prefix).""" + at hookspec_opts(historic=True) def pytest_namespace(): """return dict of name->object to be made globally available in - the pytest namespace. This hook is called before command line options - are parsed. + the pytest namespace. This hook is called at plugin registration + time. """ - at hookspec_opts(firstresult=True) -def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ - -def pytest_cmdline_preparse(config, args): - """(deprecated) modify command line arguments before option parsing. """ - + at hookspec_opts(historic=True) def pytest_addoption(parser): """register argparse-style options and ini-style config values. @@ -49,6 +45,26 @@ via (deprecated) ``pytest.config``. """ + at hookspec_opts(historic=True) +def pytest_configure(config): + """ called after command line options have been parsed + and all plugins and initial conftest files been loaded. + This hook is called for every plugin. + """ + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins as well as directly +# discoverable conftest.py local plugins. +# ------------------------------------------------------------------------- + + at hookspec_opts(firstresult=True) +def pytest_cmdline_parse(pluginmanager, args): + """return initialized config object, parsing the specified args. """ + +def pytest_cmdline_preparse(config, args): + """(deprecated) modify command line arguments before option parsing. """ + @hookspec_opts(firstresult=True) def pytest_cmdline_main(config): """ called for performing the main command line action. The default @@ -58,18 +74,6 @@ """ implements the loading of initial conftest files ahead of command line option parsing. """ -def pytest_configure(config): - """ called after command line options have been parsed - and all plugins and initial conftest files been loaded. - """ - -def pytest_unconfigure(config): - """ called before test process is exited. """ - - at hookspec_opts(firstresult=True) -def pytest_runtestloop(session): - """ called for performing the main runtest loop - (after collection finished). """ # ------------------------------------------------------------------------- # collection hooks @@ -144,6 +148,12 @@ # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- + + at hookspec_opts(firstresult=True) +def pytest_runtestloop(session): + """ called for performing the main runtest loop + (after collection finished). """ + def pytest_itemstart(item, node): """ (deprecated, use pytest_runtest_logstart). """ @@ -201,6 +211,9 @@ def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ +def pytest_unconfigure(config): + """ called before test process is exited. """ + # ------------------------------------------------------------------------- # hooks for customising the assert methods diff -r 34b1a2d8b23278cb57876c934c5f2a706ee7430a -r 2df6e23821fb7973900905b8935e35e018df0f8f testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -77,6 +77,61 @@ #assert not pm._unverified_hooks assert pm.hook.he_method1(arg=1) == [2] + def test_register_unknown_hooks(self, pm): + class Plugin1: + def he_method1(self, arg): + return arg + 1 + + pm.register(Plugin1()) + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + #assert not pm._unverified_hooks + assert pm.hook.he_method1(arg=1) == [2] + + def test_register_historic(self, pm): + class Hooks: + @hookspec_opts(historic=True) + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + + pm.hook.he_method1.call_historic(kwargs=dict(arg=1)) + l = [] + class Plugin: + def he_method1(self, arg): + l.append(arg) + + pm.register(Plugin()) + assert l == [1] + + class Plugin2: + def he_method1(self, arg): + l.append(arg*10) + pm.register(Plugin2()) + assert l == [1, 10] + pm.hook.he_method1.call_historic(dict(arg=12)) + assert l == [1, 10, 120, 12] + + def test_with_result_memorized(self, pm): + class Hooks: + @hookspec_opts(historic=True) + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + + he_method1 = pm.hook.he_method1 + he_method1.call_historic(proc=lambda res: l.append(res), kwargs=dict(arg=1)) + l = [] + class Plugin: + def he_method1(self, arg): + return arg * 10 + + pm.register(Plugin()) + + assert l == [10] + class TestAddMethodOrdering: @pytest.fixture @@ -256,8 +311,10 @@ return xyz + 1 """) config = get_plugin_manager().config + pm = config.pluginmanager + pm.hook.pytest_addhooks.call_historic(dict(pluginmanager=config.pluginmanager)) config.pluginmanager._importconftest(conf) - print(config.pluginmanager.getplugins()) + #print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] https://bitbucket.org/pytest-dev/pytest/commits/162b549f8b07/ Changeset: 162b549f8b07 Branch: more_plugin User: hpk42 Date: 2015-04-25 09:29:11+00:00 Summary: properly perform hook calls with extra methods Affected #: 3 files diff -r 2df6e23821fb7973900905b8935e35e018df0f8f -r 162b549f8b07f0596fafe28c9ed1f7d7d7ac93dd _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -447,7 +447,7 @@ self.trace = pm.trace.root.get("hook") -class HookCaller: +class HookCaller(object): def __init__(self, name, plugins, argnames=None, firstresult=None, historic=False): self.name = name @@ -462,6 +462,17 @@ if self.historic: self._call_history = [] + def clone(self): + hc = object.__new__(HookCaller) + hc.name = self.name + hc.plugins = self.plugins + hc.historic = self.historic + hc.argnames = self.argnames + hc.firstresult = self.firstresult + hc.wrappers = list(self.wrappers) + hc.nonwrappers = list(self.nonwrappers) + return hc + @property def pre(self): return self.argnames is None @@ -511,8 +522,10 @@ def callextra(self, methods, **kwargs): assert not self.historic - return self._docall(self.nonwrappers + methods + self.wrappers, - kwargs) + hc = self.clone() + for method in methods: + hc.add_method(method) + return hc(**kwargs) def _docall(self, methods, kwargs): return MultiCall(methods, kwargs, firstresult=self.firstresult).execute() @@ -521,10 +534,11 @@ self._call_history.append((kwargs, proc)) self._docall(self.nonwrappers + self.wrappers, kwargs) - def _apply_history(self, meth): + def _apply_history(self, method): if hasattr(self, "_call_history"): for kwargs, proc in self._call_history: - res = MultiCall([meth], kwargs, firstresult=True).execute() + args = [kwargs[argname] for argname in varnames(method)] + res = method(*args) if proc is not None: proc(res) diff -r 2df6e23821fb7973900905b8935e35e018df0f8f -r 162b549f8b07f0596fafe28c9ed1f7d7d7ac93dd _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -375,13 +375,15 @@ fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) metafunc = Metafunc(funcobj, fixtureinfo, self.config, cls=cls, module=module) - try: - methods = [module.pytest_generate_tests] - except AttributeError: - methods = [] + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) - self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) + if methods: + self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) + else: + self.ihook.pytest_generate_tests(metafunc=metafunc) Function = self._getcustomclass("Function") if not metafunc._calls: diff -r 2df6e23821fb7973900905b8935e35e018df0f8f -r 162b549f8b07f0596fafe28c9ed1f7d7d7ac93dd testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -129,7 +129,18 @@ return arg * 10 pm.register(Plugin()) + assert l == [10] + def test_call_extra(self, pm): + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + + def he_method1(arg): + return arg * 10 + + l = pm.hook.he_method1.callextra([he_method1], arg=1) assert l == [10] https://bitbucket.org/pytest-dev/pytest/commits/b089cfcb110c/ Changeset: b089cfcb110c Branch: more_plugin User: hpk42 Date: 2015-04-25 11:38:29+00:00 Summary: Streamline data structures Affected #: 3 files diff -r 162b549f8b07f0596fafe28c9ed1f7d7d7ac93dd -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -233,14 +233,7 @@ def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) - assert not caller.historic - hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult, - argnames=caller.argnames) - for plugin in hc.plugins: - meth = getattr(plugin, name, None) - if meth is not None: - hc.add_method(meth) - return hc + return caller.clone(plugins=plugins) def register(self, plugin, name=None): """ Register a plugin with the given name and ensure that all its @@ -278,23 +271,15 @@ names = [] for name in dir(module_or_class): if name.startswith(self._prefix): - specfunc = module_or_class.__dict__[name] - firstresult = getattr(specfunc, 'firstresult', False) - historic = getattr(specfunc, 'historic', False) hc = getattr(self.hook, name, None) - argnames = varnames(specfunc, startindex=isclass) if hc is None: - hc = HookCaller(name, [], firstresult=firstresult, - historic=historic, - argnames=argnames) + hc = HookCaller(name, module_or_class) setattr(self.hook, name, hc) else: # plugins registered this hook without knowing the spec - hc.setspec(firstresult=firstresult, argnames=argnames, - historic=historic) - for plugin in hc.plugins: - self._verify_hook(hc, specfunc, plugin) - hc.add_method(getattr(plugin, name)) + hc.setspec(module_or_class) + for plugin in hc._plugins: + self._verify_hook(hc, plugin) names.append(name) if not names: raise ValueError("did not find new %r hooks in %r" @@ -330,21 +315,17 @@ if hook is None: if self._excludefunc is not None and self._excludefunc(name): continue - hook = HookCaller(name, [plugin]) + hook = HookCaller(name) setattr(self.hook, name, hook) - elif hook.pre: - # there is only a pre non-specced stub - hook.plugins.append(plugin) - else: - # we have a hook spec, can verify early - self._verify_hook(hook, method, plugin) - hook.plugins.append(plugin) - hook.add_method(method) + elif hook.has_spec(): + self._verify_hook(hook, plugin) hook._apply_history(method) + hook.add_plugin(plugin) hookcallers.append(hook) return hookcallers - def _verify_hook(self, hook, method, plugin): + def _verify_hook(self, hook, plugin): + method = getattr(plugin, hook.name) for arg in varnames(method): if arg not in hook.argnames: pluginname = self._get_canonical_name(plugin) @@ -359,8 +340,8 @@ for name in self.hook.__dict__: if name.startswith(self._prefix): hook = getattr(self.hook, name) - if hook.pre: - for plugin in hook.plugins: + if not hook.has_spec(): + for plugin in hook._plugins: method = getattr(plugin, hook.name) if not getattr(method, "optionalhook", False): raise PluginValidationError( @@ -448,80 +429,91 @@ class HookCaller(object): - def __init__(self, name, plugins, argnames=None, firstresult=None, - historic=False): + def __init__(self, name, specmodule_or_class=None): self.name = name - self.plugins = plugins - if argnames is not None: - argnames = ["__multicall__"] + list(argnames) - self.historic = historic - self.argnames = argnames - self.firstresult = firstresult - self.wrappers = [] - self.nonwrappers = [] - if self.historic: + self._plugins = [] + self._wrappers = [] + self._nonwrappers = [] + if specmodule_or_class is not None: + self.setspec(specmodule_or_class) + + def has_spec(self): + return hasattr(self, "_specmodule_or_class") + + def clone(self, plugins=None): + assert not self.is_historic() + hc = object.__new__(HookCaller) + hc.name = self.name + hc.argnames = self.argnames + hc.firstresult = self.firstresult + if plugins is None: + hc._plugins = self._plugins + hc._wrappers = list(self._wrappers) + hc._nonwrappers = list(self._nonwrappers) + else: + hc._plugins, hc._wrappers, hc._nonwrappers = [], [], [] + for plugin in plugins: + if hasattr(plugin, hc.name): + hc.add_plugin(plugin) + return hc + + def setspec(self, specmodule_or_class): + assert not self.has_spec() + self._specmodule_or_class = specmodule_or_class + specfunc = getattr(specmodule_or_class, self.name) + self.argnames = ["__multicall__"] + list(varnames( + specfunc, startindex=inspect.isclass(specmodule_or_class) + )) + assert "self" not in self.argnames # sanity check + self.firstresult = getattr(specfunc, 'firstresult', False) + if hasattr(specfunc, "historic"): self._call_history = [] - def clone(self): - hc = object.__new__(HookCaller) - hc.name = self.name - hc.plugins = self.plugins - hc.historic = self.historic - hc.argnames = self.argnames - hc.firstresult = self.firstresult - hc.wrappers = list(self.wrappers) - hc.nonwrappers = list(self.nonwrappers) - return hc - - @property - def pre(self): - return self.argnames is None - - def setspec(self, argnames, firstresult, historic): - assert self.pre - assert "self" not in argnames # sanity check - self.argnames = ["__multicall__"] + list(argnames) - self.firstresult = firstresult - self.historic = historic + def is_historic(self): + return hasattr(self, "_call_history") def remove_plugin(self, plugin): - self.plugins.remove(plugin) + self._plugins.remove(plugin) meth = getattr(plugin, self.name) try: - self.nonwrappers.remove(meth) + self._nonwrappers.remove(meth) except ValueError: - self.wrappers.remove(meth) + self._wrappers.remove(meth) + + def add_plugin(self, plugin): + self._plugins.append(plugin) + self.add_method(getattr(plugin, self.name)) def add_method(self, meth): - assert not self.pre if hasattr(meth, 'hookwrapper'): - assert not self.historic - self.wrappers.append(meth) + assert not self.is_historic() + self._wrappers.append(meth) elif hasattr(meth, 'trylast'): - self.nonwrappers.insert(0, meth) + self._nonwrappers.insert(0, meth) elif hasattr(meth, 'tryfirst'): - self.nonwrappers.append(meth) + self._nonwrappers.append(meth) else: - if not self.nonwrappers or not hasattr(self.nonwrappers[-1], "tryfirst"): - self.nonwrappers.append(meth) + nonwrappers = self._nonwrappers + if not nonwrappers or not hasattr(nonwrappers[-1], "tryfirst"): + nonwrappers.append(meth) else: - for i in reversed(range(len(self.nonwrappers)-1)): - if hasattr(self.nonwrappers[i], "tryfirst"): + for i in reversed(range(len(nonwrappers)-1)): + if hasattr(nonwrappers[i], "tryfirst"): continue - self.nonwrappers.insert(i+1, meth) + nonwrappers.insert(i+1, meth) break else: - self.nonwrappers.insert(0, meth) + nonwrappers.insert(0, meth) def __repr__(self): return "" %(self.name,) def __call__(self, **kwargs): - assert not self.historic - return self._docall(self.nonwrappers + self.wrappers, kwargs) + assert not self.is_historic() + return self._docall(self._nonwrappers + self._wrappers, kwargs) def callextra(self, methods, **kwargs): - assert not self.historic + assert not self.is_historic() hc = self.clone() for method in methods: hc.add_method(method) @@ -532,10 +524,10 @@ def call_historic(self, kwargs, proc=None): self._call_history.append((kwargs, proc)) - self._docall(self.nonwrappers + self.wrappers, kwargs) + self._docall(self._nonwrappers + self._wrappers, kwargs) def _apply_history(self, method): - if hasattr(self, "_call_history"): + if self.is_historic(): for kwargs, proc in self._call_history: args = [kwargs[argname] for argname in varnames(method)] res = method(*args) diff -r 162b549f8b07f0596fafe28c9ed1f7d7d7ac93dd -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -356,7 +356,7 @@ m = My() pm.register(m) hc = pm.hook.pytest_load_initial_conftests - l = hc.nonwrappers + hc.wrappers + l = hc._nonwrappers + hc._wrappers assert l[-1].__module__ == "_pytest.capture" assert l[-2] == m.pytest_load_initial_conftests assert l[-3].__module__ == "_pytest.config" diff -r 162b549f8b07f0596fafe28c9ed1f7d7d7ac93dd -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -180,7 +180,7 @@ @addmeth() def he_method3(): pass - assert hc.nonwrappers == [he_method1, he_method2, he_method3] + assert hc._nonwrappers == [he_method1, he_method2, he_method3] def test_adding_nonwrappers_trylast(self, hc, addmeth): @addmeth() @@ -194,7 +194,7 @@ @addmeth() def he_method1_b(): pass - assert hc.nonwrappers == [he_method1, he_method1_middle, he_method1_b] + assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b] def test_adding_nonwrappers_trylast2(self, hc, addmeth): @addmeth() @@ -208,7 +208,7 @@ @addmeth(trylast=True) def he_method1(): pass - assert hc.nonwrappers == [he_method1, he_method1_middle, he_method1_b] + assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b] def test_adding_nonwrappers_tryfirst(self, hc, addmeth): @addmeth(tryfirst=True) @@ -222,7 +222,7 @@ @addmeth() def he_method1_b(): pass - assert hc.nonwrappers == [he_method1_middle, he_method1_b, he_method1] + assert hc._nonwrappers == [he_method1_middle, he_method1_b, he_method1] def test_adding_nonwrappers_trylast(self, hc, addmeth): @addmeth() @@ -240,7 +240,7 @@ @addmeth(trylast=True) def he_method1_d(): pass - assert hc.nonwrappers == [he_method1_d, he_method1_b, he_method1_a, he_method1_c] + assert hc._nonwrappers == [he_method1_d, he_method1_b, he_method1_a, he_method1_c] def test_adding_wrappers_ordering(self, hc, addmeth): @addmeth(hookwrapper=True) @@ -255,8 +255,8 @@ def he_method3(): pass - assert hc.nonwrappers == [he_method1_middle] - assert hc.wrappers == [he_method1, he_method3] + assert hc._nonwrappers == [he_method1_middle] + assert hc._wrappers == [he_method1, he_method3] def test_hookspec_opts(self, pm): class HookSpec: https://bitbucket.org/pytest-dev/pytest/commits/0e1a53f7948c/ Changeset: 0e1a53f7948c Branch: more_plugin User: hpk42 Date: 2015-04-25 11:38:30+00:00 Summary: make pytest_plugin_registered a historic hook Affected #: 5 files diff -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -122,8 +122,8 @@ if ret: if not conftest: self._globalplugins.append(plugin) - self.hook.pytest_plugin_registered(plugin=plugin, - manager=self) + self.hook.pytest_plugin_registered.call_historic( + kwargs=dict(plugin=plugin, manager=self)) return ret def unregister(self, plugin): @@ -707,8 +707,8 @@ def do_setns(dic): import pytest setns(pytest, dic) - self.hook.pytest_namespace.call_historic({}, proc=do_setns) - self.hook.pytest_addoption.call_historic(dict(parser=self._parser)) + self.hook.pytest_namespace.call_historic(do_setns, {}) + self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) def add_cleanup(self, func): """ Add a function to be called when the config object gets out of @@ -718,7 +718,7 @@ def _do_configure(self): assert not self._configured self._configured = True - self.hook.pytest_configure.call_historic(dict(config=self)) + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) def _ensure_unconfigure(self): if self._configured: @@ -840,7 +840,8 @@ 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(dict(pluginmanager=self.pluginmanager)) + self.hook.pytest_addhooks.call_historic( + kwargs=dict(pluginmanager=self.pluginmanager)) self._preparse(args) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) diff -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -2,7 +2,7 @@ PluginManager, basic initialization and tracing. """ import sys -import inspect +from inspect import isfunction, ismethod, isclass, formatargspec, getargspec import py py3 = sys.version_info > (3,0) @@ -267,7 +267,6 @@ def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using the prefix/excludefunc with which the PluginManager was initialized. """ - isclass = int(inspect.isclass(module_or_class)) names = [] for name in dir(module_or_class): if name.startswith(self._prefix): @@ -394,17 +393,17 @@ return cache["_varnames"] except KeyError: pass - if inspect.isclass(func): + if isclass(func): try: func = func.__init__ except AttributeError: return () startindex = 1 else: - if not inspect.isfunction(func) and not inspect.ismethod(func): + if not isfunction(func) and not ismethod(func): func = getattr(func, '__call__', func) if startindex is None: - startindex = int(inspect.ismethod(func)) + startindex = int(ismethod(func)) rawcode = py.code.getrawcode(func) try: @@ -461,10 +460,9 @@ assert not self.has_spec() self._specmodule_or_class = specmodule_or_class specfunc = getattr(specmodule_or_class, self.name) - self.argnames = ["__multicall__"] + list(varnames( - specfunc, startindex=inspect.isclass(specmodule_or_class) - )) - assert "self" not in self.argnames # sanity check + argnames = varnames(specfunc, startindex=isclass(specmodule_or_class)) + assert "self" not in argnames # sanity check + self.argnames = ["__multicall__"] + list(argnames) self.firstresult = getattr(specfunc, 'firstresult', False) if hasattr(specfunc, "historic"): self._call_history = [] @@ -512,27 +510,26 @@ assert not self.is_historic() return self._docall(self._nonwrappers + self._wrappers, kwargs) - def callextra(self, methods, **kwargs): + def call_extra(self, methods, kwargs): assert not self.is_historic() hc = self.clone() for method in methods: hc.add_method(method) return hc(**kwargs) - def _docall(self, methods, kwargs): - return MultiCall(methods, kwargs, firstresult=self.firstresult).execute() - - def call_historic(self, kwargs, proc=None): - self._call_history.append((kwargs, proc)) + def call_historic(self, proc=None, kwargs=None): + self._call_history.append((kwargs or {}, proc)) self._docall(self._nonwrappers + self._wrappers, kwargs) def _apply_history(self, method): if self.is_historic(): for kwargs, proc in self._call_history: - args = [kwargs[argname] for argname in varnames(method)] - res = method(*args) - if proc is not None: - proc(res) + res = self._docall([method], kwargs) + if res and proc is not None: + proc(res[0]) + + def _docall(self, methods, kwargs): + return MultiCall(methods, kwargs, firstresult=self.firstresult).execute() class PluginValidationError(Exception): @@ -542,5 +539,5 @@ def formatdef(func): return "%s%s" % ( func.__name__, - inspect.formatargspec(*inspect.getargspec(func)) + formatargspec(*getargspec(func)) ) diff -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -20,6 +20,11 @@ """ @hookspec_opts(historic=True) +def pytest_plugin_registered(plugin, manager): + """ a new pytest plugin got registered. """ + + + at hookspec_opts(historic=True) def pytest_addoption(parser): """register argparse-style options and ini-style config values. @@ -259,9 +264,6 @@ # error handling and internal debugging hooks # ------------------------------------------------------------------------- -def pytest_plugin_registered(plugin, manager): - """ a new pytest plugin got registered. """ - def pytest_internalerror(excrepr, excinfo): """ called for internal errors. """ diff -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -381,7 +381,8 @@ if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) if methods: - self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) + self.ihook.pytest_generate_tests.call_extra(methods, + dict(metafunc=metafunc)) else: self.ihook.pytest_generate_tests(metafunc=metafunc) @@ -1623,7 +1624,6 @@ self.session = session self.config = session.config self._arg2fixturedefs = {} - self._seenplugins = set() self._holderobjseen = set() self._arg2finish = {} self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] @@ -1648,11 +1648,7 @@ node) return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs) - ### XXX this hook should be called for historic events like pytest_configure - ### so that we don't have to do the below pytest_configure hook def pytest_plugin_registered(self, plugin): - if plugin in self._seenplugins: - return nodeid = None try: p = py.path.local(plugin.__file__) @@ -1667,13 +1663,6 @@ if p.sep != "/": nodeid = nodeid.replace(p.sep, "/") self.parsefactories(plugin, nodeid) - self._seenplugins.add(plugin) - - @pytest.hookimpl_opts(tryfirst=True) - def pytest_configure(self, config): - plugins = config.pluginmanager.getplugins() - for plugin in plugins: - self.pytest_plugin_registered(plugin) def _getautousenames(self, nodeid): """ return a tuple of fixture names to be used. """ diff -r b089cfcb110c6905e0cf6468cbe1ebca5e90e5a7 -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -111,7 +111,7 @@ l.append(arg*10) pm.register(Plugin2()) assert l == [1, 10] - pm.hook.he_method1.call_historic(dict(arg=12)) + pm.hook.he_method1.call_historic(kwargs=dict(arg=12)) assert l == [1, 10, 120, 12] def test_with_result_memorized(self, pm): @@ -122,7 +122,7 @@ pm.addhooks(Hooks) he_method1 = pm.hook.he_method1 - he_method1.call_historic(proc=lambda res: l.append(res), kwargs=dict(arg=1)) + he_method1.call_historic(lambda res: l.append(res), dict(arg=1)) l = [] class Plugin: def he_method1(self, arg): @@ -140,7 +140,7 @@ def he_method1(arg): return arg * 10 - l = pm.hook.he_method1.callextra([he_method1], arg=1) + l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) assert l == [10] @@ -323,7 +323,8 @@ """) config = get_plugin_manager().config pm = config.pluginmanager - pm.hook.pytest_addhooks.call_historic(dict(pluginmanager=config.pluginmanager)) + pm.hook.pytest_addhooks.call_historic( + kwargs=dict(pluginmanager=config.pluginmanager)) config.pluginmanager._importconftest(conf) #print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) @@ -399,10 +400,10 @@ pytestpm = get_plugin_manager() # fully initialized with plugins saveindent = [] class api1: - def pytest_plugin_registered(self, plugin): + def pytest_plugin_registered(self): saveindent.append(pytestpm.trace.root.indent) class api2: - def pytest_plugin_registered(self, plugin): + def pytest_plugin_registered(self): saveindent.append(pytestpm.trace.root.indent) raise ValueError() l = [] @@ -412,7 +413,7 @@ p = api1() pytestpm.register(p) assert pytestpm.trace.root.indent == indent - assert len(l) == 2 + assert len(l) >= 2 assert 'pytest_plugin_registered' in l[0] assert 'finish' in l[1] https://bitbucket.org/pytest-dev/pytest/commits/99546784690a/ Changeset: 99546784690a Branch: more_plugin User: hpk42 Date: 2015-04-25 16:14:39+00:00 Summary: fix issue732: make sure removed plugins remove all hook callers. Affected #: 3 files diff -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d -r 99546784690ad92d7850f9a8fe8664425b224c5b CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,9 @@ - write/refine docs for "writing plugins" which now have their own page and are separate from the "using/installing plugins`` page. +- fix issue732: properly unregister plugins from any hook calling + sites allowing to have temporary plugins during test execution. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d -r 99546784690ad92d7850f9a8fe8664425b224c5b _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -233,7 +233,14 @@ def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) - return caller.clone(plugins=plugins) + hc = HookCaller(caller.name, caller._specmodule_or_class) + for plugin in plugins: + if hasattr(plugin, name): + hc._add_plugin(plugin) + # we also keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._plugin2hookcallers.setdefault(plugin, []).append(hc) + return hc def register(self, plugin, name=None): """ Register a plugin with the given name and ensure that all its @@ -247,9 +254,8 @@ if self.hasplugin(name): raise ValueError("Plugin already registered: %s=%s\n%s" %( name, plugin, self._name2plugin)) - #self.trace("registering", name, plugin) - self._plugin2hookcallers[plugin] = self._scan_plugin(plugin) self._name2plugin[name] = plugin + self._scan_plugin(plugin) self._plugins.append(plugin) return True @@ -260,9 +266,8 @@ for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] - hookcallers = self._plugin2hookcallers.pop(plugin) - for hookcaller in hookcallers: - hookcaller.remove_plugin(plugin) + for hookcaller in self._plugin2hookcallers.pop(plugin): + hookcaller._remove_plugin(plugin) def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using @@ -276,7 +281,7 @@ setattr(self.hook, name, hc) else: # plugins registered this hook without knowing the spec - hc.setspec(module_or_class) + hc.set_specification(module_or_class) for plugin in hc._plugins: self._verify_hook(hc, plugin) names.append(name) @@ -305,7 +310,7 @@ return self._name2plugin.get(name) def _scan_plugin(self, plugin): - hookcallers = [] + self._plugin2hookcallers[plugin] = hookcallers = [] for name in dir(plugin): if name[0] == "_" or not name.startswith(self._prefix): continue @@ -319,9 +324,8 @@ elif hook.has_spec(): self._verify_hook(hook, plugin) hook._apply_history(method) - hook.add_plugin(plugin) hookcallers.append(hook) - return hookcallers + hook._add_plugin(plugin) def _verify_hook(self, hook, plugin): method = getattr(plugin, hook.name) @@ -434,29 +438,12 @@ self._wrappers = [] self._nonwrappers = [] if specmodule_or_class is not None: - self.setspec(specmodule_or_class) + self.set_specification(specmodule_or_class) def has_spec(self): return hasattr(self, "_specmodule_or_class") - def clone(self, plugins=None): - assert not self.is_historic() - hc = object.__new__(HookCaller) - hc.name = self.name - hc.argnames = self.argnames - hc.firstresult = self.firstresult - if plugins is None: - hc._plugins = self._plugins - hc._wrappers = list(self._wrappers) - hc._nonwrappers = list(self._nonwrappers) - else: - hc._plugins, hc._wrappers, hc._nonwrappers = [], [], [] - for plugin in plugins: - if hasattr(plugin, hc.name): - hc.add_plugin(plugin) - return hc - - def setspec(self, specmodule_or_class): + def set_specification(self, specmodule_or_class): assert not self.has_spec() self._specmodule_or_class = specmodule_or_class specfunc = getattr(specmodule_or_class, self.name) @@ -470,7 +457,7 @@ def is_historic(self): return hasattr(self, "_call_history") - def remove_plugin(self, plugin): + def _remove_plugin(self, plugin): self._plugins.remove(plugin) meth = getattr(plugin, self.name) try: @@ -478,11 +465,11 @@ except ValueError: self._wrappers.remove(meth) - def add_plugin(self, plugin): + def _add_plugin(self, plugin): self._plugins.append(plugin) - self.add_method(getattr(plugin, self.name)) + self._add_method(getattr(plugin, self.name)) - def add_method(self, meth): + def _add_method(self, meth): if hasattr(meth, 'hookwrapper'): assert not self.is_historic() self._wrappers.append(meth) @@ -511,11 +498,15 @@ return self._docall(self._nonwrappers + self._wrappers, kwargs) def call_extra(self, methods, kwargs): - assert not self.is_historic() - hc = self.clone() + """ Call the hook with some additional temporarily participating + methods using the specified kwargs as call parameters. """ + old = list(self._nonwrappers), list(self._wrappers) for method in methods: - hc.add_method(method) - return hc(**kwargs) + self._add_method(method) + try: + return self(**kwargs) + finally: + self._nonwrappers, self._wrappers = old def call_historic(self, proc=None, kwargs=None): self._call_history.append((kwargs or {}, proc)) diff -r 0e1a53f7948c7a6552cb817d0e8976d92da3447d -r 99546784690ad92d7850f9a8fe8664425b224c5b testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -143,6 +143,25 @@ l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) assert l == [10] + def test_make_hook_caller_unregistered(self, pm): + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + + l = [] + class Plugin: + def he_method1(self, arg): + l.append(arg * 10) + plugin = Plugin() + pm.register(plugin) + hc = pm.make_hook_caller("he_method1", [plugin]) + hc(arg=1) + assert l == [10] + pm.unregister(plugin) + hc(arg=2) + assert l == [10] + class TestAddMethodOrdering: @pytest.fixture @@ -163,7 +182,7 @@ func.trylast = True if hookwrapper: func.hookwrapper = True - hc.add_method(func) + hc._add_method(func) return func return wrap return addmeth https://bitbucket.org/pytest-dev/pytest/commits/d5c102154a5b/ Changeset: d5c102154a5b Branch: more_plugin User: hpk42 Date: 2015-04-25 16:14:41+00:00 Summary: avoid direct circular reference between config and pluginmanager Affected #: 3 files diff -r 99546784690ad92d7850f9a8fe8664425b224c5b -r d5c102154a5b8eb171f87019de4b2c4b61565498 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -60,17 +60,17 @@ def _preloadplugins(): assert not _preinit - _preinit.append(get_plugin_manager()) + _preinit.append(get_config()) -def get_plugin_manager(): +def get_config(): if _preinit: return _preinit.pop(0) # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - pluginmanager.config = Config(pluginmanager) # XXX attr needed? + config = Config(pluginmanager) for spec in default_plugins: pluginmanager.import_plugin(spec) - return pluginmanager + return config def _prepareconfig(args=None, plugins=None): if args is None: @@ -81,7 +81,7 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_plugin_manager() + pluginmanager = get_config().pluginmanager if plugins: for plugin in plugins: pluginmanager.register(plugin) @@ -738,7 +738,7 @@ return self.pluginmanager.getplugin("terminalreporter")._tw def pytest_cmdline_parse(self, pluginmanager, args): - assert self == pluginmanager.config, (self, pluginmanager.config) + # REF1 assert self == pluginmanager.config, (self, pluginmanager.config) self.parse(args) return self @@ -768,8 +768,7 @@ @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ - pluginmanager = get_plugin_manager() - config = pluginmanager.config + config = get_config() config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) for x in config.option.plugins: diff -r 99546784690ad92d7850f9a8fe8664425b224c5b -r d5c102154a5b8eb171f87019de4b2c4b61565498 testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -348,8 +348,8 @@ def test_load_initial_conftest_last_ordering(testdir): - from _pytest.config import get_plugin_manager - pm = get_plugin_manager() + from _pytest.config import get_config + pm = get_config().pluginmanager class My: def pytest_load_initial_conftests(self): pass diff -r 99546784690ad92d7850f9a8fe8664425b224c5b -r d5c102154a5b8eb171f87019de4b2c4b61565498 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,6 +1,6 @@ import pytest, py, os from _pytest.core import * # noqa -from _pytest.config import get_plugin_manager +from _pytest.config import get_config @pytest.fixture @@ -41,14 +41,14 @@ pytestpm.check_pending() def test_register_mismatch_arg(self): - pm = get_plugin_manager() + pm = get_config().pluginmanager class hello: def pytest_configure(self, asd): pass pytest.raises(Exception, lambda: pm.register(hello())) def test_register(self): - pm = get_plugin_manager() + pm = get_config().pluginmanager class MyPlugin: pass my = MyPlugin() @@ -340,7 +340,7 @@ def pytest_myhook(xyz): return xyz + 1 """) - config = get_plugin_manager().config + config = get_config() pm = config.pluginmanager pm.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=config.pluginmanager)) @@ -416,7 +416,7 @@ assert len(l) == 2 def test_hook_tracing(self): - pytestpm = get_plugin_manager() # fully initialized with plugins + pytestpm = get_config().pluginmanager # fully initialized with plugins saveindent = [] class api1: def pytest_plugin_registered(self): @@ -927,7 +927,7 @@ assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" def test_consider_module_import_module(self, testdir): - pytestpm = get_plugin_manager() + pytestpm = get_config().pluginmanager mod = py.std.types.ModuleType("x") mod.pytest_plugins = "pytest_a" aplugin = testdir.makepyfile(pytest_a="#") https://bitbucket.org/pytest-dev/pytest/commits/bc58da3ce714/ Changeset: bc58da3ce714 Branch: more_plugin User: hpk42 Date: 2015-04-25 16:15:39+00:00 Summary: simplify tracing mechanics by simply going through an indirection Affected #: 5 files diff -r d5c102154a5b8eb171f87019de4b2c4b61565498 -r bc58da3ce7142f1885f6d4308a3534e984e196b0 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -115,7 +115,8 @@ err = py.io.dupfile(err, encoding=encoding) except Exception: pass - self.set_tracing(err.write) + self.trace.root.setwriter(err.write) + self.enable_tracing() def register(self, plugin, name=None, conftest=False): ret = super(PytestPluginManager, self).register(plugin, name) diff -r d5c102154a5b8eb171f87019de4b2c4b61565498 -r bc58da3ce7142f1885f6d4308a3534e984e196b0 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -60,6 +60,7 @@ return func return setattr_hookimpl_opts + class TagTracer: def __init__(self): self._tag2proc = {} @@ -106,6 +107,7 @@ assert isinstance(tags, tuple) self._tag2proc[tags] = processor + class TagTracerSub: def __init__(self, root, tags): self.root = root @@ -118,25 +120,6 @@ return self.__class__(self.root, self.tags + (name,)) -def add_method_wrapper(cls, wrapper_func): - """ Substitute the function named "wrapperfunc.__name__" at class - "cls" with a function that wraps the call to the original function. - Return an undo function which can be called to reset the class to use - the old method again. - - wrapper_func is called with the same arguments as the method - it wraps and its result is used as a wrap_controller for - calling the original function. - """ - name = wrapper_func.__name__ - oldcall = getattr(cls, name) - def wrap_exec(*args, **kwargs): - gen = wrapper_func(*args, **kwargs) - return wrapped_call(gen, lambda: oldcall(*args, **kwargs)) - - setattr(cls, name, wrap_exec) - return lambda: setattr(cls, name, oldcall) - def raise_wrapfail(wrap_controller, msg): co = wrap_controller.gi_code raise RuntimeError("wrap_controller at %r %s:%d %s" % @@ -186,6 +169,25 @@ py.builtin._reraise(*ex) +class TracedHookExecution: + def __init__(self, pluginmanager, before, after): + self.pluginmanager = pluginmanager + self.before = before + self.after = after + self.oldcall = pluginmanager._inner_hookexec + assert not isinstance(self.oldcall, TracedHookExecution) + self.pluginmanager._inner_hookexec = self + + def __call__(self, hook, methods, kwargs): + self.before(hook, methods, kwargs) + outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs)) + self.after(outcome, hook, methods, kwargs) + return outcome.get_result() + + def undo(self): + self.pluginmanager._inner_hookexec = self.oldcall + + class PluginManager(object): """ Core Pluginmanager class which manages registration of plugin objects and 1:N hook calling. @@ -209,31 +211,31 @@ self._plugins = [] self._plugin2hookcallers = {} self.trace = TagTracer().get("pluginmanage") - self.hook = HookRelay(pm=self) + self.hook = HookRelay(self.trace.root.get("hook")) + self._inner_hookexec = lambda hook, methods, kwargs: \ + MultiCall(methods, kwargs, hook.firstresult).execute() - def set_tracing(self, writer): - """ turn on tracing to the given writer method and - return an undo function. """ - self.trace.root.setwriter(writer) - # reconfigure HookCalling to perform tracing - assert not hasattr(self, "_wrapping") - self._wrapping = True + def _hookexec(self, hook, methods, kwargs): + return self._inner_hookexec(hook, methods, kwargs) - hooktrace = self.hook.trace + def enable_tracing(self): + """ enable tracing of hook calls and return an undo function. """ + hooktrace = self.hook._trace - def _docall(self, methods, kwargs): + def before(hook, methods, kwargs): hooktrace.root.indent += 1 - hooktrace(self.name, kwargs) - box = yield - if box.excinfo is None: - hooktrace("finish", self.name, "-->", box.result) + hooktrace(hook.name, kwargs) + + def after(outcome, hook, methods, kwargs): + if outcome.excinfo is None: + hooktrace("finish", hook.name, "-->", outcome.result) hooktrace.root.indent -= 1 - return add_method_wrapper(HookCaller, _docall) + return TracedHookExecution(self, before, after).undo def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) - hc = HookCaller(caller.name, caller._specmodule_or_class) + hc = HookCaller(caller.name, self._hookexec, caller._specmodule_or_class) for plugin in plugins: if hasattr(plugin, name): hc._add_plugin(plugin) @@ -277,7 +279,7 @@ if name.startswith(self._prefix): hc = getattr(self.hook, name, None) if hc is None: - hc = HookCaller(name, module_or_class) + hc = HookCaller(name, self._hookexec, module_or_class) setattr(self.hook, name, hc) else: # plugins registered this hook without knowing the spec @@ -319,7 +321,7 @@ if hook is None: if self._excludefunc is not None and self._excludefunc(name): continue - hook = HookCaller(name) + hook = HookCaller(name, self._hookexec) setattr(self.hook, name, hook) elif hook.has_spec(): self._verify_hook(hook, plugin) @@ -362,15 +364,11 @@ self.methods = methods self.kwargs = kwargs self.kwargs["__multicall__"] = self - self.results = [] self.firstresult = firstresult - def __repr__(self): - status = "%d results, %d meths" % (len(self.results), len(self.methods)) - return "" %(status, self.kwargs) - def execute(self): all_kwargs = self.kwargs + self.results = results = [] while self.methods: method = self.methods.pop() args = [all_kwargs[argname] for argname in varnames(method)] @@ -378,11 +376,18 @@ return wrapped_call(method(*args), self.execute) res = method(*args) if res is not None: - self.results.append(res) if self.firstresult: return res + results.append(res) if not self.firstresult: - return self.results + return results + + def __repr__(self): + status = "%d meths" % (len(self.methods),) + if hasattr(self, "results"): + status = ("%d results, " % len(self.results)) + status + return "" %(status, self.kwargs) + def varnames(func, startindex=None): @@ -426,17 +431,17 @@ class HookRelay: - def __init__(self, pm): - self._pm = pm - self.trace = pm.trace.root.get("hook") + def __init__(self, trace): + self._trace = trace class HookCaller(object): - def __init__(self, name, specmodule_or_class=None): + def __init__(self, name, hook_execute, specmodule_or_class=None): self.name = name self._plugins = [] self._wrappers = [] self._nonwrappers = [] + self._hookexec = hook_execute if specmodule_or_class is not None: self.set_specification(specmodule_or_class) @@ -495,7 +500,12 @@ def __call__(self, **kwargs): assert not self.is_historic() - return self._docall(self._nonwrappers + self._wrappers, kwargs) + return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) + + def call_historic(self, proc=None, kwargs=None): + self._call_history.append((kwargs or {}, proc)) + # historizing hooks don't return results + self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) def call_extra(self, methods, kwargs): """ Call the hook with some additional temporarily participating @@ -508,20 +518,13 @@ finally: self._nonwrappers, self._wrappers = old - def call_historic(self, proc=None, kwargs=None): - self._call_history.append((kwargs or {}, proc)) - self._docall(self._nonwrappers + self._wrappers, kwargs) - def _apply_history(self, method): if self.is_historic(): for kwargs, proc in self._call_history: - res = self._docall([method], kwargs) + res = self._hookexec(self, [method], kwargs) if res and proc is not None: proc(res[0]) - def _docall(self, methods, kwargs): - return MultiCall(methods, kwargs, firstresult=self.firstresult).execute() - class PluginValidationError(Exception): """ plugin failed validation. """ diff -r d5c102154a5b8eb171f87019de4b2c4b61565498 -r bc58da3ce7142f1885f6d4308a3534e984e196b0 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -34,13 +34,15 @@ pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(debugfile.write) + config.trace.root.setwriter(debugfile.write) + undo_tracing = config.pluginmanager.enable_tracing() sys.stderr.write("writing pytestdebug information to %s\n" % path) def unset_tracing(): debugfile.close() sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) config.trace.root.setwriter(None) + undo_tracing() config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): diff -r d5c102154a5b8eb171f87019de4b2c4b61565498 -r bc58da3ce7142f1885f6d4308a3534e984e196b0 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -11,7 +11,7 @@ import py import pytest from py.builtin import print_ -from _pytest.core import HookCaller, add_method_wrapper +from _pytest.core import HookCaller, TracedHookExecution from _pytest.main import Session, EXIT_OK @@ -79,12 +79,12 @@ self._pluginmanager = pluginmanager self.calls = [] - def _docall(hookcaller, methods, kwargs): - self.calls.append(ParsedCall(hookcaller.name, kwargs)) - yield - self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - #if hasattr(pluginmanager, "config"): - # pluginmanager.add_shutdown(self._undo_wrapping) + def before(hook, method, kwargs): + self.calls.append(ParsedCall(hook.name, kwargs)) + def after(outcome, hook, method, kwargs): + pass + executor = TracedHookExecution(pluginmanager, before, after) + self._undo_wrapping = executor.undo def finish_recording(self): self._undo_wrapping() diff -r d5c102154a5b8eb171f87019de4b2c4b61565498 -r bc58da3ce7142f1885f6d4308a3534e984e196b0 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -426,7 +426,8 @@ saveindent.append(pytestpm.trace.root.indent) raise ValueError() l = [] - undo = pytestpm.set_tracing(l.append) + pytestpm.trace.root.setwriter(l.append) + undo = pytestpm.enable_tracing() try: indent = pytestpm.trace.root.indent p = api1() @@ -788,109 +789,6 @@ assert "qwe" not in str(excinfo.value) assert "aaaa" in str(excinfo.value) -class TestWrapMethod: - def test_basic_hapmypath(self): - class A: - def f(self): - return "A.f" - - l = [] - def f(self): - l.append(1) - box = yield - assert box.result == "A.f" - l.append(2) - undo = add_method_wrapper(A, f) - - assert A().f() == "A.f" - assert l == [1,2] - undo() - l[:] = [] - assert A().f() == "A.f" - assert l == [] - - def test_no_yield(self): - class A: - def method(self): - return - - def method(self): - if 0: - yield - - add_method_wrapper(A, method) - with pytest.raises(RuntimeError) as excinfo: - A().method() - - assert "method" in str(excinfo.value) - assert "did not yield" in str(excinfo.value) - - def test_method_raises(self): - class A: - def error(self, val): - raise ValueError(val) - - l = [] - def error(self, val): - l.append(val) - yield - l.append(None) - - undo = add_method_wrapper(A, error) - - with pytest.raises(ValueError): - A().error(42) - assert l == [42, None] - undo() - l[:] = [] - with pytest.raises(ValueError): - A().error(42) - assert l == [] - - def test_controller_swallows_method_raises(self): - class A: - def error(self, val): - raise ValueError(val) - - def error(self, val): - box = yield - box.force_result(2) - - add_method_wrapper(A, error) - assert A().error(42) == 2 - - def test_reraise_on_controller_StopIteration(self): - class A: - def error(self, val): - raise ValueError(val) - - def error(self, val): - try: - yield - except ValueError: - pass - - add_method_wrapper(A, error) - with pytest.raises(ValueError): - A().error(42) - - @pytest.mark.xfail(reason="if needed later") - def test_modify_call_args(self): - class A: - def error(self, val1, val2): - raise ValueError(val1+val2) - - l = [] - def error(self): - box = yield (1,), {'val2': 2} - assert box.excinfo[1].args == (3,) - l.append(1) - - add_method_wrapper(A, error) - with pytest.raises(ValueError): - A().error() - assert l == [1] - ### to be shifted to own test file from _pytest.config import PytestPluginManager https://bitbucket.org/pytest-dev/pytest/commits/3e9beb7563b6/ Changeset: 3e9beb7563b6 Branch: more_plugin User: hpk42 Date: 2015-04-25 16:15:42+00:00 Summary: simplify addition of method and scanning of plugins Affected #: 2 files diff -r bc58da3ce7142f1885f6d4308a3534e984e196b0 -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -257,8 +257,23 @@ raise ValueError("Plugin already registered: %s=%s\n%s" %( name, plugin, self._name2plugin)) self._name2plugin[name] = plugin - self._scan_plugin(plugin) self._plugins.append(plugin) + + # register prefix-matching hooks of the plugin + self._plugin2hookcallers[plugin] = hookcallers = [] + for name in dir(plugin): + if name.startswith(self._prefix): + hook = getattr(self.hook, name, None) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + hook = HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) + elif hook.has_spec(): + self._verify_hook(hook, plugin) + hook._maybe_apply_history(getattr(plugin, name)) + hookcallers.append(hook) + hook._add_plugin(plugin) return True def unregister(self, plugin): @@ -311,29 +326,16 @@ """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) - def _scan_plugin(self, plugin): - self._plugin2hookcallers[plugin] = hookcallers = [] - for name in dir(plugin): - if name[0] == "_" or not name.startswith(self._prefix): - continue - hook = getattr(self.hook, name, None) - method = getattr(plugin, name) - if hook is None: - if self._excludefunc is not None and self._excludefunc(name): - continue - hook = HookCaller(name, self._hookexec) - setattr(self.hook, name, hook) - elif hook.has_spec(): - self._verify_hook(hook, plugin) - hook._apply_history(method) - hookcallers.append(hook) - hook._add_plugin(plugin) - def _verify_hook(self, hook, plugin): method = getattr(plugin, hook.name) + pluginname = self._get_canonical_name(plugin) + if hook.is_historic() and hasattr(method, "hookwrapper"): + raise PluginValidationError( + "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %( + pluginname, hook.name)) + for arg in varnames(method): if arg not in hook.argnames: - pluginname = self._get_canonical_name(plugin) raise PluginValidationError( "Plugin %r\nhook %r\nargument %r not available\n" "plugin definition: %s\n" @@ -369,6 +371,8 @@ def execute(self): all_kwargs = self.kwargs self.results = results = [] + firstresult = self.firstresult + while self.methods: method = self.methods.pop() args = [all_kwargs[argname] for argname in varnames(method)] @@ -376,10 +380,11 @@ return wrapped_call(method(*args), self.execute) res = method(*args) if res is not None: - if self.firstresult: + if firstresult: return res results.append(res) - if not self.firstresult: + + if not firstresult: return results def __repr__(self): @@ -476,24 +481,19 @@ def _add_method(self, meth): if hasattr(meth, 'hookwrapper'): - assert not self.is_historic() self._wrappers.append(meth) elif hasattr(meth, 'trylast'): self._nonwrappers.insert(0, meth) elif hasattr(meth, 'tryfirst'): self._nonwrappers.append(meth) else: + # find the last nonwrapper which is not tryfirst marked nonwrappers = self._nonwrappers - if not nonwrappers or not hasattr(nonwrappers[-1], "tryfirst"): - nonwrappers.append(meth) - else: - for i in reversed(range(len(nonwrappers)-1)): - if hasattr(nonwrappers[i], "tryfirst"): - continue - nonwrappers.insert(i+1, meth) - break - else: - nonwrappers.insert(0, meth) + i = len(nonwrappers) - 1 + while i >= 0 and hasattr(nonwrappers[i], "tryfirst"): + i -= 1 + # and insert right in front of the tryfirst ones + nonwrappers.insert(i+1, meth) def __repr__(self): return "" %(self.name,) @@ -518,7 +518,7 @@ finally: self._nonwrappers, self._wrappers = old - def _apply_history(self, method): + def _maybe_apply_history(self, method): if self.is_historic(): for kwargs, proc in self._call_history: res = self._hookexec(self, [method], kwargs) diff -r bc58da3ce7142f1885f6d4308a3534e984e196b0 -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -131,6 +131,22 @@ pm.register(Plugin()) assert l == [10] + def test_register_historic_incompat_hookwrapper(self, pm): + class Hooks: + @hookspec_opts(historic=True) + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + + l = [] + class Plugin: + @hookimpl_opts(hookwrapper=True) + def he_method1(self, arg): + l.append(arg) + + with pytest.raises(PluginValidationError): + pm.register(Plugin()) + def test_call_extra(self, pm): class Hooks: def he_method1(self, arg): https://bitbucket.org/pytest-dev/pytest/commits/1149ad4609d5/ Changeset: 1149ad4609d5 Branch: more_plugin User: hpk42 Date: 2015-04-25 18:17:32+00:00 Summary: simplify plugins bookkeeping further, refine API Affected #: 6 files diff -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 -r 1149ad4609d5737f363e8b3b73099e851096d847 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -127,13 +127,17 @@ kwargs=dict(plugin=plugin, manager=self)) return ret - def unregister(self, plugin): - super(PytestPluginManager, self).unregister(plugin) + def unregister(self, plugin=None, name=None): + plugin = super(PytestPluginManager, self).unregister(plugin, name) try: self._globalplugins.remove(plugin) except ValueError: pass + def getplugin(self, name): + # deprecated + return self.get_plugin(name) + def pytest_configure(self, config): config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " @@ -238,17 +242,14 @@ except ImportError: return # XXX issue a warning for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: + if self.get_plugin(ep.name) or ep.name in self._name2plugin: continue try: plugin = ep.load() except DistributionNotFound: continue + self.register(plugin, name=ep.name) self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) def consider_preparse(self, args): for opt1,opt2 in zip(args, args[1:]): @@ -257,14 +258,9 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 + self.set_blocked(arg[3:]) else: - if self.getplugin(arg) is None: - self.import_plugin(arg) + self.import_plugin(arg) def consider_conftest(self, conftestmodule): if self.register(conftestmodule, name=conftestmodule.__file__, @@ -290,7 +286,7 @@ # basename for historic purposes but must be imported with the # _pytest prefix. assert isinstance(modname, str) - if self.getplugin(modname) is not None: + if self.get_plugin(modname) is not None: return if modname in builtin_plugins: importspec = "_pytest." + modname @@ -736,7 +732,7 @@ fslocation=None, nodeid=None) def get_terminal_writer(self): - return self.pluginmanager.getplugin("terminalreporter")._tw + return self.pluginmanager.get_plugin("terminalreporter")._tw def pytest_cmdline_parse(self, pluginmanager, args): # REF1 assert self == pluginmanager.config, (self, pluginmanager.config) diff -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 -r 1149ad4609d5737f363e8b3b73099e851096d847 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -112,10 +112,13 @@ def __init__(self, root, tags): self.root = root self.tags = tags + def __call__(self, *args): self.root.processmessage(self.tags, args) + def setmyprocessor(self, processor): self.root.setprocessor(self.tags, processor) + def get(self, name): return self.__class__(self.root, self.tags + (name,)) @@ -125,6 +128,7 @@ raise RuntimeError("wrap_controller at %r %s:%d %s" % (co.co_name, co.co_filename, co.co_firstlineno, msg)) + def wrapped_call(wrap_controller, func): """ Wrap calling to a function with a generator which needs to yield exactly once. The yield point will trigger calling the wrapped function @@ -208,7 +212,6 @@ self._prefix = prefix self._excludefunc = excludefunc self._name2plugin = {} - self._plugins = [] self._plugin2hookcallers = {} self.trace = TagTracer().get("pluginmanage") self.hook = HookRelay(self.trace.root.get("hook")) @@ -244,22 +247,25 @@ self._plugin2hookcallers.setdefault(plugin, []).append(hc) return hc + def get_canonical_name(self, plugin): + """ Return canonical name for the plugin object. """ + return getattr(plugin, "__name__", None) or str(id(plugin)) + def register(self, plugin, name=None): - """ Register a plugin with the given name and ensure that all its - hook implementations are integrated. If the name is not specified - we use the ``__name__`` attribute of the plugin object or, if that - doesn't exist, the id of the plugin. This method will raise a - ValueError if the eventual name is already registered. """ - name = name or self._get_canonical_name(plugin) - if self._name2plugin.get(name, None) == -1: - return - if self.hasplugin(name): + """ Register a plugin and return its canonical name or None if it was + blocked from registering. Raise a ValueError if the plugin is already + registered. """ + plugin_name = name or self.get_canonical_name(plugin) + + if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: + if self._name2plugin.get(plugin_name, -1) is None: + return # blocked plugin, return None to indicate no registration raise ValueError("Plugin already registered: %s=%s\n%s" %( - name, plugin, self._name2plugin)) - self._name2plugin[name] = plugin - self._plugins.append(plugin) + plugin_name, plugin, self._name2plugin)) - # register prefix-matching hooks of the plugin + self._name2plugin[plugin_name] = plugin + + # register prefix-matching hook specs of the plugin self._plugin2hookcallers[plugin] = hookcallers = [] for name in dir(plugin): if name.startswith(self._prefix): @@ -274,18 +280,33 @@ hook._maybe_apply_history(getattr(plugin, name)) hookcallers.append(hook) hook._add_plugin(plugin) - return True + return plugin_name - def unregister(self, plugin): - """ unregister the plugin object and all its contained hook implementations - from internal data structures. """ - self._plugins.remove(plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - for hookcaller in self._plugin2hookcallers.pop(plugin): + def unregister(self, plugin=None, name=None): + """ unregister a plugin object and all its contained hook implementations + from internal data structures. One of ``plugin`` or ``name`` needs to + be specified. """ + if name is None: + assert plugin is not None + name = self.get_canonical_name(plugin) + + if plugin is None: + plugin = self.get_plugin(name) + + # None signals blocked registrations, don't delete it + if self._name2plugin.get(name): + del self._name2plugin[name] + + for hookcaller in self._plugin2hookcallers.pop(plugin, []): hookcaller._remove_plugin(plugin) + return plugin + + def set_blocked(self, name): + """ block registrations of the given name, unregister if already registered. """ + self.unregister(name=name) + self._name2plugin[name] = None + def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using the prefix/excludefunc with which the PluginManager was initialized. """ @@ -302,33 +323,27 @@ for plugin in hc._plugins: self._verify_hook(hc, plugin) names.append(name) + if not names: raise ValueError("did not find new %r hooks in %r" %(self._prefix, module_or_class)) - def getplugins(self): - """ return the complete list of registered plugins. NOTE that - you will get the internal list and need to make a copy if you - modify the list.""" - return self._plugins + def get_plugins(self): + """ return the set of registered plugins. """ + return set(self._plugin2hookcallers) - def isregistered(self, plugin): - """ Return True if the plugin is already registered under its - canonical name. """ - return self.hasplugin(self._get_canonical_name(plugin)) or \ - plugin in self._plugins + def is_registered(self, plugin): + """ Return True if the plugin is already registered. """ + return plugin in self._plugin2hookcallers - def hasplugin(self, name): - """ Return True if there is a registered with the given name. """ - return name in self._name2plugin - - def getplugin(self, name): + def get_plugin(self, name): """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) def _verify_hook(self, hook, plugin): method = getattr(plugin, hook.name) - pluginname = self._get_canonical_name(plugin) + pluginname = self.get_canonical_name(plugin) + if hook.is_historic() and hasattr(method, "hookwrapper"): raise PluginValidationError( "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %( @@ -344,6 +359,8 @@ ", ".join(hook.argnames))) def check_pending(self): + """ Verify that all hooks which have not been verified against + a hook specification are optional, otherwise raise PluginValidationError""" for name in self.hook.__dict__: if name.startswith(self._prefix): hook = getattr(self.hook, name) @@ -354,10 +371,6 @@ raise PluginValidationError( "unknown hook %r in plugin %r" %(name, plugin)) - def _get_canonical_name(self, plugin): - return getattr(plugin, "__name__", None) or str(id(plugin)) - - class MultiCall: """ execute a call into multiple python functions/methods. """ diff -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 -r 1149ad4609d5737f363e8b3b73099e851096d847 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -11,7 +11,7 @@ import py import pytest from py.builtin import print_ -from _pytest.core import HookCaller, TracedHookExecution +from _pytest.core import TracedHookExecution from _pytest.main import Session, EXIT_OK diff -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 -r 1149ad4609d5737f363e8b3b73099e851096d847 testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -313,7 +313,7 @@ monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) config = testdir.parseconfig("-p", "no:mytestplugin") plugin = config.pluginmanager.getplugin("mytestplugin") - assert plugin == -1 + assert plugin is None def test_cmdline_processargs_simple(testdir): testdir.makeconftest(""" diff -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 -r 1149ad4609d5737f363e8b3b73099e851096d847 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -17,20 +17,34 @@ pm.register(42, name="abc") with pytest.raises(ValueError): pm.register(42, name="abc") + with pytest.raises(ValueError): + pm.register(42, name="def") def test_pm(self, pm): class A: pass a1, a2 = A(), A() pm.register(a1) - assert pm.isregistered(a1) + assert pm.is_registered(a1) pm.register(a2, "hello") - assert pm.isregistered(a2) - l = pm.getplugins() + assert pm.is_registered(a2) + l = pm.get_plugins() assert a1 in l assert a2 in l - assert pm.getplugin('hello') == a2 - pm.unregister(a1) - assert not pm.isregistered(a1) + assert pm.get_plugin('hello') == a2 + assert pm.unregister(a1) == a1 + assert not pm.is_registered(a1) + + def test_set_blocked(self, pm): + class A: pass + a1 = A() + name = pm.register(a1) + assert pm.is_registered(a1) + pm.set_blocked(name) + assert not pm.is_registered(a1) + + pm.set_blocked("somename") + assert not pm.register(A(), "somename") + pm.unregister(name="somename") def test_register_mismatch_method(self, pytestpm): class hello: @@ -53,29 +67,16 @@ pass my = MyPlugin() pm.register(my) - assert pm.getplugins() + assert pm.get_plugins() my2 = MyPlugin() pm.register(my2) - assert pm.getplugins()[-2:] == [my, my2] + assert set([my,my2]).issubset(pm.get_plugins()) - assert pm.isregistered(my) - assert pm.isregistered(my2) + assert pm.is_registered(my) + assert pm.is_registered(my2) pm.unregister(my) - assert not pm.isregistered(my) - assert pm.getplugins()[-1:] == [my2] - - def test_register_unknown_hooks(self, pm): - class Plugin1: - def he_method1(self, arg): - return arg + 1 - - pm.register(Plugin1()) - class Hooks: - def he_method1(self, arg): - pass - pm.addhooks(Hooks) - #assert not pm._unverified_hooks - assert pm.hook.he_method1(arg=1) == [2] + assert not pm.is_registered(my) + assert my not in pm.get_plugins() def test_register_unknown_hooks(self, pm): class Plugin1: @@ -231,6 +232,26 @@ pass assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b] + def test_adding_nonwrappers_trylast3(self, hc, addmeth): + @addmeth() + def he_method1_a(): + pass + + @addmeth(trylast=True) + def he_method1_b(): + pass + + @addmeth() + def he_method1_c(): + pass + + @addmeth(trylast=True) + def he_method1_d(): + pass + assert hc._nonwrappers == [he_method1_d, he_method1_b, + he_method1_a, he_method1_c] + + def test_adding_nonwrappers_trylast2(self, hc, addmeth): @addmeth() def he_method1_middle(): @@ -259,24 +280,6 @@ pass assert hc._nonwrappers == [he_method1_middle, he_method1_b, he_method1] - def test_adding_nonwrappers_trylast(self, hc, addmeth): - @addmeth() - def he_method1_a(): - pass - - @addmeth(trylast=True) - def he_method1_b(): - pass - - @addmeth() - def he_method1_c(): - pass - - @addmeth(trylast=True) - def he_method1_d(): - pass - assert hc._nonwrappers == [he_method1_d, he_method1_b, he_method1_a, he_method1_c] - def test_adding_wrappers_ordering(self, hc, addmeth): @addmeth(hookwrapper=True) def he_method1(): @@ -361,7 +364,7 @@ pm.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=config.pluginmanager)) config.pluginmanager._importconftest(conf) - #print(config.pluginmanager.getplugins()) + #print(config.pluginmanager.get_plugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] @@ -814,21 +817,21 @@ pm = PytestPluginManager() mod = py.std.types.ModuleType("x.y.pytest_hello") pm.register(mod) - assert pm.isregistered(mod) - l = pm.getplugins() + assert pm.is_registered(mod) + l = pm.get_plugins() assert mod in l pytest.raises(ValueError, "pm.register(mod)") pytest.raises(ValueError, lambda: pm.register(mod)) - #assert not pm.isregistered(mod2) - assert pm.getplugins() == l + #assert not pm.is_registered(mod2) + assert pm.get_plugins() == l def test_canonical_import(self, monkeypatch): mod = py.std.types.ModuleType("pytest_xyz") monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) pm = PytestPluginManager() pm.import_plugin('pytest_xyz') - assert pm.getplugin('pytest_xyz') == mod - assert pm.isregistered(mod) + assert pm.get_plugin('pytest_xyz') == mod + assert pm.is_registered(mod) def test_consider_module(self, testdir, pytestpm): testdir.syspathinsert() @@ -837,8 +840,8 @@ mod = py.std.types.ModuleType("temp") mod.pytest_plugins = ["pytest_p1", "pytest_p2"] pytestpm.consider_module(mod) - assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1" - assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" + assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1" + assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2" def test_consider_module_import_module(self, testdir): pytestpm = get_config().pluginmanager @@ -880,13 +883,13 @@ testdir.syspathinsert() testdir.makepyfile(xy123="#") monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pytestpm.getplugins()) + l1 = len(pytestpm.get_plugins()) pytestpm.consider_env() - l2 = len(pytestpm.getplugins()) + l2 = len(pytestpm.get_plugins()) assert l2 == l1 + 1 - assert pytestpm.getplugin('xy123') + assert pytestpm.get_plugin('xy123') pytestpm.consider_env() - l3 = len(pytestpm.getplugins()) + l3 = len(pytestpm.get_plugins()) assert l2 == l3 def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm): @@ -904,7 +907,7 @@ monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) pytestpm.consider_setuptools_entrypoints() - plugin = pytestpm.getplugin("mytestplugin") + plugin = pytestpm.get_plugin("pytest_mytestplugin") assert plugin.x == 42 def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm): @@ -918,7 +921,7 @@ p = testdir.makepyfile(""" import pytest def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') + plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500') assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") @@ -934,13 +937,13 @@ pluginname = "pytest_hello" testdir.makepyfile(**{pluginname: ""}) pytestpm.import_plugin("pytest_hello") - len1 = len(pytestpm.getplugins()) + len1 = len(pytestpm.get_plugins()) pytestpm.import_plugin("pytest_hello") - len2 = len(pytestpm.getplugins()) + len2 = len(pytestpm.get_plugins()) assert len1 == len2 - plugin1 = pytestpm.getplugin("pytest_hello") + plugin1 = pytestpm.get_plugin("pytest_hello") assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pytestpm.getplugin("pytest_hello") + plugin2 = pytestpm.get_plugin("pytest_hello") assert plugin2 is plugin1 def test_import_plugin_dotted_name(self, testdir, pytestpm): @@ -951,7 +954,7 @@ testdir.mkpydir("pkg").join("plug.py").write("x=3") pluginname = "pkg.plug" pytestpm.import_plugin(pluginname) - mod = pytestpm.getplugin("pkg.plug") + mod = pytestpm.get_plugin("pkg.plug") assert mod.x == 3 def test_consider_conftest_deps(self, testdir, pytestpm): @@ -967,15 +970,16 @@ def test_plugin_prevent_register(self, pytestpm): pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) - l1 = pytestpm.getplugins() + l1 = pytestpm.get_plugins() pytestpm.register(42, name="abc") - l2 = pytestpm.getplugins() + l2 = pytestpm.get_plugins() assert len(l2) == len(l1) + assert 42 not in l2 def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): pytestpm.register(42, name="abc") - l1 = pytestpm.getplugins() + l1 = pytestpm.get_plugins() assert 42 in l1 pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) - l2 = pytestpm.getplugins() + l2 = pytestpm.get_plugins() assert 42 not in l2 diff -r 3e9beb7563b6322f4ccb7b8d84dca77cc373db53 -r 1149ad4609d5737f363e8b3b73099e851096d847 testing/test_terminal.py --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -457,7 +457,7 @@ ]) assert result.ret == 1 - if not pytestconfig.pluginmanager.hasplugin("xdist"): + if not pytestconfig.pluginmanager.get_plugin("xdist"): pytest.skip("xdist plugin not installed") result = testdir.runpytest(p1, '-v', '-n 1') https://bitbucket.org/pytest-dev/pytest/commits/a541c2586ba1/ Changeset: a541c2586ba1 Branch: more_plugin User: hpk42 Date: 2015-04-25 18:42:41+00:00 Summary: ensure proper get_name references Affected #: 2 files diff -r 1149ad4609d5737f363e8b3b73099e851096d847 -r a541c2586ba19a4d10f2efad0f8eede50b584fef _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -237,6 +237,10 @@ return TracedHookExecution(self, before, after).undo def make_hook_caller(self, name, plugins): + """ Return a new HookCaller instance which manages calls to + all methods named "name" in the plugins. The new hook caller + is registered internally such that when one of the plugins gets + unregistered, its method will be removed from the hook caller. """ caller = getattr(self.hook, name) hc = HookCaller(caller.name, self._hookexec, caller._specmodule_or_class) for plugin in plugins: @@ -248,7 +252,7 @@ return hc def get_canonical_name(self, plugin): - """ Return canonical name for the plugin object. """ + """ Return canonical name for a plugin object. """ return getattr(plugin, "__name__", None) or str(id(plugin)) def register(self, plugin, name=None): @@ -288,7 +292,7 @@ be specified. """ if name is None: assert plugin is not None - name = self.get_canonical_name(plugin) + name = self.get_name(plugin) if plugin is None: plugin = self.get_plugin(name) @@ -340,9 +344,15 @@ """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) + def get_name(self, plugin): + """ Return name for registered plugin or None if not registered. """ + for name, val in self._name2plugin.items(): + if plugin == val: + return name + def _verify_hook(self, hook, plugin): method = getattr(plugin, hook.name) - pluginname = self.get_canonical_name(plugin) + pluginname = self.get_name(plugin) if hook.is_historic() and hasattr(method, "hookwrapper"): raise PluginValidationError( diff -r 1149ad4609d5737f363e8b3b73099e851096d847 -r a541c2586ba19a4d10f2efad0f8eede50b584fef testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -34,6 +34,22 @@ assert pm.unregister(a1) == a1 assert not pm.is_registered(a1) + def test_pm_name(self, pm): + class A: pass + a1 = A() + name = pm.register(a1, name="hello") + assert name == "hello" + pm.unregister(a1) + assert pm.get_plugin(a1) is None + assert not pm.is_registered(a1) + assert not pm.get_plugins() + name2 = pm.register(a1, name="hello") + assert name2 == name + pm.unregister(name="hello") + assert pm.get_plugin(a1) is None + assert not pm.is_registered(a1) + assert not pm.get_plugins() + def test_set_blocked(self, pm): class A: pass a1 = A() https://bitbucket.org/pytest-dev/pytest/commits/b8139cd5899a/ Changeset: b8139cd5899a Branch: more_plugin User: hpk42 Date: 2015-04-25 20:13:42+00:00 Summary: specialize make_hook_caller to work with a subset of the registered plugins. Affected #: 4 files diff -r a541c2586ba19a4d10f2efad0f8eede50b584fef -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -197,6 +197,7 @@ if conftestpath.check(file=1): mod = self._importconftest(conftestpath) clist.append(mod) + self._path2confmods[path] = clist return clist @@ -220,6 +221,7 @@ mod = conftestpath.pyimport() except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) + self._conftestpath2mod[conftestpath] = mod dirpath = conftestpath.dirpath() if dirpath in self._path2confmods: diff -r a541c2586ba19a4d10f2efad0f8eede50b584fef -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -236,6 +236,21 @@ return TracedHookExecution(self, before, after).undo + def subset_hook_caller(self, name, remove_plugins): + """ Return a new HookCaller instance which manages calls to + the plugins but without hooks from remove_plugins taking part. """ + hc = getattr(self.hook, name) + plugins_to_remove = [plugin for plugin in remove_plugins + if hasattr(plugin, name)] + if plugins_to_remove: + hc = hc.clone() + for plugin in plugins_to_remove: + hc._remove_plugin(plugin) + # we also keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._plugin2hookcallers.setdefault(plugin, []).append(hc) + return hc + def make_hook_caller(self, name, plugins): """ Return a new HookCaller instance which manages calls to all methods named "name" in the plugins. The new hook caller diff -r a541c2586ba19a4d10f2efad0f8eede50b584fef -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -362,9 +362,6 @@ def listnames(self): return [x.name for x in self.listchain()] - def getplugins(self): - return self.config._getmatchingplugins(self.fspath) - def addfinalizer(self, fin): """ register a function to be called when this node is finalized. diff -r a541c2586ba19a4d10f2efad0f8eede50b584fef -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -195,6 +195,44 @@ hc(arg=2) assert l == [10] + def test_subset_hook_caller(self, pm): + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + + l = [] + class Plugin1: + def he_method1(self, arg): + l.append(arg) + class Plugin2: + def he_method1(self, arg): + l.append(arg*10) + class PluginNo: + pass + + plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo() + pm.register(plugin1) + pm.register(plugin2) + pm.register(plugin3) + pm.hook.he_method1(arg=1) + assert l == [10, 1] + l[:] = [] + + hc = pm.subset_hook_caller("he_method1", [plugin1]) + hc(arg=2) + assert l == [20] + l[:] = [] + + hc = pm.subset_hook_caller("he_method1", [plugin2]) + hc(arg=2) + assert l == [2] + l[:] = [] + + pm.unregister(plugin1) + hc(arg=2) + assert l == [] + class TestAddMethodOrdering: @pytest.fixture https://bitbucket.org/pytest-dev/pytest/commits/24043de90f90/ Changeset: 24043de90f90 Branch: more_plugin User: hpk42 Date: 2015-04-25 22:10:52+00:00 Summary: introduce a new subset_hook_caller instead of remove make_hook_caller and adapat and refine conftest/global plugin management accordingly Affected #: 4 files diff -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 -r 24043de90f902e808fde9a6308ec3e9d46eedf50 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -99,7 +99,7 @@ excludefunc=exclude_pytest_names) self._warnings = [] self._plugin_distinfo = [] - self._globalplugins = [] + self._conftest_plugins = set() # state related to local conftest plugins self._path2confmods = {} @@ -121,21 +121,12 @@ def register(self, plugin, name=None, conftest=False): ret = super(PytestPluginManager, self).register(plugin, name) if ret: - if not conftest: - self._globalplugins.append(plugin) self.hook.pytest_plugin_registered.call_historic( kwargs=dict(plugin=plugin, manager=self)) return ret - def unregister(self, plugin=None, name=None): - plugin = super(PytestPluginManager, self).unregister(plugin, name) - try: - self._globalplugins.remove(plugin) - except ValueError: - pass - def getplugin(self, name): - # deprecated + # deprecated naming return self.get_plugin(name) def pytest_configure(self, config): @@ -189,14 +180,20 @@ try: return self._path2confmods[path] except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self._importconftest(conftestpath) - clist.append(mod) + if path.isfile(): + clist = self._getconftestmodules(path.dirpath()) + else: + # XXX these days we may rather want to use config.rootdir + # and allow users to opt into looking into the rootdir parent + # directories instead of requiring to specify confcutdir + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): + mod = self._importconftest(conftestpath) + clist.append(mod) self._path2confmods[path] = clist return clist @@ -222,6 +219,7 @@ except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) + self._conftest_plugins.add(mod) self._conftestpath2mod[conftestpath] = mod dirpath = conftestpath.dirpath() if dirpath in self._path2confmods: @@ -782,10 +780,6 @@ if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) - def _getmatchingplugins(self, fspath): - return self.pluginmanager._globalplugins + \ - self.pluginmanager._getconftestmodules(fspath) - def pytest_load_initial_conftests(self, early_config): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) pytest_load_initial_conftests.trylast = True diff -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 -r 24043de90f902e808fde9a6308ec3e9d46eedf50 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -238,7 +238,8 @@ def subset_hook_caller(self, name, remove_plugins): """ Return a new HookCaller instance which manages calls to - the plugins but without hooks from remove_plugins taking part. """ + the plugins but without hooks from the plugins in remove_plugins + taking part. """ hc = getattr(self.hook, name) plugins_to_remove = [plugin for plugin in remove_plugins if hasattr(plugin, name)] @@ -246,24 +247,6 @@ hc = hc.clone() for plugin in plugins_to_remove: hc._remove_plugin(plugin) - # we also keep track of this hook caller so it - # gets properly removed on plugin unregistration - self._plugin2hookcallers.setdefault(plugin, []).append(hc) - return hc - - def make_hook_caller(self, name, plugins): - """ Return a new HookCaller instance which manages calls to - all methods named "name" in the plugins. The new hook caller - is registered internally such that when one of the plugins gets - unregistered, its method will be removed from the hook caller. """ - caller = getattr(self.hook, name) - hc = HookCaller(caller.name, self._hookexec, caller._specmodule_or_class) - for plugin in plugins: - if hasattr(plugin, name): - hc._add_plugin(plugin) - # we also keep track of this hook caller so it - # gets properly removed on plugin unregistration - self._plugin2hookcallers.setdefault(plugin, []).append(hc) return hc def get_canonical_name(self, plugin): @@ -271,8 +254,8 @@ return getattr(plugin, "__name__", None) or str(id(plugin)) def register(self, plugin, name=None): - """ Register a plugin and return its canonical name or None if it was - blocked from registering. Raise a ValueError if the plugin is already + """ Register a plugin and return its canonical name or None if the name + is blocked from registering. Raise a ValueError if the plugin is already registered. """ plugin_name = name or self.get_canonical_name(plugin) @@ -303,16 +286,15 @@ def unregister(self, plugin=None, name=None): """ unregister a plugin object and all its contained hook implementations - from internal data structures. One of ``plugin`` or ``name`` needs to - be specified. """ + from internal data structures. """ if name is None: - assert plugin is not None + assert plugin is not None, "one of name or plugin needs to be specified" name = self.get_name(plugin) if plugin is None: plugin = self.get_plugin(name) - # None signals blocked registrations, don't delete it + # if self._name2plugin[name] == None registration was blocked: ignore if self._name2plugin.get(name): del self._name2plugin[name] @@ -485,6 +467,7 @@ self._wrappers = [] self._nonwrappers = [] self._hookexec = hook_execute + self._subcaller = [] if specmodule_or_class is not None: self.set_specification(specmodule_or_class) @@ -502,6 +485,21 @@ if hasattr(specfunc, "historic"): self._call_history = [] + def clone(self): + assert not self.is_historic() + hc = object.__new__(HookCaller) + hc.name = self.name + hc._plugins = list(self._plugins) + hc._wrappers = list(self._wrappers) + hc._nonwrappers = list(self._nonwrappers) + hc._hookexec = self._hookexec + hc.argnames = self.argnames + hc.firstresult = self.firstresult + # we keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._subcaller.append(hc) + return hc + def is_historic(self): return hasattr(self, "_call_history") @@ -512,6 +510,10 @@ self._nonwrappers.remove(meth) except ValueError: self._wrappers.remove(meth) + if hasattr(self, "_subcaller"): + for hc in self._subcaller: + if plugin in hc._plugins: + hc._remove_plugin(plugin) def _add_plugin(self, plugin): self._plugins.append(plugin) diff -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 -r 24043de90f902e808fde9a6308ec3e9d46eedf50 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -151,18 +151,17 @@ ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -class FSHookProxy(object): - def __init__(self, fspath, config): +class FSHookProxy: + def __init__(self, fspath, pm, remove_mods): self.fspath = fspath - self.config = config + self.pm = pm + self.remove_mods = remove_mods def __getattr__(self, name): - plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.pluginmanager.make_hook_caller(name, plugins) + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) self.__dict__[name] = x return x - def compatproperty(name): def fget(self): # deprecated - use pytest.name @@ -538,8 +537,20 @@ try: return self._fs2hookproxy[fspath] except KeyError: - self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config) - return x + # check if we have the common case of running + # hooks with all conftest.py filesall conftest.py + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules(fspath) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # one or more conftests are not in use at this fspath + proxy = FSHookProxy(fspath, pm, remove_mods) + else: + # all plugis are active for this fspath + proxy = self.config.hook + + self._fs2hookproxy[fspath] = proxy + return proxy def perform_collect(self, args=None, genitems=True): hook = self.config.hook diff -r b8139cd5899a8cd8fb764a7d83a44397e641a0f1 -r 24043de90f902e808fde9a6308ec3e9d46eedf50 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -176,25 +176,6 @@ l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1)) assert l == [10] - def test_make_hook_caller_unregistered(self, pm): - class Hooks: - def he_method1(self, arg): - pass - pm.addhooks(Hooks) - - l = [] - class Plugin: - def he_method1(self, arg): - l.append(arg * 10) - plugin = Plugin() - pm.register(plugin) - hc = pm.make_hook_caller("he_method1", [plugin]) - hc(arg=1) - assert l == [10] - pm.unregister(plugin) - hc(arg=2) - assert l == [10] - def test_subset_hook_caller(self, pm): class Hooks: def he_method1(self, arg): @@ -232,6 +213,11 @@ pm.unregister(plugin1) hc(arg=2) assert l == [] + l[:] = [] + + pm.hook.he_method1(arg=1) + assert l == [10] + class TestAddMethodOrdering: https://bitbucket.org/pytest-dev/pytest/commits/b7a3c01f99ab/ Changeset: b7a3c01f99ab Branch: more_plugin User: hpk42 Date: 2015-04-25 22:22:34+00:00 Summary: fix some doc strings Affected #: 2 files diff -r 24043de90f902e808fde9a6308ec3e9d46eedf50 -r b7a3c01f99ab9f7a051816a2110695a2004aa60f _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -118,7 +118,7 @@ self.trace.root.setwriter(err.write) self.enable_tracing() - def register(self, plugin, name=None, conftest=False): + def register(self, plugin, name=None): ret = super(PytestPluginManager, self).register(plugin, name) if ret: self.hook.pytest_plugin_registered.call_historic( @@ -263,8 +263,7 @@ self.import_plugin(arg) def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): + if self.register(conftestmodule, name=conftestmodule.__file__): self.consider_module(conftestmodule) def consider_env(self): diff -r 24043de90f902e808fde9a6308ec3e9d46eedf50 -r b7a3c01f99ab9f7a051816a2110695a2004aa60f _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -203,9 +203,8 @@ plugin objects. An optional excludefunc allows to blacklist names which are not considered as hooks despite a matching prefix. - For debugging purposes you can call ``set_tracing(writer)`` - which will subsequently send debug information to the specified - write function. + For debugging purposes you can call ``enable_tracing()`` + which will subsequently send debug information to the trace helper. """ def __init__(self, prefix, excludefunc=None): @@ -219,6 +218,8 @@ MultiCall(methods, kwargs, hook.firstresult).execute() def _hookexec(self, hook, methods, kwargs): + # called from all hookcaller instances. + # enable_tracing will set its own wrapping function at self._inner_hookexec return self._inner_hookexec(hook, methods, kwargs) def enable_tracing(self): @@ -237,9 +238,9 @@ return TracedHookExecution(self, before, after).undo def subset_hook_caller(self, name, remove_plugins): - """ Return a new HookCaller instance which manages calls to - the plugins but without hooks from the plugins in remove_plugins - taking part. """ + """ Return a new HookCaller instance for the named method + which manages calls to all registered plugins except the + ones from remove_plugins. """ hc = getattr(self.hook, name) plugins_to_remove = [plugin for plugin in remove_plugins if hasattr(plugin, name)] @@ -496,7 +497,7 @@ hc.argnames = self.argnames hc.firstresult = self.firstresult # we keep track of this hook caller so it - # gets properly removed on plugin unregistration + # gets properly pruned on plugin unregistration self._subcaller.append(hc) return hc https://bitbucket.org/pytest-dev/pytest/commits/4c95536ad590/ Changeset: 4c95536ad590 Branch: more_plugin User: hpk42 Date: 2015-04-25 22:41:29+00:00 Summary: move consider_setuptools_entrypoints to core pluginmanager Affected #: 3 files diff -r b7a3c01f99ab9f7a051816a2110695a2004aa60f -r 4c95536ad590e3c71fb2944ec4d25cafb6487b3d _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -9,7 +9,7 @@ # DON't import pytest here because it causes import cycle troubles import sys, os from _pytest import hookspec # the extension point definitions -from _pytest.core import PluginManager +from _pytest.core import PluginManager, hookimpl_opts # pytest startup # @@ -98,7 +98,6 @@ super(PytestPluginManager, self).__init__(prefix="pytest_", excludefunc=exclude_pytest_names) self._warnings = [] - self._plugin_distinfo = [] self._conftest_plugins = set() # state related to local conftest plugins @@ -126,16 +125,10 @@ return ret def getplugin(self, name): - # deprecated naming + # support deprecated naming because plugins (xdist e.g.) use it return self.get_plugin(name) def pytest_configure(self, config): - config.addinivalue_line("markers", - "tryfirst: mark a hook implementation function such that the " - "plugin machinery will try to call it first/as early as possible.") - config.addinivalue_line("markers", - "trylast: mark a hook implementation function such that the " - "plugin machinery will try to call it last/as late as possible.") for warning in self._warnings: config.warn(code="I1", message=warning) @@ -236,21 +229,6 @@ # # - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - if self.get_plugin(ep.name) or ep.name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self.register(plugin, name=ep.name) - self._plugin_distinfo.append((ep.dist, plugin)) - def consider_preparse(self, args): for opt1,opt2 in zip(args, args[1:]): if opt1 == "-p": @@ -679,6 +657,7 @@ notset = Notset() FILE_OR_DIR = 'file_or_dir' + class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ @@ -779,9 +758,9 @@ if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) + @hookimpl_opts(trylast=True) def pytest_load_initial_conftests(self, early_config): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - pytest_load_initial_conftests.trylast = True def _initini(self, args): parsed_args = self._parser.parse_known_args(args) @@ -798,7 +777,7 @@ args[:] = self.getini("addopts") + args self._checkversion() self.pluginmanager.consider_preparse(args) - self.pluginmanager.consider_setuptools_entrypoints() + self.pluginmanager.load_setuptools_entrypoints("pytest11") self.pluginmanager.consider_env() self.known_args_namespace = ns = self._parser.parse_known_args(args) try: diff -r b7a3c01f99ab9f7a051816a2110695a2004aa60f -r 4c95536ad590e3c71fb2944ec4d25cafb6487b3d _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -212,6 +212,7 @@ self._excludefunc = excludefunc self._name2plugin = {} self._plugin2hookcallers = {} + self._plugin_distinfo = [] self.trace = TagTracer().get("pluginmanage") self.hook = HookRelay(self.trace.root.get("hook")) self._inner_hookexec = lambda hook, methods, kwargs: \ @@ -379,6 +380,25 @@ raise PluginValidationError( "unknown hook %r in plugin %r" %(name, plugin)) + def load_setuptools_entrypoints(self, entrypoint_name): + """ Load modules from querying the specified entrypoint name. + Return None if setuptools was not operable, otherwise + the number of loaded plugins. """ + try: + from pkg_resources import iter_entry_points, DistributionNotFound + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points(entrypoint_name): + if self.get_plugin(ep.name) or ep.name in self._name2plugin: + continue + try: + plugin = ep.load() + except DistributionNotFound: + continue + self.register(plugin, name=ep.name) + self._plugin_distinfo.append((ep.dist, plugin)) + return len(self._plugin_distinfo) + class MultiCall: """ execute a call into multiple python functions/methods. """ diff -r b7a3c01f99ab9f7a051816a2110695a2004aa60f -r 4c95536ad590e3c71fb2944ec4d25cafb6487b3d testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -383,6 +383,32 @@ results = pm.hook.he_myhook(arg1=17) assert results == 18 + def test_load_setuptools_instantiation(self, monkeypatch, pm): + pkg_resources = pytest.importorskip("pkg_resources") + def my_iter(name): + assert name == "hello" + class EntryPoint: + name = "myname" + dist = None + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + num = pm.load_setuptools_entrypoints("hello") + assert num == 1 + plugin = pm.get_plugin("myname") + assert plugin.x == 42 + assert pm._plugin_distinfo == [(None, plugin)] + + def test_load_setuptools_not_installed(self, monkeypatch, pm): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + assert pm.load_setuptools_entrypoints("qwe") is None + # ok, we did not explode + class TestPytestPluginInteractions: @@ -932,30 +958,6 @@ l3 = len(pytestpm.get_plugins()) assert l2 == l3 - def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm): - pkg_resources = pytest.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "pytest_mytestplugin" - dist = None - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pytestpm.consider_setuptools_entrypoints() - plugin = pytestpm.get_plugin("pytest_mytestplugin") - assert plugin.x == 42 - - def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pytestpm.consider_setuptools_entrypoints() - # ok, we did not explode - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): testdir.makepyfile(pytest_x500="#") p = testdir.makepyfile(""" https://bitbucket.org/pytest-dev/pytest/commits/bedd1996dfa2/ Changeset: bedd1996dfa2 Branch: more_plugin User: hpk42 Date: 2015-04-25 22:47:24+00:00 Summary: re-add tryfirst/trylast marker documentation, mark it as to be removed Affected #: 1 file diff -r 4c95536ad590e3c71fb2944ec4d25cafb6487b3d -r bedd1996dfa2b99ed839943bf290e6883d66ef33 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -129,6 +129,14 @@ return self.get_plugin(name) def pytest_configure(self, config): + # XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...) + # we should remove tryfirst/trylast as markers + config.addinivalue_line("markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible.") + config.addinivalue_line("markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible.") for warning in self._warnings: config.warn(code="I1", message=warning) https://bitbucket.org/pytest-dev/pytest/commits/981e265271a2/ Changeset: 981e265271a2 Branch: more_plugin User: hpk42 Date: 2015-04-26 15:17:59+00:00 Summary: actually revert back to using older simpler method for subset hook calling. It is slightly more inefficient but easier to implement and read. Affected #: 1 file diff -r bedd1996dfa2b99ed839943bf290e6883d66ef33 -r 981e265271a255b6eb14bedef94ff278c4e9c023 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -242,14 +242,19 @@ """ Return a new HookCaller instance for the named method which manages calls to all registered plugins except the ones from remove_plugins. """ - hc = getattr(self.hook, name) + orig = getattr(self.hook, name) plugins_to_remove = [plugin for plugin in remove_plugins if hasattr(plugin, name)] if plugins_to_remove: - hc = hc.clone() - for plugin in plugins_to_remove: - hc._remove_plugin(plugin) - return hc + hc = HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class) + for plugin in orig._plugins: + if plugin not in plugins_to_remove: + hc._add_plugin(plugin) + # we also keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._plugin2hookcallers.setdefault(plugin, []).append(hc) + return hc + return orig def get_canonical_name(self, plugin): """ Return canonical name for a plugin object. """ @@ -488,7 +493,6 @@ self._wrappers = [] self._nonwrappers = [] self._hookexec = hook_execute - self._subcaller = [] if specmodule_or_class is not None: self.set_specification(specmodule_or_class) @@ -506,21 +510,6 @@ if hasattr(specfunc, "historic"): self._call_history = [] - def clone(self): - assert not self.is_historic() - hc = object.__new__(HookCaller) - hc.name = self.name - hc._plugins = list(self._plugins) - hc._wrappers = list(self._wrappers) - hc._nonwrappers = list(self._nonwrappers) - hc._hookexec = self._hookexec - hc.argnames = self.argnames - hc.firstresult = self.firstresult - # we keep track of this hook caller so it - # gets properly pruned on plugin unregistration - self._subcaller.append(hc) - return hc - def is_historic(self): return hasattr(self, "_call_history") @@ -531,10 +520,6 @@ self._nonwrappers.remove(meth) except ValueError: self._wrappers.remove(meth) - if hasattr(self, "_subcaller"): - for hc in self._subcaller: - if plugin in hc._plugins: - hc._remove_plugin(plugin) def _add_plugin(self, plugin): self._plugins.append(plugin) @@ -553,6 +538,7 @@ i = len(nonwrappers) - 1 while i >= 0 and hasattr(nonwrappers[i], "tryfirst"): i -= 1 + # and insert right in front of the tryfirst ones nonwrappers.insert(i+1, meth) https://bitbucket.org/pytest-dev/pytest/commits/ef9d8bd8b77b/ Changeset: ef9d8bd8b77b Branch: more_plugin User: hpk42 Date: 2015-04-27 10:50:34+00:00 Summary: deprecate and warn about __multicall__ usage in hooks, refine docs about hook ordering, make hookwrappers respect tryfirst/trylast Affected #: 7 files diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -39,6 +39,10 @@ - fix issue732: properly unregister plugins from any hook calling sites allowing to have temporary plugins during test execution. +- deprecate and warn about ``__multicall__`` argument in hook + implementations. Use the ``hookwrapper`` mechanism instead already + introduced with pytest-2.7. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -9,7 +9,7 @@ # DON't import pytest here because it causes import cycle troubles import sys, os from _pytest import hookspec # the extension point definitions -from _pytest.core import PluginManager, hookimpl_opts +from _pytest.core import PluginManager, hookimpl_opts, varnames # pytest startup # @@ -117,6 +117,18 @@ self.trace.root.setwriter(err.write) self.enable_tracing() + + def _verify_hook(self, hook, plugin): + super(PytestPluginManager, self)._verify_hook(hook, plugin) + method = getattr(plugin, hook.name) + if "__multicall__" in varnames(method): + fslineno = py.code.getfslineno(method) + warning = dict(code="I1", + fslocation=fslineno, + message="%r hook uses deprecated __multicall__ " + "argument" % (hook.name)) + self._warnings.append(warning) + def register(self, plugin, name=None): ret = super(PytestPluginManager, self).register(plugin, name) if ret: @@ -138,7 +150,10 @@ "trylast: mark a hook implementation function such that the " "plugin machinery will try to call it last/as late as possible.") for warning in self._warnings: - config.warn(code="I1", message=warning) + if isinstance(warning, dict): + config.warn(**warning) + else: + config.warn(code="I1", message=warning) # # internal API for local conftest plugin handling @@ -712,10 +727,10 @@ fin = self._cleanup.pop() fin() - def warn(self, code, message): + def warn(self, code, message, fslocation=None): """ generate a warning for this test session. """ self.hook.pytest_logwarning(code=code, message=message, - fslocation=None, nodeid=None) + fslocation=fslocation, nodeid=None) def get_terminal_writer(self): return self.pluginmanager.get_plugin("terminalreporter")._tw diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -408,6 +408,12 @@ class MultiCall: """ execute a call into multiple python functions/methods. """ + # XXX note that the __multicall__ argument is supported only + # for pytest compatibility reasons. It was never officially + # supported there and is explicitely deprecated since 2.8 + # so we can remove it soon, allowing to avoid the below recursion + # in execute() and simplify/speed up the execute loop. + def __init__(self, methods, kwargs, firstresult=False): self.methods = methods self.kwargs = kwargs @@ -527,20 +533,20 @@ def _add_method(self, meth): if hasattr(meth, 'hookwrapper'): - self._wrappers.append(meth) - elif hasattr(meth, 'trylast'): - self._nonwrappers.insert(0, meth) + methods = self._wrappers + else: + methods = self._nonwrappers + + if hasattr(meth, 'trylast'): + methods.insert(0, meth) elif hasattr(meth, 'tryfirst'): - self._nonwrappers.append(meth) + methods.append(meth) else: - # find the last nonwrapper which is not tryfirst marked - nonwrappers = self._nonwrappers - i = len(nonwrappers) - 1 - while i >= 0 and hasattr(nonwrappers[i], "tryfirst"): + # find last non-tryfirst method + i = len(methods) - 1 + while i >= 0 and hasattr(methods[i], "tryfirst"): i -= 1 - - # and insert right in front of the tryfirst ones - nonwrappers.insert(i+1, meth) + methods.insert(i + 1, meth) def __repr__(self): return "" %(self.name,) diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -164,6 +164,8 @@ def pytest_logwarning(self, code, fslocation, message, nodeid): warnings = self.stats.setdefault("warnings", []) + if isinstance(fslocation, tuple): + fslocation = "%s:%d" % fslocation warning = WarningReport(code=code, fslocation=fslocation, message=message, nodeid=nodeid) warnings.append(warning) diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 doc/en/writing_plugins.txt --- a/doc/en/writing_plugins.txt +++ b/doc/en/writing_plugins.txt @@ -221,36 +221,21 @@ breaking the signatures of existing hook implementations. It is one of the reasons for the general long-lived compatibility of pytest plugins. -Hook function results ---------------------- +Note that hook functions other than ``pytest_runtest_*`` are not +allowed to raise exceptions. Doing so will break the pytest run. + + + +firstresult: stop at first non-None result +------------------------------------------- Most calls to ``pytest`` hooks result in a **list of results** which contains all non-None results of the called hook functions. -Some hooks are specified so that the hook call only executes until the -first function returned a non-None value which is then also the -result of the overall hook call. The remaining hook functions will -not be called in this case. - -Note that hook functions other than ``pytest_runtest_*`` are not -allowed to raise exceptions. Doing so will break the pytest run. - -Hook function ordering ----------------------- - -For any given hook there may be more than one implementation and we thus -generally view ``hook`` execution as a ``1:N`` function call where ``N`` -is the number of registered functions. There are ways to -influence if a hook implementation comes before or after others, i.e. -the position in the ``N``-sized list of functions:: - - @pytest.hookimpl_spec(tryfirst=True) - def pytest_collection_modifyitems(items): - # will execute as early as possible - - @pytest.hookimpl_spec(trylast=True) - def pytest_collection_modifyitems(items): - # will execute as late as possible +Some hook specifications use the ``firstresult=True`` option so that the hook +call only executes until the first of N registered functions returns a +non-None result which is then taken as result of the overall hook call. +The remaining hook functions will not be called in this case. hookwrapper: executing around other hooks @@ -290,6 +275,47 @@ If the result of the underlying hook is a mutable object, they may modify that result, however. + + +Hook function ordering / call example +------------------------------------- + +For any given hook specification there may be more than one +implementation and we thus generally view ``hook`` execution as a +``1:N`` function call where ``N`` is the number of registered functions. +There are ways to influence if a hook implementation comes before or +after others, i.e. the position in the ``N``-sized list of functions:: + + # Plugin 1 + @pytest.hookimpl_spec(tryfirst=True) + def pytest_collection_modifyitems(items): + # will execute as early as possible + + # Plugin 2 + @pytest.hookimpl_spec(trylast=True) + def pytest_collection_modifyitems(items): + # will execute as late as possible + + # Plugin 3 + @pytest.hookimpl_spec(hookwrapper=True) + def pytest_collection_modifyitems(items): + # will execute even before the tryfirst one above! + outcome = yield + # will execute after all non-hookwrappers executed + +Here is the order of execution: + +1. Plugin3's pytest_collection_modifyitems called until the yield point +2. Plugin1's pytest_collection_modifyitems is called +3. Plugin2's pytest_collection_modifyitems is called +4. Plugin3's pytest_collection_modifyitems called for executing after the yield + The yield receives a :py:class:`CallOutcome` instance which encapsulates + the result from calling the non-wrappers. Wrappers cannot modify the result. + +It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with +``hookwrapper=True`` in which case it will influence the ordering of hookwrappers +among each other. + Declaring new hooks ------------------------ diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 testing/conftest.py --- a/testing/conftest.py +++ b/testing/conftest.py @@ -66,13 +66,12 @@ error.append(error[0]) raise AssertionError("\n".join(error)) - at pytest.hookimpl_opts(trylast=True) -def pytest_runtest_teardown(item, __multicall__): + at pytest.hookimpl_opts(hookwrapper=True, trylast=True) +def pytest_runtest_teardown(item): + yield item.config._basedir.chdir() if hasattr(item.config, '_openfiles'): - x = __multicall__.execute() check_open_files(item.config) - return x # XXX copied from execnet's conftest.py - needs to be merged winpymap = { diff -r 981e265271a255b6eb14bedef94ff278c4e9c023 -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -336,6 +336,19 @@ assert hc._nonwrappers == [he_method1_middle] assert hc._wrappers == [he_method1, he_method3] + def test_adding_wrappers_ordering_tryfirst(self, hc, addmeth): + @addmeth(hookwrapper=True, tryfirst=True) + def he_method1(): + pass + + @addmeth(hookwrapper=True) + def he_method2(): + pass + + assert hc._nonwrappers == [] + assert hc._wrappers == [he_method2, he_method1] + + def test_hookspec_opts(self, pm): class HookSpec: @hookspec_opts() @@ -530,6 +543,16 @@ finally: undo() + def test_warn_on_deprecated_multicall(self, pytestpm): + class Plugin: + def pytest_configure(self, __multicall__): + pass + + before = list(pytestpm._warnings) + pytestpm.register(Plugin()) + assert len(pytestpm._warnings) == len(before) + 1 + assert "deprecated" in pytestpm._warnings[-1]["message"] + def test_namespace_has_default_and_env_plugins(testdir): p = testdir.makepyfile(""" @@ -969,7 +992,7 @@ monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") result = testdir.runpytest(p) assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) + result.stdout.fnmatch_lines(["*1 passed*"]) def test_import_plugin_importname(self, testdir, pytestpm): pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') https://bitbucket.org/pytest-dev/pytest/commits/c480b9a898f8/ Changeset: c480b9a898f8 Branch: more_plugin User: hpk42 Date: 2015-04-27 12:10:33+00:00 Summary: simplify load_setuptools_entrypoints and refine comments/docstrings Affected #: 3 files diff -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 -r c480b9a898f87b3d2f5905604412cea14fb5dad2 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -800,7 +800,10 @@ args[:] = self.getini("addopts") + args self._checkversion() self.pluginmanager.consider_preparse(args) - self.pluginmanager.load_setuptools_entrypoints("pytest11") + try: + self.pluginmanager.load_setuptools_entrypoints("pytest11") + 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) try: diff -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 -r c480b9a898f87b3d2f5905604412cea14fb5dad2 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -256,10 +256,6 @@ return hc return orig - def get_canonical_name(self, plugin): - """ Return canonical name for a plugin object. """ - return getattr(plugin, "__name__", None) or str(id(plugin)) - def register(self, plugin, name=None): """ Register a plugin and return its canonical name or None if the name is blocked from registering. Raise a ValueError if the plugin is already @@ -344,6 +340,13 @@ """ Return True if the plugin is already registered. """ return plugin in self._plugin2hookcallers + def get_canonical_name(self, plugin): + """ Return canonical name for a plugin object. Note that a plugin + may be registered under a different name which was specified + by the caller of register(plugin, name). To obtain the name + of an registered plugin use ``get_name(plugin)`` instead.""" + return getattr(plugin, "__name__", None) or str(id(plugin)) + def get_plugin(self, name): """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) @@ -386,14 +389,11 @@ "unknown hook %r in plugin %r" %(name, plugin)) def load_setuptools_entrypoints(self, entrypoint_name): - """ Load modules from querying the specified entrypoint name. - Return None if setuptools was not operable, otherwise - the number of loaded plugins. """ - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning + """ Load modules from querying the specified setuptools entrypoint name. + Return the number of loaded plugins. """ + from pkg_resources import iter_entry_points, DistributionNotFound for ep in iter_entry_points(entrypoint_name): + # is the plugin registered or blocked? if self.get_plugin(ep.name) or ep.name in self._name2plugin: continue try: diff -r ef9d8bd8b77bd79f203ad7152657573a4ba13667 -r c480b9a898f87b3d2f5905604412cea14fb5dad2 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -419,8 +419,8 @@ def test_load_setuptools_not_installed(self, monkeypatch, pm): monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', py.std.types.ModuleType("pkg_resources")) - assert pm.load_setuptools_entrypoints("qwe") is None - # ok, we did not explode + with pytest.raises(ImportError): + pm.load_setuptools_entrypoints("qwe") class TestPytestPluginInteractions: https://bitbucket.org/pytest-dev/pytest/commits/24f4d48abeeb/ Changeset: 24f4d48abeeb User: flub Date: 2015-04-27 12:17:40+00:00 Summary: Merged in hpk42/pytest-patches/more_plugin (pull request #282) another major pluginmanager refactor and docs Affected #: 27 files diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,23 @@ change but it might still break 3rd party plugins which relied on details like especially the pluginmanager.add_shutdown() API. Thanks Holger Krekel. + +- pluginmanagement: introduce ``pytest.hookimpl_opts`` and + ``pytest.hookspec_opts`` decorators for setting impl/spec + specific parameters. This substitutes the previous + now deprecated use of ``pytest.mark`` which is meant to + contain markers for test functions only. + +- write/refine docs for "writing plugins" which now have their + own page and are separate from the "using/installing plugins`` page. + +- fix issue732: properly unregister plugins from any hook calling + sites allowing to have temporary plugins during test execution. + +- deprecate and warn about ``__multicall__`` argument in hook + implementations. Use the ``hookwrapper`` mechanism instead already + introduced with pytest-2.7. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -29,7 +29,7 @@ help="shortcut for --capture=no.") - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_load_initial_conftests(early_config, parser, args): ns = early_config.known_args_namespace pluginmanager = early_config.pluginmanager @@ -101,7 +101,7 @@ if capfuncarg is not None: capfuncarg.close() - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_make_collect_report(self, collector): if isinstance(collector, pytest.File): self.resumecapture() @@ -115,13 +115,13 @@ else: yield - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_setup(self, item): self.resumecapture() yield self.suspendcapture_item(item, "setup") - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_call(self, item): self.resumecapture() self.activate_funcargs(item) @@ -129,17 +129,17 @@ #self.deactivate_funcargs() called from suspendcapture() self.suspendcapture_item(item, "call") - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_teardown(self, item): self.resumecapture() yield self.suspendcapture_item(item, "teardown") - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_keyboard_interrupt(self, excinfo): self.reset_capturings() - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_internalerror(self, excinfo): self.reset_capturings() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -9,7 +9,7 @@ # DON't import pytest here because it causes import cycle troubles import sys, os from _pytest import hookspec # the extension point definitions -from _pytest.core import PluginManager +from _pytest.core import PluginManager, hookimpl_opts, varnames # pytest startup # @@ -38,6 +38,7 @@ tw.line("ERROR: could not load %s\n" % (e.path), red=True) return 4 else: + config.pluginmanager.check_pending() return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace @@ -59,17 +60,17 @@ def _preloadplugins(): assert not _preinit - _preinit.append(get_plugin_manager()) + _preinit.append(get_config()) -def get_plugin_manager(): +def get_config(): if _preinit: return _preinit.pop(0) # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - pluginmanager.config = Config(pluginmanager) # XXX attr needed? + config = Config(pluginmanager) for spec in default_plugins: pluginmanager.import_plugin(spec) - return pluginmanager + return config def _prepareconfig(args=None, plugins=None): if args is None: @@ -80,7 +81,7 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_plugin_manager() + pluginmanager = get_config().pluginmanager if plugins: for plugin in plugins: pluginmanager.register(plugin) @@ -97,8 +98,7 @@ super(PytestPluginManager, self).__init__(prefix="pytest_", excludefunc=exclude_pytest_names) self._warnings = [] - self._plugin_distinfo = [] - self._globalplugins = [] + self._conftest_plugins = set() # state related to local conftest plugins self._path2confmods = {} @@ -114,28 +114,35 @@ err = py.io.dupfile(err, encoding=encoding) except Exception: pass - self.set_tracing(err.write) + self.trace.root.setwriter(err.write) + self.enable_tracing() - def register(self, plugin, name=None, conftest=False): + + def _verify_hook(self, hook, plugin): + super(PytestPluginManager, self)._verify_hook(hook, plugin) + method = getattr(plugin, hook.name) + if "__multicall__" in varnames(method): + fslineno = py.code.getfslineno(method) + warning = dict(code="I1", + fslocation=fslineno, + message="%r hook uses deprecated __multicall__ " + "argument" % (hook.name)) + self._warnings.append(warning) + + def register(self, plugin, name=None): ret = super(PytestPluginManager, self).register(plugin, name) - if ret and not conftest: - self._globalplugins.append(plugin) + if ret: + self.hook.pytest_plugin_registered.call_historic( + kwargs=dict(plugin=plugin, manager=self)) return ret - def _do_register(self, plugin, name): - # called from core PluginManager class - if hasattr(self, "config"): - self.config._register_plugin(plugin, name) - return super(PytestPluginManager, self)._do_register(plugin, name) - - def unregister(self, plugin): - super(PytestPluginManager, self).unregister(plugin) - try: - self._globalplugins.remove(plugin) - except ValueError: - pass + def getplugin(self, name): + # support deprecated naming because plugins (xdist e.g.) use it + return self.get_plugin(name) def pytest_configure(self, config): + # XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...) + # we should remove tryfirst/trylast as markers config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " "plugin machinery will try to call it first/as early as possible.") @@ -143,7 +150,10 @@ "trylast: mark a hook implementation function such that the " "plugin machinery will try to call it last/as late as possible.") for warning in self._warnings: - config.warn(code="I1", message=warning) + if isinstance(warning, dict): + config.warn(**warning) + else: + config.warn(code="I1", message=warning) # # internal API for local conftest plugin handling @@ -186,14 +196,21 @@ try: return self._path2confmods[path] except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self._importconftest(conftestpath) - clist.append(mod) + if path.isfile(): + clist = self._getconftestmodules(path.dirpath()) + else: + # XXX these days we may rather want to use config.rootdir + # and allow users to opt into looking into the rootdir parent + # directories instead of requiring to specify confcutdir + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._path2confmods[path] = clist return clist @@ -217,6 +234,8 @@ mod = conftestpath.pyimport() except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) + + self._conftest_plugins.add(mod) self._conftestpath2mod[conftestpath] = mod dirpath = conftestpath.dirpath() if dirpath in self._path2confmods: @@ -233,24 +252,6 @@ # # - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) - def consider_preparse(self, args): for opt1,opt2 in zip(args, args[1:]): if opt1 == "-p": @@ -258,18 +259,12 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 + self.set_blocked(arg[3:]) else: - if self.getplugin(arg) is None: - self.import_plugin(arg) + self.import_plugin(arg) def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): + if self.register(conftestmodule, name=conftestmodule.__file__): self.consider_module(conftestmodule) def consider_env(self): @@ -291,7 +286,7 @@ # basename for historic purposes but must be imported with the # _pytest prefix. assert isinstance(modname, str) - if self.getplugin(modname) is not None: + if self.get_plugin(modname) is not None: return if modname in builtin_plugins: importspec = "_pytest." + modname @@ -685,6 +680,7 @@ notset = Notset() FILE_OR_DIR = 'file_or_dir' + class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ @@ -706,20 +702,11 @@ self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - - def _register_plugin(self, plugin, name): - call_plugin = self.pluginmanager.call_plugin - call_plugin(plugin, "pytest_addhooks", - {'pluginmanager': self.pluginmanager}) - self.hook.pytest_plugin_registered(plugin=plugin, - manager=self.pluginmanager) - dic = call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: + def do_setns(dic): import pytest setns(pytest, dic) - call_plugin(plugin, "pytest_addoption", {'parser': self._parser}) - if self._configured: - call_plugin(plugin, "pytest_configure", {'config': self}) + self.hook.pytest_namespace.call_historic(do_setns, {}) + self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) def add_cleanup(self, func): """ Add a function to be called when the config object gets out of @@ -729,26 +716,27 @@ def _do_configure(self): assert not self._configured self._configured = True - self.hook.pytest_configure(config=self) + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) def _ensure_unconfigure(self): if self._configured: self._configured = False self.hook.pytest_unconfigure(config=self) + self.hook.pytest_configure._call_history = [] while self._cleanup: fin = self._cleanup.pop() fin() - def warn(self, code, message): + def warn(self, code, message, fslocation=None): """ generate a warning for this test session. """ self.hook.pytest_logwarning(code=code, message=message, - fslocation=None, nodeid=None) + fslocation=fslocation, nodeid=None) def get_terminal_writer(self): - return self.pluginmanager.getplugin("terminalreporter")._tw + return self.pluginmanager.get_plugin("terminalreporter")._tw def pytest_cmdline_parse(self, pluginmanager, args): - assert self == pluginmanager.config, (self, pluginmanager.config) + # REF1 assert self == pluginmanager.config, (self, pluginmanager.config) self.parse(args) return self @@ -778,8 +766,7 @@ @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ - pluginmanager = get_plugin_manager() - config = pluginmanager.config + config = get_config() config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) for x in config.option.plugins: @@ -794,13 +781,9 @@ if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) - def _getmatchingplugins(self, fspath): - return self.pluginmanager._globalplugins + \ - self.pluginmanager._getconftestmodules(fspath) - + @hookimpl_opts(trylast=True) def pytest_load_initial_conftests(self, early_config): self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - pytest_load_initial_conftests.trylast = True def _initini(self, args): parsed_args = self._parser.parse_known_args(args) @@ -817,7 +800,10 @@ args[:] = self.getini("addopts") + args self._checkversion() self.pluginmanager.consider_preparse(args) - self.pluginmanager.consider_setuptools_entrypoints() + try: + self.pluginmanager.load_setuptools_entrypoints("pytest11") + 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) try: @@ -850,6 +836,8 @@ 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) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -2,11 +2,65 @@ PluginManager, basic initialization and tracing. """ import sys -import inspect +from inspect import isfunction, ismethod, isclass, formatargspec, getargspec import py py3 = sys.version_info > (3,0) +def hookspec_opts(firstresult=False, historic=False): + """ returns a decorator which will define a function as a hook specfication. + + If firstresult is True the 1:N hook call (N being the number of registered + hook implementation functions) will stop at I<=N when the I'th function + returns a non-None result. + + If historic is True calls to a hook will be memorized and replayed + on later registered plugins. + """ + def setattr_hookspec_opts(func): + if historic and firstresult: + raise ValueError("cannot have a historic firstresult hook") + if firstresult: + func.firstresult = firstresult + if historic: + func.historic = historic + return func + return setattr_hookspec_opts + + +def hookimpl_opts(hookwrapper=False, optionalhook=False, + tryfirst=False, trylast=False): + """ Return a decorator which marks a function as a hook implementation. + + If optionalhook is True a missing matching hook specification will not result + in an error (by default it is an error if no matching spec is found). + + If tryfirst is True this hook implementation will run as early as possible + in the chain of N hook implementations for a specfication. + + If trylast is True this hook implementation will run as late as possible + in the chain of N hook implementations. + + If hookwrapper is True the hook implementations needs to execute exactly + one "yield". The code before the yield is run early before any non-hookwrapper + function is run. The code after the yield is run after all non-hookwrapper + function have run. The yield receives an ``CallOutcome`` object representing + the exception or result outcome of the inner calls (including other hookwrapper + calls). + """ + def setattr_hookimpl_opts(func): + if hookwrapper: + func.hookwrapper = True + if optionalhook: + func.optionalhook = True + if tryfirst: + func.tryfirst = True + if trylast: + func.trylast = True + return func + return setattr_hookimpl_opts + + class TagTracer: def __init__(self): self._tag2proc = {} @@ -53,42 +107,28 @@ assert isinstance(tags, tuple) self._tag2proc[tags] = processor + class TagTracerSub: def __init__(self, root, tags): self.root = root self.tags = tags + def __call__(self, *args): self.root.processmessage(self.tags, args) + def setmyprocessor(self, processor): self.root.setprocessor(self.tags, processor) + def get(self, name): return self.__class__(self.root, self.tags + (name,)) -def add_method_wrapper(cls, wrapper_func): - """ Substitute the function named "wrapperfunc.__name__" at class - "cls" with a function that wraps the call to the original function. - Return an undo function which can be called to reset the class to use - the old method again. - - wrapper_func is called with the same arguments as the method - it wraps and its result is used as a wrap_controller for - calling the original function. - """ - name = wrapper_func.__name__ - oldcall = getattr(cls, name) - def wrap_exec(*args, **kwargs): - gen = wrapper_func(*args, **kwargs) - return wrapped_call(gen, lambda: oldcall(*args, **kwargs)) - - setattr(cls, name, wrap_exec) - return lambda: setattr(cls, name, oldcall) - def raise_wrapfail(wrap_controller, msg): co = wrap_controller.gi_code raise RuntimeError("wrap_controller at %r %s:%d %s" % (co.co_name, co.co_filename, co.co_firstlineno, msg)) + def wrapped_call(wrap_controller, func): """ Wrap calling to a function with a generator which needs to yield exactly once. The yield point will trigger calling the wrapped function @@ -133,6 +173,25 @@ py.builtin._reraise(*ex) +class TracedHookExecution: + def __init__(self, pluginmanager, before, after): + self.pluginmanager = pluginmanager + self.before = before + self.after = after + self.oldcall = pluginmanager._inner_hookexec + assert not isinstance(self.oldcall, TracedHookExecution) + self.pluginmanager._inner_hookexec = self + + def __call__(self, hook, methods, kwargs): + self.before(hook, methods, kwargs) + outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs)) + self.after(outcome, hook, methods, kwargs) + return outcome.get_result() + + def undo(self): + self.pluginmanager._inner_hookexec = self.oldcall + + class PluginManager(object): """ Core Pluginmanager class which manages registration of plugin objects and 1:N hook calling. @@ -144,197 +203,228 @@ plugin objects. An optional excludefunc allows to blacklist names which are not considered as hooks despite a matching prefix. - For debugging purposes you can call ``set_tracing(writer)`` - which will subsequently send debug information to the specified - write function. + For debugging purposes you can call ``enable_tracing()`` + which will subsequently send debug information to the trace helper. """ def __init__(self, prefix, excludefunc=None): self._prefix = prefix self._excludefunc = excludefunc self._name2plugin = {} - self._plugins = [] self._plugin2hookcallers = {} + self._plugin_distinfo = [] self.trace = TagTracer().get("pluginmanage") - self.hook = HookRelay(pm=self) + self.hook = HookRelay(self.trace.root.get("hook")) + self._inner_hookexec = lambda hook, methods, kwargs: \ + MultiCall(methods, kwargs, hook.firstresult).execute() - def set_tracing(self, writer): - """ turn on tracing to the given writer method and - return an undo function. """ - self.trace.root.setwriter(writer) - # reconfigure HookCalling to perform tracing - assert not hasattr(self, "_wrapping") - self._wrapping = True + def _hookexec(self, hook, methods, kwargs): + # called from all hookcaller instances. + # enable_tracing will set its own wrapping function at self._inner_hookexec + return self._inner_hookexec(hook, methods, kwargs) - hooktrace = self.hook.trace + def enable_tracing(self): + """ enable tracing of hook calls and return an undo function. """ + hooktrace = self.hook._trace - def _docall(self, methods, kwargs): + def before(hook, methods, kwargs): hooktrace.root.indent += 1 - hooktrace(self.name, kwargs) - box = yield - if box.excinfo is None: - hooktrace("finish", self.name, "-->", box.result) + hooktrace(hook.name, kwargs) + + def after(outcome, hook, methods, kwargs): + if outcome.excinfo is None: + hooktrace("finish", hook.name, "-->", outcome.result) hooktrace.root.indent -= 1 - return add_method_wrapper(HookCaller, _docall) + return TracedHookExecution(self, before, after).undo - def make_hook_caller(self, name, plugins): - caller = getattr(self.hook, name) - methods = self.listattr(name, plugins=plugins) - return HookCaller(caller.name, caller.firstresult, - argnames=caller.argnames, methods=methods) + def subset_hook_caller(self, name, remove_plugins): + """ Return a new HookCaller instance for the named method + which manages calls to all registered plugins except the + ones from remove_plugins. """ + orig = getattr(self.hook, name) + plugins_to_remove = [plugin for plugin in remove_plugins + if hasattr(plugin, name)] + if plugins_to_remove: + hc = HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class) + for plugin in orig._plugins: + if plugin not in plugins_to_remove: + hc._add_plugin(plugin) + # we also keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._plugin2hookcallers.setdefault(plugin, []).append(hc) + return hc + return orig def register(self, plugin, name=None): - """ Register a plugin with the given name and ensure that all its - hook implementations are integrated. If the name is not specified - we use the ``__name__`` attribute of the plugin object or, if that - doesn't exist, the id of the plugin. This method will raise a - ValueError if the eventual name is already registered. """ - name = name or self._get_canonical_name(plugin) - if self._name2plugin.get(name, None) == -1: - return - if self.hasplugin(name): + """ Register a plugin and return its canonical name or None if the name + is blocked from registering. Raise a ValueError if the plugin is already + registered. """ + plugin_name = name or self.get_canonical_name(plugin) + + if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: + if self._name2plugin.get(plugin_name, -1) is None: + return # blocked plugin, return None to indicate no registration raise ValueError("Plugin already registered: %s=%s\n%s" %( - name, plugin, self._name2plugin)) - #self.trace("registering", name, plugin) - # allow subclasses to intercept here by calling a helper - return self._do_register(plugin, name) + plugin_name, plugin, self._name2plugin)) - def _do_register(self, plugin, name): - hookcallers = list(self._scan_plugin(plugin)) - self._plugin2hookcallers[plugin] = hookcallers - self._name2plugin[name] = plugin - self._plugins.append(plugin) - # rescan all methods for the hookcallers we found - for hookcaller in hookcallers: - self._scan_methods(hookcaller) - return True + self._name2plugin[plugin_name] = plugin - def unregister(self, plugin): - """ unregister the plugin object and all its contained hook implementations + # register prefix-matching hook specs of the plugin + self._plugin2hookcallers[plugin] = hookcallers = [] + for name in dir(plugin): + if name.startswith(self._prefix): + hook = getattr(self.hook, name, None) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + hook = HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) + elif hook.has_spec(): + self._verify_hook(hook, plugin) + hook._maybe_apply_history(getattr(plugin, name)) + hookcallers.append(hook) + hook._add_plugin(plugin) + return plugin_name + + def unregister(self, plugin=None, name=None): + """ unregister a plugin object and all its contained hook implementations from internal data structures. """ - self._plugins.remove(plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - hookcallers = self._plugin2hookcallers.pop(plugin) - for hookcaller in hookcallers: - self._scan_methods(hookcaller) + if name is None: + assert plugin is not None, "one of name or plugin needs to be specified" + name = self.get_name(plugin) + + if plugin is None: + plugin = self.get_plugin(name) + + # if self._name2plugin[name] == None registration was blocked: ignore + if self._name2plugin.get(name): + del self._name2plugin[name] + + for hookcaller in self._plugin2hookcallers.pop(plugin, []): + hookcaller._remove_plugin(plugin) + + return plugin + + def set_blocked(self, name): + """ block registrations of the given name, unregister if already registered. """ + self.unregister(name=name) + self._name2plugin[name] = None def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using the prefix/excludefunc with which the PluginManager was initialized. """ - isclass = int(inspect.isclass(module_or_class)) names = [] for name in dir(module_or_class): if name.startswith(self._prefix): - method = module_or_class.__dict__[name] - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self.hook, name, hc) + hc = getattr(self.hook, name, None) + if hc is None: + hc = HookCaller(name, self._hookexec, module_or_class) + setattr(self.hook, name, hc) + else: + # plugins registered this hook without knowing the spec + hc.set_specification(module_or_class) + for plugin in hc._plugins: + self._verify_hook(hc, plugin) names.append(name) + if not names: raise ValueError("did not find new %r hooks in %r" %(self._prefix, module_or_class)) - def getplugins(self): - """ return the complete list of registered plugins. NOTE that - you will get the internal list and need to make a copy if you - modify the list.""" - return self._plugins + def get_plugins(self): + """ return the set of registered plugins. """ + return set(self._plugin2hookcallers) - def isregistered(self, plugin): - """ Return True if the plugin is already registered under its - canonical name. """ - return self.hasplugin(self._get_canonical_name(plugin)) or \ - plugin in self._plugins + def is_registered(self, plugin): + """ Return True if the plugin is already registered. """ + return plugin in self._plugin2hookcallers - def hasplugin(self, name): - """ Return True if there is a registered with the given name. """ - return name in self._name2plugin + def get_canonical_name(self, plugin): + """ Return canonical name for a plugin object. Note that a plugin + may be registered under a different name which was specified + by the caller of register(plugin, name). To obtain the name + of an registered plugin use ``get_name(plugin)`` instead.""" + return getattr(plugin, "__name__", None) or str(id(plugin)) - def getplugin(self, name): + def get_plugin(self, name): """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - last = [] - wrappers = [] - for plugin in plugins: + def get_name(self, plugin): + """ Return name for registered plugin or None if not registered. """ + for name, val in self._name2plugin.items(): + if plugin == val: + return name + + def _verify_hook(self, hook, plugin): + method = getattr(plugin, hook.name) + pluginname = self.get_name(plugin) + + if hook.is_historic() and hasattr(method, "hookwrapper"): + raise PluginValidationError( + "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %( + pluginname, hook.name)) + + for arg in varnames(method): + if arg not in hook.argnames: + raise PluginValidationError( + "Plugin %r\nhook %r\nargument %r not available\n" + "plugin definition: %s\n" + "available hookargs: %s" %( + pluginname, hook.name, arg, formatdef(method), + ", ".join(hook.argnames))) + + def check_pending(self): + """ Verify that all hooks which have not been verified against + a hook specification are optional, otherwise raise PluginValidationError""" + for name in self.hook.__dict__: + if name.startswith(self._prefix): + hook = getattr(self.hook, name) + if not hook.has_spec(): + for plugin in hook._plugins: + method = getattr(plugin, hook.name) + if not getattr(method, "optionalhook", False): + raise PluginValidationError( + "unknown hook %r in plugin %r" %(name, plugin)) + + def load_setuptools_entrypoints(self, entrypoint_name): + """ Load modules from querying the specified setuptools entrypoint name. + Return the number of loaded plugins. """ + from pkg_resources import iter_entry_points, DistributionNotFound + for ep in iter_entry_points(entrypoint_name): + # is the plugin registered or blocked? + if self.get_plugin(ep.name) or ep.name in self._name2plugin: + continue try: - meth = getattr(plugin, attrname) - except AttributeError: + plugin = ep.load() + except DistributionNotFound: continue - if hasattr(meth, 'hookwrapper'): - wrappers.append(meth) - elif hasattr(meth, 'tryfirst'): - last.append(meth) - elif hasattr(meth, 'trylast'): - l.insert(0, meth) - else: - l.append(meth) - l.extend(last) - l.extend(wrappers) - return l - - def _scan_methods(self, hookcaller): - hookcaller.methods = self.listattr(hookcaller.name) - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() - - - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - - for name in dir(plugin): - if name[0] == "_" or not name.startswith(self._prefix): - continue - hook = getattr(self.hook, name, None) - method = getattr(plugin, name) - if hook is None: - if self._excludefunc is not None and self._excludefunc(name): - continue - if getattr(method, 'optionalhook', False): - continue - fail("found unknown hook: %r", name) - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook - - def _get_canonical_name(self, plugin): - return getattr(plugin, "__name__", None) or str(id(plugin)) - + self.register(plugin, name=ep.name) + self._plugin_distinfo.append((ep.dist, plugin)) + return len(self._plugin_distinfo) class MultiCall: """ execute a call into multiple python functions/methods. """ + # XXX note that the __multicall__ argument is supported only + # for pytest compatibility reasons. It was never officially + # supported there and is explicitely deprecated since 2.8 + # so we can remove it soon, allowing to avoid the below recursion + # in execute() and simplify/speed up the execute loop. + def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) + self.methods = methods self.kwargs = kwargs self.kwargs["__multicall__"] = self - self.results = [] self.firstresult = firstresult - def __repr__(self): - status = "%d results, %d meths" % (len(self.results), len(self.methods)) - return "" %(status, self.kwargs) - def execute(self): all_kwargs = self.kwargs + self.results = results = [] + firstresult = self.firstresult + while self.methods: method = self.methods.pop() args = [all_kwargs[argname] for argname in varnames(method)] @@ -342,11 +432,19 @@ return wrapped_call(method(*args), self.execute) res = method(*args) if res is not None: - self.results.append(res) - if self.firstresult: + if firstresult: return res - if not self.firstresult: - return self.results + results.append(res) + + if not firstresult: + return results + + def __repr__(self): + status = "%d meths" % (len(self.methods),) + if hasattr(self, "results"): + status = ("%d results, " % len(self.results)) + status + return "" %(status, self.kwargs) + def varnames(func, startindex=None): @@ -361,17 +459,17 @@ return cache["_varnames"] except KeyError: pass - if inspect.isclass(func): + if isclass(func): try: func = func.__init__ except AttributeError: return () startindex = 1 else: - if not inspect.isfunction(func) and not inspect.ismethod(func): + if not isfunction(func) and not ismethod(func): func = getattr(func, '__call__', func) if startindex is None: - startindex = int(inspect.ismethod(func)) + startindex = int(ismethod(func)) rawcode = py.code.getrawcode(func) try: @@ -390,32 +488,95 @@ class HookRelay: - def __init__(self, pm): - self._pm = pm - self.trace = pm.trace.root.get("hook") + def __init__(self, trace): + self._trace = trace -class HookCaller: - def __init__(self, name, firstresult, argnames, methods=()): +class HookCaller(object): + def __init__(self, name, hook_execute, specmodule_or_class=None): self.name = name - self.firstresult = firstresult - self.argnames = ["__multicall__"] - self.argnames.extend(argnames) + self._plugins = [] + self._wrappers = [] + self._nonwrappers = [] + self._hookexec = hook_execute + if specmodule_or_class is not None: + self.set_specification(specmodule_or_class) + + def has_spec(self): + return hasattr(self, "_specmodule_or_class") + + def set_specification(self, specmodule_or_class): + assert not self.has_spec() + self._specmodule_or_class = specmodule_or_class + specfunc = getattr(specmodule_or_class, self.name) + argnames = varnames(specfunc, startindex=isclass(specmodule_or_class)) assert "self" not in argnames # sanity check - self.methods = methods + self.argnames = ["__multicall__"] + list(argnames) + self.firstresult = getattr(specfunc, 'firstresult', False) + if hasattr(specfunc, "historic"): + self._call_history = [] + + def is_historic(self): + return hasattr(self, "_call_history") + + def _remove_plugin(self, plugin): + self._plugins.remove(plugin) + meth = getattr(plugin, self.name) + try: + self._nonwrappers.remove(meth) + except ValueError: + self._wrappers.remove(meth) + + def _add_plugin(self, plugin): + self._plugins.append(plugin) + self._add_method(getattr(plugin, self.name)) + + def _add_method(self, meth): + if hasattr(meth, 'hookwrapper'): + methods = self._wrappers + else: + methods = self._nonwrappers + + if hasattr(meth, 'trylast'): + methods.insert(0, meth) + elif hasattr(meth, 'tryfirst'): + methods.append(meth) + else: + # find last non-tryfirst method + i = len(methods) - 1 + while i >= 0 and hasattr(methods[i], "tryfirst"): + i -= 1 + methods.insert(i + 1, meth) def __repr__(self): return "" %(self.name,) def __call__(self, **kwargs): - return self._docall(self.methods, kwargs) + assert not self.is_historic() + return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) - def callextra(self, methods, **kwargs): - return self._docall(self.methods + methods, kwargs) + def call_historic(self, proc=None, kwargs=None): + self._call_history.append((kwargs or {}, proc)) + # historizing hooks don't return results + self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) - def _docall(self, methods, kwargs): - return MultiCall(methods, kwargs, - firstresult=self.firstresult).execute() + def call_extra(self, methods, kwargs): + """ Call the hook with some additional temporarily participating + methods using the specified kwargs as call parameters. """ + old = list(self._nonwrappers), list(self._wrappers) + for method in methods: + self._add_method(method) + try: + return self(**kwargs) + finally: + self._nonwrappers, self._wrappers = old + + def _maybe_apply_history(self, method): + if self.is_historic(): + for kwargs, proc in self._call_history: + res = self._hookexec(self, [method], kwargs) + if res and proc is not None: + proc(res[0]) class PluginValidationError(Exception): @@ -425,5 +586,5 @@ def formatdef(func): return "%s%s" % ( func.__name__, - inspect.formatargspec(*inspect.getargspec(func)) + formatargspec(*getargspec(func)) ) diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -22,7 +22,7 @@ help="store internal tracing debug information in 'pytestdebug.log'.") - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_cmdline_parse(): outcome = yield config = outcome.get_result() @@ -34,13 +34,15 @@ pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(debugfile.write) + config.trace.root.setwriter(debugfile.write) + undo_tracing = config.pluginmanager.enable_tracing() sys.stderr.write("writing pytestdebug information to %s\n" % path) def unset_tracing(): debugfile.close() sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) config.trace.root.setwriter(None) + undo_tracing() config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -1,27 +1,30 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ +from _pytest.core import hookspec_opts + # ------------------------------------------------------------------------- -# Initialization +# Initialization hooks called for every plugin # ------------------------------------------------------------------------- + at hookspec_opts(historic=True) def pytest_addhooks(pluginmanager): - """called at plugin load time to allow adding new hooks via a call to + """called at plugin registration time to allow adding new hooks via a call to pluginmanager.addhooks(module_or_class, prefix).""" + at hookspec_opts(historic=True) def pytest_namespace(): """return dict of name->object to be made globally available in - the pytest namespace. This hook is called before command line options - are parsed. + the pytest namespace. This hook is called at plugin registration + time. """ -def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ -pytest_cmdline_parse.firstresult = True + at hookspec_opts(historic=True) +def pytest_plugin_registered(plugin, manager): + """ a new pytest plugin got registered. """ -def pytest_cmdline_preparse(config, args): - """(deprecated) modify command line arguments before option parsing. """ + at hookspec_opts(historic=True) def pytest_addoption(parser): """register argparse-style options and ini-style config values. @@ -47,35 +50,43 @@ via (deprecated) ``pytest.config``. """ + at hookspec_opts(historic=True) +def pytest_configure(config): + """ called after command line options have been parsed + and all plugins and initial conftest files been loaded. + This hook is called for every plugin. + """ + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins as well as directly +# discoverable conftest.py local plugins. +# ------------------------------------------------------------------------- + + at hookspec_opts(firstresult=True) +def pytest_cmdline_parse(pluginmanager, args): + """return initialized config object, parsing the specified args. """ + +def pytest_cmdline_preparse(config, args): + """(deprecated) modify command line arguments before option parsing. """ + + at hookspec_opts(firstresult=True) def pytest_cmdline_main(config): """ called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. """ -pytest_cmdline_main.firstresult = True def pytest_load_initial_conftests(args, early_config, parser): """ implements the loading of initial conftest files ahead of command line option parsing. """ -def pytest_configure(config): - """ called after command line options have been parsed - and all plugins and initial conftest files been loaded. - """ - -def pytest_unconfigure(config): - """ called before test process is exited. """ - -def pytest_runtestloop(session): - """ called for performing the main runtest loop - (after collection finished). """ -pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_collection(session): """ perform the collection protocol for the given session. """ -pytest_collection.firstresult = True def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order @@ -84,16 +95,16 @@ def pytest_collection_finish(session): """ called after collection has been performed and modified. """ + at hookspec_opts(firstresult=True) def pytest_ignore_collect(path, config): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. """ -pytest_ignore_collect.firstresult = True + at hookspec_opts(firstresult=True) def pytest_collect_directory(path, parent): """ called before traversing a directory for collection files. """ -pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): """ return collection Node or None for the given path. Any new node @@ -112,29 +123,29 @@ def pytest_deselected(items): """ called for test items deselected by keyword. """ + at hookspec_opts(firstresult=True) def pytest_make_collect_report(collector): """ perform ``collector.collect()`` and return a CollectReport. """ -pytest_make_collect_report.firstresult = True # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_pycollect_makemodule(path, parent): """ return a Module collector or None for the given path. This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to create test modules for files that do not match as a test module. """ -pytest_pycollect_makemodule.firstresult = True + at hookspec_opts(firstresult=True) def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ -pytest_pycollect_makeitem.firstresult = True + at hookspec_opts(firstresult=True) def pytest_pyfunc_call(pyfuncitem): """ call underlying test function. """ -pytest_pyfunc_call.firstresult = True def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" @@ -142,9 +153,16 @@ # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- + + at hookspec_opts(firstresult=True) +def pytest_runtestloop(session): + """ called for performing the main runtest loop + (after collection finished). """ + def pytest_itemstart(item, node): """ (deprecated, use pytest_runtest_logstart). """ + at hookspec_opts(firstresult=True) def pytest_runtest_protocol(item, nextitem): """ implements the runtest_setup/call/teardown protocol for the given test item, including capturing exceptions and calling @@ -158,7 +176,6 @@ :return boolean: True if no further hook implementations should be invoked. """ -pytest_runtest_protocol.firstresult = True def pytest_runtest_logstart(nodeid, location): """ signal the start of running a single test item. """ @@ -178,12 +195,12 @@ so that nextitem only needs to call setup-functions. """ + at hookspec_opts(firstresult=True) def pytest_runtest_makereport(item, call): """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item` and :py:class:`_pytest.runner.CallInfo`. """ -pytest_runtest_makereport.firstresult = True def pytest_runtest_logreport(report): """ process a test setup/call/teardown report relating to @@ -199,6 +216,9 @@ def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ +def pytest_unconfigure(config): + """ called before test process is exited. """ + # ------------------------------------------------------------------------- # hooks for customising the assert methods @@ -220,9 +240,9 @@ def pytest_report_header(config, startdir): """ return a string to be displayed as header info for terminal reporting.""" + at hookspec_opts(firstresult=True) def pytest_report_teststatus(report): """ return result-category, shortletter and verbose word for reporting.""" -pytest_report_teststatus.firstresult = True def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ @@ -236,17 +256,14 @@ # doctest hooks # ------------------------------------------------------------------------- + at hookspec_opts(firstresult=True) def pytest_doctest_prepare_content(content): """ return processed content for a given doctest""" -pytest_doctest_prepare_content.firstresult = True # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- -def pytest_plugin_registered(plugin, manager): - """ a new pytest plugin got registered. """ - def pytest_internalerror(excrepr, excinfo): """ called for internal errors. """ diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -151,18 +151,17 @@ ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -class FSHookProxy(object): - def __init__(self, fspath, config): +class FSHookProxy: + def __init__(self, fspath, pm, remove_mods): self.fspath = fspath - self.config = config + self.pm = pm + self.remove_mods = remove_mods def __getattr__(self, name): - plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.pluginmanager.make_hook_caller(name, plugins) + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) self.__dict__[name] = x return x - def compatproperty(name): def fget(self): # deprecated - use pytest.name @@ -362,9 +361,6 @@ def listnames(self): return [x.name for x in self.listchain()] - def getplugins(self): - return self.config._getmatchingplugins(self.fspath) - def addfinalizer(self, fin): """ register a function to be called when this node is finalized. @@ -519,12 +515,12 @@ def _makeid(self): return "" - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_collectstart(self): if self.shouldstop: raise self.Interrupted(self.shouldstop) - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_logreport(self, report): if report.failed and not hasattr(report, 'wasxfail'): self._testsfailed += 1 @@ -541,8 +537,20 @@ try: return self._fs2hookproxy[fspath] except KeyError: - self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config) - return x + # check if we have the common case of running + # hooks with all conftest.py filesall conftest.py + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules(fspath) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # one or more conftests are not in use at this fspath + proxy = FSHookProxy(fspath, pm, remove_mods) + else: + # all plugis are active for this fspath + proxy = self.config.hook + + self._fs2hookproxy[fspath] = proxy + return proxy def perform_collect(self, args=None, genitems=True): hook = self.config.hook diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/nose.py --- a/_pytest/nose.py +++ b/_pytest/nose.py @@ -24,7 +24,7 @@ call.excinfo = call2.excinfo - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_runtest_setup(item): if is_potential_nosetest(item): if isinstance(item.parent, pytest.Generator): diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pastebin.py --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -11,7 +11,7 @@ choices=['failed', 'all'], help="send failed|all info to bpaste.net pastebin service.") - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_configure(config): if config.option.pastebin == "all": tr = config.pluginmanager.getplugin('terminalreporter') diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -11,7 +11,7 @@ import py import pytest from py.builtin import print_ -from _pytest.core import HookCaller, add_method_wrapper +from _pytest.core import TracedHookExecution from _pytest.main import Session, EXIT_OK @@ -79,12 +79,12 @@ self._pluginmanager = pluginmanager self.calls = [] - def _docall(hookcaller, methods, kwargs): - self.calls.append(ParsedCall(hookcaller.name, kwargs)) - yield - self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - #if hasattr(pluginmanager, "config"): - # pluginmanager.add_shutdown(self._undo_wrapping) + def before(hook, method, kwargs): + self.calls.append(ParsedCall(hook.name, kwargs)) + def after(outcome, hook, method, kwargs): + pass + executor = TracedHookExecution(pluginmanager, before, after) + self._undo_wrapping = executor.undo def finish_recording(self): self._undo_wrapping() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -172,7 +172,7 @@ def pytest_sessionstart(session): session._fixturemanager = FixtureManager(session) - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_namespace(): raises.Exception = pytest.fail.Exception return { @@ -191,7 +191,7 @@ return request.config - at pytest.mark.trylast + at pytest.hookimpl_opts(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj if pyfuncitem._isyieldedfunction(): @@ -219,7 +219,7 @@ def pytest_pycollect_makemodule(path, parent): return Module(path, parent) - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_pycollect_makeitem(collector, name, obj): outcome = yield res = outcome.get_result() @@ -375,13 +375,16 @@ fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) metafunc = Metafunc(funcobj, fixtureinfo, self.config, cls=cls, module=module) - try: - methods = [module.pytest_generate_tests] - except AttributeError: - methods = [] + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) - self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) + if methods: + self.ihook.pytest_generate_tests.call_extra(methods, + dict(metafunc=metafunc)) + else: + self.ihook.pytest_generate_tests(metafunc=metafunc) Function = self._getcustomclass("Function") if not metafunc._calls: @@ -1621,7 +1624,6 @@ self.session = session self.config = session.config self._arg2fixturedefs = {} - self._seenplugins = set() self._holderobjseen = set() self._arg2finish = {} self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] @@ -1646,11 +1648,7 @@ node) return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs) - ### XXX this hook should be called for historic events like pytest_configure - ### so that we don't have to do the below pytest_configure hook def pytest_plugin_registered(self, plugin): - if plugin in self._seenplugins: - return nodeid = None try: p = py.path.local(plugin.__file__) @@ -1665,13 +1663,6 @@ if p.sep != "/": nodeid = nodeid.replace(p.sep, "/") self.parsefactories(plugin, nodeid) - self._seenplugins.add(plugin) - - @pytest.mark.tryfirst - def pytest_configure(self, config): - plugins = config.pluginmanager.getplugins() - for plugin in plugins: - self.pytest_plugin_registered(plugin) def _getautousenames(self, nodeid): """ return a tuple of fixture names to be used. """ diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/skipping.py --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -133,7 +133,7 @@ return expl - at pytest.mark.tryfirst + at pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_setup(item): evalskip = MarkEvaluator(item, 'skipif') if evalskip.istrue(): @@ -151,7 +151,7 @@ if not evalxfail.get('run', True): pytest.xfail("[NOTRUN] " + evalxfail.getexplanation()) - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -164,6 +164,8 @@ def pytest_logwarning(self, code, fslocation, message, nodeid): warnings = self.stats.setdefault("warnings", []) + if isinstance(fslocation, tuple): + fslocation = "%s:%d" % fslocation warning = WarningReport(code=code, fslocation=fslocation, message=message, nodeid=nodeid) warnings.append(warning) @@ -265,7 +267,7 @@ def pytest_collection_modifyitems(self): self.report_collect(True) - @pytest.mark.trylast + @pytest.hookimpl_opts(trylast=True) def pytest_sessionstart(self, session): self._sessionstarttime = time.time() if not self.showheader: @@ -350,7 +352,7 @@ indent = (len(stack) - 1) * " " self._tw.line("%s%s" % (indent, col)) - @pytest.mark.hookwrapper + @pytest.hookimpl_opts(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): outcome = yield outcome.get_result() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/unittest.py --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -140,7 +140,7 @@ if traceback: excinfo.traceback = traceback - at pytest.mark.tryfirst + at pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call): if isinstance(item, TestCaseFunction): if item._excinfo: @@ -152,7 +152,7 @@ # twisted trial support - at pytest.mark.hookwrapper + at pytest.hookimpl_opts(hookwrapper=True) def pytest_runtest_protocol(item): if isinstance(item, TestCaseFunction) and \ 'twisted.trial.unittest' in sys.modules: diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/example/markers.txt --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -201,9 +201,9 @@ @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. For an example on how to add and work with markers from a plugin, see @@ -375,9 +375,9 @@ @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. Reading markers which were set from multiple places diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/example/simple.txt --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.txt @@ -534,7 +534,7 @@ import pytest import os.path - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call, __multicall__): # execute all other hooks to obtain the report object rep = __multicall__.execute() @@ -607,7 +607,7 @@ import pytest - @pytest.mark.tryfirst + @pytest.hookimpl_opts(tryfirst=True) def pytest_runtest_makereport(item, call, __multicall__): # execute all other hooks to obtain the report object rep = __multicall__.execute() diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -56,6 +56,7 @@ - all collection, reporting, running aspects are delegated to hook functions - customizations can be per-directory, per-project or per PyPI released plugin - it is easy to add command line options or customize existing behaviour + - :ref:`easy to write your own plugins ` .. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Mon Apr 27 14:26:22 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 27 Apr 2015 12:26:22 -0000 Subject: [Pytest-commit] commit/pytest: 3 new changesets Message-ID: <20150427122622.15547.59323@app10.ash-private.bitbucket.org> 3 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/333de4cad7f0/ Changeset: 333de4cad7f0 Branch: more_plugin User: hpk42 Date: 2015-04-27 12:25:49+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/80fc5f72816d/ Changeset: 80fc5f72816d Branch: plugin_no_pytest User: hpk42 Date: 2015-04-27 12:25:53+00:00 Summary: close branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/9fa77cbe9a1e/ Changeset: 9fa77cbe9a1e Branch: cx_freeze_ubuntu User: hpk42 Date: 2015-04-27 12:26:02+00:00 Summary: close branch Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Mon Apr 27 14:35:41 2015 From: builds at drone.io (Drone.io Build) Date: Mon, 27 Apr 2015 12:35:41 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 98 Message-ID: <20150427123541.30329.89886@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/98 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3965:9fa77cbe9a1e Author : holger krekel Branch : cx_freeze_ubuntu Message: close branch -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Mon Apr 27 20:45:21 2015 From: issues-reply at bitbucket.org (Julien Duponchelle) Date: Mon, 27 Apr 2015 18:45:21 -0000 Subject: [Pytest-commit] Issue #733: xdist fail with module' object has no attribute 'TCPServer' (pytest-dev/pytest) Message-ID: <20150427184521.5884.74999@app10.ash-private.bitbucket.org> New issue 733: xdist fail with module' object has no attribute 'TCPServer' https://bitbucket.org/pytest-dev/pytest/issue/733/xdist-fail-with-module-object-has-no Julien Duponchelle: I try to run: py.test -d --tx socket=127.0.0.1:8888 --rsyncdir tests tests And i receive a very long error output: ``` #!python ================================================================ test session starts ================================================================= platform darwin -- Python 3.4.2 -- py-1.4.26 -- pytest-2.7.0 rootdir: /Users/noplay/code/gns3/gns3-server, inifile: tox.ini plugins: capturelog, cov, timeout, xdist gw0 C[gw0] node down: Traceback (most recent call last): File "/private/tmp/pyexecnetcache/_pytest/config.py", line 513, in getconftestmodules return self._path2confmods[path] KeyError: local('/private/tmp/pyexecnetcache/tests') During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/private/tmp/pyexecnetcache/_pytest/config.py", line 537, in importconftest return self._conftestpath2mod[conftestpath] KeyError: local('/private/tmp/pyexecnetcache/tests/conftest.py') During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/private/tmp/pyexecnetcache/_pytest/config.py", line 543, in importconftest mod = conftestpath.pyimport() File "/private/tmp/pyexecnetcache/py/_path/local.py", line 641, in pyimport __import__(modname) File "/private/tmp/pyexecnetcache/tests/conftest.py", line 26, in from aiohttp import web File "/Users/noplay/.virtualenvs/gns3-server/lib/python3.4/site-packages/aiohttp/__init__.py", line 7, in from .protocol import * # noqa File "/Users/noplay/.virtualenvs/gns3-server/lib/python3.4/site-packages/aiohttp/protocol.py", line 11, in import http.server File "/usr/local/Cellar/python3/3.4.2_1/Frameworks/Python.framework/Versions/3.4/lib/python3.4/http/server.py", line 127, in class HTTPServer(socketserver.TCPServer): AttributeError: 'module' object has no attribute 'TCPServer' During handling of the above exception, another exception occurred: Traceback (most recent call last): File """" base execnet gateway code send to the other side for bootstrapping. NOTE: aims to be compatible to Python 2.5-3.X, Jython and IronPython (C) 2004-2013 Holger Krekel, Armin Rigo, Benjamin Peterson, Ronny Pfannschmidt and others """ from __future__ import with_statement import sys, os, weakref import traceback, struct # NOTE that we want to avoid try/except style importing # to avoid setting sys.exc_info() during import # ISPY3 = sys.version_info >= (3, 0) if ISPY3: from io import BytesIO exec("def do_exec(co, loc): exec(co, loc)\n" "def reraise(cls, val, tb): raise val\n") unicode = str _long_type = int from _thread import interrupt_main SUBPROCESS32 = False else: from StringIO import StringIO as BytesIO exec("def do_exec(co, loc): exec co in loc\n" "def reraise(cls, val, tb): raise cls, val, tb\n") bytes = str _long_type = long try: from thread import interrupt_main except ImportError: interrupt_main = None try: import subprocess32 # NOQA SUBPROCESS32 = True except ImportError: SUBPROCESS32 = False sys.exc_clear() #f = open("/tmp/execnet-%s" % os.getpid(), "w") #def log_extra(*msg): # f.write(" ".join([str(x) for x in msg]) + "\n") class EmptySemaphore: acquire = release = lambda self: None def get_execmodel(backend): if hasattr(backend, "backend"): return backend if backend == "thread": importdef = { 'get_ident': ['thread::get_ident', '_thread::get_ident'], '_start_new_thread': ['thread::start_new_thread', '_thread::start_new_thread'], 'threading': ["threading",], 'queue': ["queue", "Queue"], 'sleep': ['time::sleep'], 'subprocess': ['subprocess32' if SUBPROCESS32 else 'subprocess'], 'socket': ['socket'], '_fdopen': ['os::fdopen'], '_lock': ['threading'], '_event': ['threading'], } def exec_start(self, func, args=()): self._start_new_thread(func, args) elif backend == "eventlet": importdef = { 'get_ident': ['eventlet.green.thread::get_ident'], '_spawn_n': ['eventlet::spawn_n'], 'threading': ['eventlet.green.threading'], 'queue': ["eventlet.queue"], 'sleep': ['eventlet::sleep'], 'subprocess': ['eventlet.green.subprocess'], 'socket': ['eventlet.green.socket'], '_fdopen': ['eventlet.green.os::fdopen'], '_lock': ['eventlet.green.threading'], '_event': ['eventlet.green.threading'], } def exec_start(self, func, args=()): self._spawn_n(func, *args) elif backend == "gevent": importdef = { 'get_ident': ['gevent.thread::get_ident'], '_spawn_n': ['gevent::spawn'], 'threading': ['threading'], 'queue': ["gevent.queue"], 'sleep': ['gevent::sleep'], 'subprocess': ['gevent.subprocess'], 'socket': ['gevent.socket'], # XXX '_fdopen': ['gevent.fileobject::FileObjectThread'], '_lock': ['gevent.lock'], '_event': ['gevent.event'], } def exec_start(self, func, args=()): self._spawn_n(func, *args) else: raise ValueError("unknown execmodel %r" %(backend,)) class ExecModel: def __init__(self, name): self._importdef = importdef self.backend = name self._count = 0 def __repr__(self): return "" % self.backend def __getattr__(self, name): locs = self._importdef.get(name) if locs is None: raise AttributeError(name) for loc in locs: parts = loc.split("::") loc = parts.pop(0) try: mod = __import__(loc, None, None, "__doc__") except ImportError: pass else: if parts: mod = getattr(mod, parts[0]) setattr(self, name, mod) return mod raise AttributeError(name) start = exec_start def fdopen(self, fd, mode, bufsize=1): return self._fdopen(fd, mode, bufsize) def WorkerPool(self, hasprimary=False): return WorkerPool(self, hasprimary=hasprimary) def Semaphore(self, size=None): if size is None: return EmptySemaphore() return self._lock.Semaphore(size) def Lock(self): return self._lock.RLock() def RLock(self): return self._lock.RLock() def Event(self): event = self._event.Event() if sys.version_info < (2,7): # patch wait function to return event state instead of None real_wait = event.wait def wait(timeout=None): real_wait(timeout=timeout) return event.isSet() event.wait = wait return event def PopenPiped(self, args): PIPE = self.subprocess.PIPE return self.subprocess.Popen(args, stdout=PIPE, stdin=PIPE) return ExecModel(backend) class Reply(object): """ reply instances provide access to the result of a function execution that got dispatched through WorkerPool.spawn() """ def __init__(self, task, threadmodel): self.task = task self._result_ready = threadmodel.Event() self.running = True def get(self, timeout=None): """ get the result object from an asynchronous function execution. if the function execution raised an exception, then calling get() will reraise that exception including its traceback. """ self.waitfinish(timeout) try: return self._result except AttributeError: reraise(*(self._excinfo[:3])) # noqa def waitfinish(self, timeout=None): if not self._result_ready.wait(timeout): raise IOError("timeout waiting for %r" %(self.task, )) def run(self): func, args, kwargs = self.task try: try: self._result = func(*args, **kwargs) except: # sys may be already None when shutting down the interpreter if sys is not None: self._excinfo = sys.exc_info() finally: self._result_ready.set() self.running = False class WorkerPool(object): """ A WorkerPool allows to spawn function executions to threads, returning a reply object on which you can ask for the result (and get exceptions reraised). This implementation allows the main thread to integrate itself into performing function execution through calling integrate_as_primary_thread() which will return when the pool received a trigger_shutdown(). """ def __init__(self, execmodel, hasprimary=False): """ by default allow unlimited number of spawns. """ self.execmodel = execmodel self._running_lock = self.execmodel.Lock() self._running = set() self._shuttingdown = False self._waitall_events = [] if hasprimary: if self.execmodel.backend != "thread": raise ValueError("hasprimary=True requires thread model") self._primary_thread_task_ready = self.execmodel.Event() else: self._primary_thread_task_ready = None def integrate_as_primary_thread(self): """ integrate the thread with which we are called as a primary thread for executing functions triggered with spawn(). """ assert self.execmodel.backend == "thread", self.execmodel primary_thread_task_ready = self._primary_thread_task_ready # interacts with code at REF1 while 1: primary_thread_task_ready.wait() reply = self._primary_thread_task if reply is None: # trigger_shutdown() woke us up break self._perform_spawn(reply) # we are concurrent with trigger_shutdown and spawn with self._running_lock: if self._shuttingdown: break primary_thread_task_ready.clear() def trigger_shutdown(self): with self._running_lock: self._shuttingdown = True if self._primary_thread_task_ready is not None: self._primary_thread_task = None self._primary_thread_task_ready.set() def active_count(self): return len(self._running) def _perform_spawn(self, reply): reply.run() with self._running_lock: self._running.remove(reply) if not self._running: while self._waitall_events: waitall_event = self._waitall_events.pop() waitall_event.set() def _try_send_to_primary_thread(self, reply): # REF1 in 'thread' model we give priority to running in main thread # note that we should be called with _running_lock hold primary_thread_task_ready = self._primary_thread_task_ready if primary_thread_task_ready is not None: if not primary_thread_task_ready.isSet(): self._primary_thread_task = reply # wake up primary thread primary_thread_task_ready.set() return True return False def spawn(self, func, *args, **kwargs): """ return Reply object for the asynchronous dispatch of the given func(*args, **kwargs). """ reply = Reply((func, args, kwargs), self.execmodel) with self._running_lock: if self._shuttingdown: raise ValueError("pool is shutting down") self._running.add(reply) if not self._try_send_to_primary_thread(reply): self.execmodel.start(self._perform_spawn, (reply,)) return reply def terminate(self, timeout=None): """ trigger shutdown and wait for completion of all executions. """ self.trigger_shutdown() return self.waitall(timeout=timeout) def waitall(self, timeout=None): """ wait until all active spawns have finished executing. """ with self._running_lock: if not self._running: return True # if a Reply still runs, we let run_and_release # signal us -- note that we are still holding the # _running_lock to avoid race conditions my_waitall_event = self.execmodel.Event() self._waitall_events.append(my_waitall_event) return my_waitall_event.wait(timeout=timeout) sysex = (KeyboardInterrupt, SystemExit) DEBUG = os.environ.get('EXECNET_DEBUG') pid = os.getpid() if DEBUG == '2': def trace(*msg): try: line = " ".join(map(str, msg)) sys.stderr.write("[%s] %s\n" % (pid, line)) sys.stderr.flush() except Exception: pass # nothing we can do, likely interpreter-shutdown elif DEBUG: import tempfile, os.path fn = os.path.join(tempfile.gettempdir(), 'execnet-debug-%d' % pid) #sys.stderr.write("execnet-debug at %r" %(fn,)) debugfile = open(fn, 'w') def trace(*msg): try: line = " ".join(map(str, msg)) debugfile.write(line + "\n") debugfile.flush() except Exception: try: v = sys.exc_info()[1] sys.stderr.write( "[%s] exception during tracing: %r\n" % (pid, v)) except Exception: pass # nothing we can do, likely interpreter-shutdown else: notrace = trace = lambda *msg: None class Popen2IO: error = (IOError, OSError, EOFError) def __init__(self, outfile, infile, execmodel): # we need raw byte streams self.outfile, self.infile = outfile, infile if sys.platform == "win32": import msvcrt try: msvcrt.setmode(infile.fileno(), os.O_BINARY) msvcrt.setmode(outfile.fileno(), os.O_BINARY) except (AttributeError, IOError): pass self._read = getattr(infile, "buffer", infile).read self._write = getattr(outfile, "buffer", outfile).write self.execmodel = execmodel def read(self, numbytes): """Read exactly 'numbytes' bytes from the pipe. """ # a file in non-blocking mode may return less bytes, so we loop buf = bytes() while numbytes > len(buf): data = self._read(numbytes-len(buf)) if not data: raise EOFError("expected %d bytes, got %d" %(numbytes, len(buf))) buf += data return buf def write(self, data): """write out all data bytes. """ assert isinstance(data, bytes) self._write(data) self.outfile.flush() def close_read(self): self.infile.close() def close_write(self): self.outfile.close() class Message: """ encapsulates Messages and their wire protocol. """ _types = [] def __init__(self, msgcode, channelid=0, data=''): self.msgcode = msgcode self.channelid = channelid self.data = data @staticmethod def from_io(io): try: header = io.read(9) # type 1, channel 4, payload 4 if not header: raise EOFError("empty read") except EOFError: e = sys.exc_info()[1] raise EOFError('couldnt load message header, ' + e.args[0]) msgtype, channel, payload = struct.unpack('!bii', header) return Message(msgtype, channel, io.read(payload)) def to_io(self, io): header = struct.pack('!bii', self.msgcode, self.channelid, len(self.data)) io.write(header+self.data) def received(self, gateway): self._types[self.msgcode](self, gateway) def __repr__(self): name = self._types[self.msgcode].__name__.upper() return "" %( name, self.channelid, len(self.data)) class GatewayReceivedTerminate(Exception): """ Receiverthread got termination message. """ def _setupmessages(): def status(message, gateway): # we use the channelid to send back information # but don't instantiate a channel object d = {'numchannels': len(gateway._channelfactory._channels), 'numexecuting': gateway._execpool.active_count(), 'execmodel': gateway.execmodel.backend, } gateway._send(Message.CHANNEL_DATA, message.channelid, dumps_internal(d)) gateway._send(Message.CHANNEL_CLOSE, message.channelid) def channel_exec(message, gateway): channel = gateway._channelfactory.new(message.channelid) gateway._local_schedulexec(channel=channel,sourcetask=message.data) def channel_data(message, gateway): gateway._channelfactory._local_receive(message.channelid, message.data) def channel_close(message, gateway): gateway._channelfactory._local_close(message.channelid) def channel_close_error(message, gateway): remote_error = RemoteError(loads_internal(message.data)) gateway._channelfactory._local_close(message.channelid, remote_error) def channel_last_message(message, gateway): gateway._channelfactory._local_close(message.channelid, sendonly=True) def gateway_terminate(message, gateway): raise GatewayReceivedTerminate(gateway) def reconfigure(message, gateway): if message.channelid == 0: target = gateway else: target = gateway._channelfactory.new(message.channelid) target._strconfig = loads_internal(message.data, gateway) types = [ status, reconfigure, gateway_terminate, channel_exec, channel_data, channel_close, channel_close_error, channel_last_message, ] for i, handler in enumerate(types): Message._types.append(handler) setattr(Message, handler.__name__.upper(), i) _setupmessages() def geterrortext(excinfo, format_exception=traceback.format_exception, sysex=sysex): try: l = format_exception(*excinfo) errortext = "".join(l) except sysex: raise except: errortext = '%s: %s' % (excinfo[0].__name__, excinfo[1]) return errortext class RemoteError(Exception): """ Exception containing a stringified error from the other side. """ def __init__(self, formatted): self.formatted = formatted Exception.__init__(self) def __str__(self): return self.formatted def __repr__(self): return "%s: %s" %(self.__class__.__name__, self.formatted) def warn(self): if self.formatted != INTERRUPT_TEXT: # XXX do this better sys.stderr.write("[%s] Warning: unhandled %r\n" % (os.getpid(), self,)) class TimeoutError(IOError): """ Exception indicating that a timeout was reached. """ NO_ENDMARKER_WANTED = object() class Channel(object): """Communication channel between two Python Interpreter execution points.""" RemoteError = RemoteError TimeoutError = TimeoutError _INTERNALWAKEUP = 1000 _executing = False def __init__(self, gateway, id): assert isinstance(id, int) self.gateway = gateway #XXX: defaults copied from Unserializer self._strconfig = getattr(gateway, '_strconfig', (True, False)) self.id = id self._items = self.gateway.execmodel.queue.Queue() self._closed = False self._receiveclosed = self.gateway.execmodel.Event() self._remoteerrors = [] def _trace(self, *msg): self.gateway._trace(self.id, *msg) def setcallback(self, callback, endmarker=NO_ENDMARKER_WANTED): """ set a callback function for receiving items. All already queued items will immediately trigger the callback. Afterwards the callback will execute in the receiver thread for each received data item and calls to ``receive()`` will raise an error. If an endmarker is specified the callback will eventually be called with the endmarker when the channel closes. """ _callbacks = self.gateway._channelfactory._callbacks with self.gateway._receivelock: if self._items is None: raise IOError("%r has callback already registered" %(self,)) items = self._items self._items = None while 1: try: olditem = items.get(block=False) except self.gateway.execmodel.queue.Empty: if not (self._closed or self._receiveclosed.isSet()): _callbacks[self.id] = ( callback, endmarker, self._strconfig, ) break else: if olditem is ENDMARKER: items.put(olditem) # for other receivers if endmarker is not NO_ENDMARKER_WANTED: callback(endmarker) break else: callback(olditem) def __repr__(self): flag = self.isclosed() and "closed" or "open" return "" % (self.id, flag) def __del__(self): if self.gateway is None: # can be None in tests return self._trace("channel.__del__") # no multithreading issues here, because we have the last ref to 'self' if self._closed: # state transition "closed" --> "deleted" for error in self._remoteerrors: error.warn() elif self._receiveclosed.isSet(): # state transition "sendonly" --> "deleted" # the remote channel is already in "deleted" state, nothing to do pass else: # state transition "opened" --> "deleted" # check if we are in the middle of interpreter shutdown # in which case the process will go away and we probably # don't need to try to send a closing or last message # (and often it won't work anymore to send things out) if Message is not None: if self._items is None: # has_callback msgcode = Message.CHANNEL_LAST_MESSAGE else: msgcode = Message.CHANNEL_CLOSE try: self.gateway._send(msgcode, self.id) except (IOError, ValueError): # ignore problems with sending pass def _getremoteerror(self): try: return self._remoteerrors.pop(0) except IndexError: try: return self.gateway._error except AttributeError: pass return None # # public API for channel objects # def isclosed(self): """ return True if the channel is closed. A closed channel may still hold items. """ return self._closed def makefile(self, mode='w', proxyclose=False): """ return a file-like object. mode can be 'w' or 'r' for writeable/readable files. if proxyclose is true file.close() will also close the channel. """ if mode == "w": return ChannelFileWrite(channel=self, proxyclose=proxyclose) elif mode == "r": return ChannelFileRead(channel=self, proxyclose=proxyclose) raise ValueError("mode %r not availabe" %(mode,)) def close(self, error=None): """ close down this channel with an optional error message. Note that closing of a channel tied to remote_exec happens automatically at the end of execution and cannot be done explicitely. """ if self._executing: raise IOError("cannot explicitly close channel within remote_exec") if self._closed: self.gateway._trace(self, "ignoring redundant call to close()") if not self._closed: # state transition "opened/sendonly" --> "closed" # threads warning: the channel might be closed under our feet, # but it's never damaging to send too many CHANNEL_CLOSE messages # however, if the other side triggered a close already, we # do not send back a closed message. if not self._receiveclosed.isSet(): put = self.gateway._send if error is not None: put(Message.CHANNEL_CLOSE_ERROR, self.id, dumps_internal(error)) else: put(Message.CHANNEL_CLOSE, self.id) self._trace("sent channel close message") if isinstance(error, RemoteError): self._remoteerrors.append(error) self._closed = True # --> "closed" self._receiveclosed.set() queue = self._items if queue is not None: queue.put(ENDMARKER) self.gateway._channelfactory._no_longer_opened(self.id) def waitclose(self, timeout=None): """ wait until this channel is closed (or the remote side otherwise signalled that no more data was being sent). The channel may still hold receiveable items, but not receive any more after waitclose() has returned. Exceptions from executing code on the other side are reraised as local channel.RemoteErrors. EOFError is raised if the reading-connection was prematurely closed, which often indicates a dying process. self.TimeoutError is raised after the specified number of seconds (default is None, i.e. wait indefinitely). """ self._receiveclosed.wait(timeout=timeout) # wait for non-"opened" state if not self._receiveclosed.isSet(): raise self.TimeoutError("Timeout after %r seconds" % timeout) error = self._getremoteerror() if error: raise error def send(self, item): """sends the given item to the other side of the channel, possibly blocking if the sender queue is full. The item must be a simple python type and will be copied to the other side by value. IOError is raised if the write pipe was prematurely closed. """ if self.isclosed(): raise IOError("cannot send to %r" %(self,)) self.gateway._send(Message.CHANNEL_DATA, self.id, dumps_internal(item)) def receive(self, timeout=None): """receive a data item that was sent from the other side. timeout: None [default] blocked waiting. A positive number indicates the number of seconds after which a channel.TimeoutError exception will be raised if no item was received. Note that exceptions from the remotely executing code will be reraised as channel.RemoteError exceptions containing a textual representation of the remote traceback. """ itemqueue = self._items if itemqueue is None: raise IOError("cannot receive(), channel has receiver callback") try: x = itemqueue.get(timeout=timeout) except self.gateway.execmodel.queue.Empty: raise self.TimeoutError("no item after %r seconds" %(timeout)) if x is ENDMARKER: itemqueue.put(x) # for other receivers raise self._getremoteerror() or EOFError() else: return x def __iter__(self): return self def next(self): try: return self.receive() except EOFError: raise StopIteration __next__ = next def reconfigure(self, py2str_as_py3str=True, py3str_as_py2str=False): """ set the string coercion for this channel the default is to try to convert py2 str as py3 str, but not to try and convert py3 str to py2 str """ self._strconfig = (py2str_as_py3str, py3str_as_py2str) data = dumps_internal(self._strconfig) self.gateway._send(Message.RECONFIGURE, self.id, data=data) ENDMARKER = object() INTERRUPT_TEXT = "keyboard-interrupted" class ChannelFactory(object): def __init__(self, gateway, startcount=1): self._channels = weakref.WeakValueDictionary() self._callbacks = {} self._writelock = gateway.execmodel.Lock() self.gateway = gateway self.count = startcount self.finished = False self._list = list # needed during interp-shutdown def new(self, id=None): """ create a new Channel with 'id' (or create new id if None). """ with self._writelock: if self.finished: raise IOError("connexion already closed: %s" % (self.gateway,)) if id is None: id = self.count self.count += 2 try: channel = self._channels[id] except KeyError: channel = self._channels[id] = Channel(self.gateway, id) return channel def channels(self): return self._list(self._channels.values()) # # internal methods, called from the receiver thread # def _no_longer_opened(self, id): try: del self._channels[id] except KeyError: pass try: callback, endmarker, strconfig = self._callbacks.pop(id) except KeyError: pass else: if endmarker is not NO_ENDMARKER_WANTED: callback(endmarker) def _local_close(self, id, remoteerror=None, sendonly=False): channel = self._channels.get(id) if channel is None: # channel already in "deleted" state if remoteerror: remoteerror.warn() self._no_longer_opened(id) else: # state transition to "closed" state if remoteerror: channel._remoteerrors.append(remoteerror) queue = channel._items if queue is not None: queue.put(ENDMARKER) self._no_longer_opened(id) if not sendonly: # otherwise #--> "sendonly" channel._closed = True # --> "closed" channel._receiveclosed.set() def _local_receive(self, id, data): # executes in receiver thread channel = self._channels.get(id) try: callback, endmarker, strconfig = self._callbacks[id] except KeyError: queue = channel and channel._items if queue is None: pass # drop data else: item = loads_internal(data, channel) queue.put(item) else: try: data = loads_internal(data, channel, strconfig) callback(data) # even if channel may be already closed except Exception: excinfo = sys.exc_info() self.gateway._trace("exception during callback: %s" % excinfo[1]) errortext = self.gateway._geterrortext(excinfo) self.gateway._send(Message.CHANNEL_CLOSE_ERROR, id, dumps_internal(errortext)) self._local_close(id, errortext) def _finished_receiving(self): with self._writelock: self.finished = True for id in self._list(self._channels): self._local_close(id, sendonly=True) for id in self._list(self._callbacks): self._no_longer_opened(id) class ChannelFile(object): def __init__(self, channel, proxyclose=True): self.channel = channel self._proxyclose = proxyclose def isatty(self): return False def close(self): if self._proxyclose: self.channel.close() def __repr__(self): state = self.channel.isclosed() and 'closed' or 'open' return '' %(self.channel.id, state) class ChannelFileWrite(ChannelFile): def write(self, out): self.channel.send(out) def flush(self): pass class ChannelFileRead(ChannelFile): def __init__(self, channel, proxyclose=True): super(ChannelFileRead, self).__init__(channel, proxyclose) self._buffer = None def read(self, n): try: if self._buffer is None: self._buffer = self.channel.receive() while len(self._buffer) < n: self._buffer += self.channel.receive() except EOFError: self.close() if self._buffer is None: ret = "" else: ret = self._buffer[:n] self._buffer = self._buffer[n:] return ret def readline(self): if self._buffer is not None: i = self._buffer.find("\n") if i != -1: return self.read(i+1) line = self.read(len(self._buffer)+1) else: line = self.read(1) while line and line[-1] != "\n": c = self.read(1) if not c: break line += c return line class BaseGateway(object): exc_info = sys.exc_info _sysex = sysex id = "" def __init__(self, io, id, _startcount=2): self.execmodel = io.execmodel self._io = io self.id = id self._strconfig = (Unserializer.py2str_as_py3str, Unserializer.py3str_as_py2str) self._channelfactory = ChannelFactory(self, _startcount) self._receivelock = self.execmodel.RLock() # globals may be NONE at process-termination self.__trace = trace self._geterrortext = geterrortext self._receivepool = self.execmodel.WorkerPool() def _trace(self, *msg): self.__trace(self.id, *msg) def _initreceive(self): self._receivepool.spawn(self._thread_receiver) def _thread_receiver(self): def log(*msg): self._trace("[receiver-thread]", *msg) log("RECEIVERTHREAD: starting to run") io = self._io try: while 1: msg = Message.from_io(io) log("received", msg) with self._receivelock: msg.received(self) del msg except (KeyboardInterrupt, GatewayReceivedTerminate): pass except EOFError: log("EOF without prior gateway termination message") self._error = self.exc_info()[1] except Exception: log(self._geterrortext(self.exc_info())) log('finishing receiving thread') # wake up and terminate any execution waiting to receive self._channelfactory._finished_receiving() log('terminating execution') self._terminate_execution() log('closing read') self._io.close_read() log('closing write') self._io.close_write() log('terminating our receive pseudo pool') self._receivepool.trigger_shutdown() def _terminate_execution(self): pass def _send(self, msgcode, channelid=0, data=bytes()): message = Message(msgcode, channelid, data) try: message.to_io(self._io) self._trace('sent', message) except (IOError, ValueError): e = sys.exc_info()[1] self._trace('failed to send', message, e) # ValueError might be because the IO is already closed raise IOError("cannot send (already closed?)") def _local_schedulexec(self, channel, sourcetask): channel.close("execution disallowed") # _____________________________________________________________________ # # High Level Interface # _____________________________________________________________________ # def newchannel(self): """ return a new independent channel. """ return self._channelfactory.new() def join(self, timeout=None): """ Wait for receiverthread to terminate. """ self._trace("waiting for receiver thread to finish") self._receivepool.waitall() class SlaveGateway(BaseGateway): def _local_schedulexec(self, channel, sourcetask): sourcetask = loads_internal(sourcetask) self._execpool.spawn(self.executetask, ((channel, sourcetask))) def _terminate_execution(self): # called from receiverthread self._trace("shutting down execution pool") self._execpool.trigger_shutdown() if not self._execpool.waitall(5.0): self._trace("execution ongoing after 5 secs, trying interrupt_main") # We try hard to terminate execution based on the assumption # that there is only one gateway object running per-process. if sys.platform != "win32": self._trace("sending ourselves a SIGINT") os.kill(os.getpid(), 2) # send ourselves a SIGINT elif interrupt_main is not None: self._trace("calling interrupt_main()") interrupt_main() if not self._execpool.waitall(10.0): self._trace("execution did not finish in another 10 secs, " "calling os._exit()") os._exit(1) def serve(self): trace = lambda msg: self._trace("[serve] " + msg) hasprimary = self.execmodel.backend == "thread" self._execpool = self.execmodel.WorkerPool(hasprimary=hasprimary) trace("spawning receiver thread") self._initreceive() try: if hasprimary: # this will return when we are in shutdown trace("integrating as primary thread") self._execpool.integrate_as_primary_thread() trace("joining receiver thread") self.join() except KeyboardInterrupt: # in the slave we can't really do anything sensible trace("swallowing keyboardinterrupt, serve finished") def executetask(self, item): try: channel, (source, call_name, kwargs) = item if not ISPY3 and kwargs: # some python2 versions do not accept unicode keyword params # note: Unserializer generally turns py2-str to py3-str objects newkwargs = {} for name, value in kwargs.items(): if isinstance(name, unicode): name = name.encode('ascii') newkwargs[name] = value kwargs = newkwargs loc = {'channel' : channel, '__name__': '__channelexec__'} self._trace("execution starts[%s]: %s" % (channel.id, repr(source)[:50])) channel._executing = True try: co = compile(source+'\n', '', 'exec') do_exec(co, loc) # noqa if call_name: self._trace('calling %s(**%60r)' % (call_name, kwargs)) function = loc[call_name] function(channel, **kwargs) finally: channel._executing = False self._trace("execution finished") except KeyboardInterrupt: channel.close(INTERRUPT_TEXT) raise except: excinfo = self.exc_info() if not isinstance(excinfo[1], EOFError): if not channel.gateway._channelfactory.finished: self._trace("got exception: %r" % (excinfo[1],)) errortext = self._geterrortext(excinfo) channel.close(errortext) return self._trace("ignoring EOFError because receiving finished") channel.close() # # Cross-Python pickling code, tested from test_serializer.py # class DataFormatError(Exception): pass class DumpError(DataFormatError): """Error while serializing an object.""" class LoadError(DataFormatError): """Error while unserializing an object.""" if ISPY3: def bchr(n): return bytes([n]) else: bchr = chr DUMPFORMAT_VERSION = bchr(1) FOUR_BYTE_INT_MAX = 2147483647 FLOAT_FORMAT = "!d" FLOAT_FORMAT_SIZE = struct.calcsize(FLOAT_FORMAT) class _Stop(Exception): pass class Unserializer(object): num2func = {} # is filled after this class definition py2str_as_py3str = True # True py3str_as_py2str = False # false means py2 will get unicode def __init__(self, stream, channel_or_gateway=None, strconfig=None): gateway = getattr(channel_or_gateway, 'gateway', channel_or_gateway) strconfig = getattr(channel_or_gateway, '_strconfig', strconfig) if strconfig: self.py2str_as_py3str, self.py3str_as_py2str = strconfig self.stream = stream self.channelfactory = getattr(gateway, '_channelfactory', gateway) def load(self, versioned=False): if versioned: ver = self.stream.read(1) if ver != DUMPFORMAT_VERSION: raise LoadError("wrong dumpformat version %r" % ver) self.stack = [] try: while True: opcode = self.stream.read(1) if not opcode: raise EOFError try: loader = self.num2func[opcode] except KeyError: raise LoadError("unkown opcode %r - " "wire protocol corruption?" % (opcode,)) loader(self) except _Stop: if len(self.stack) != 1: raise LoadError("internal unserialization error") return self.stack.pop(0) else: raise LoadError("didn't get STOP") def load_none(self): self.stack.append(None) def load_true(self): self.stack.append(True) def load_false(self): self.stack.append(False) def load_int(self): i = self._read_int4() self.stack.append(i) def load_longint(self): s = self._read_byte_string() self.stack.append(int(s)) if ISPY3: load_long = load_int load_longlong = load_longint else: def load_long(self): i = self._read_int4() self.stack.append(long(i)) def load_longlong(self): l = self._read_byte_string() self.stack.append(long(l)) def load_float(self): binary = self.stream.read(FLOAT_FORMAT_SIZE) self.stack.append(struct.unpack(FLOAT_FORMAT, binary)[0]) def _read_int4(self): return struct.unpack("!i", self.stream.read(4))[0] def _read_byte_string(self): length = self._read_int4() as_bytes = self.stream.read(length) return as_bytes def load_py3string(self): as_bytes = self._read_byte_string() if not ISPY3 and self.py3str_as_py2str: # XXX Should we try to decode into latin-1? self.stack.append(as_bytes) else: self.stack.append(as_bytes.decode("utf-8")) def load_py2string(self): as_bytes = self._read_byte_string() if ISPY3 and self.py2str_as_py3str: s = as_bytes.decode("latin-1") else: s = as_bytes self.stack.append(s) def load_bytes(self): s = self._read_byte_string() self.stack.append(s) def load_unicode(self): self.stack.append(self._read_byte_string().decode("utf-8")) def load_newlist(self): length = self._read_int4() self.stack.append([None] * length) def load_setitem(self): if len(self.stack) < 3: raise LoadError("not enough items for setitem") value = self.stack.pop() key = self.stack.pop() self.stack[-1][key] = value def load_newdict(self): self.stack.append({}) def _load_collection(self, type_): length = self._read_int4() if length: res = type_(self.stack[-length:]) del self.stack[-length:] self.stack.append(res) else: self.stack.append(type_()) def load_buildtuple(self): self._load_collection(tuple) def load_set(self): self._load_collection(set) def load_frozenset(self): self._load_collection(frozenset) def load_stop(self): raise _Stop def load_channel(self): id = self._read_int4() newchannel = self.channelfactory.new(id) self.stack.append(newchannel) # automatically build opcodes and byte-encoding class opcode: """ container for name -> num mappings. """ def _buildopcodes(): l = [] for name, func in Unserializer.__dict__.items(): if name.startswith("load_"): opname = name[5:].upper() l.append((opname, func)) l.sort() for i,(opname, func) in enumerate(l): assert i < 26, "xxx" i = bchr(64+i) Unserializer.num2func[i] = func setattr(opcode, opname, i) _buildopcodes() def dumps(obj): """ return a serialized bytestring of the given obj. The obj and all contained objects must be of a builtin python type (so nested dicts, sets, etc. are all ok but not user-level instances). """ return _Serializer().save(obj, versioned=True) def dump(byteio, obj): """ write a serialized bytestring of the given obj to the given stream. """ _Serializer(write=byteio.write).save(obj, versioned=True) def loads(bytestring, py2str_as_py3str=False, py3str_as_py2str=False): """ return the object as deserialized from the given bytestring. py2str_as_py3str: if true then string (str) objects previously dumped on Python2 will be loaded as Python3 strings which really are text objects. py3str_as_py2str: if true then string (str) objects previously dumped on Python3 will be loaded as Python2 strings instead of unicode objects. if the bytestring was dumped with an incompatible protocol version or if the bytestring is corrupted, the ``execnet.DataFormatError`` will be raised. """ io = BytesIO(bytestring) return load(io, py2str_as_py3str=py2str_as_py3str, py3str_as_py2str=py3str_as_py2str) def load(io, py2str_as_py3str=False, py3str_as_py2str=False): """ derserialize an object form the specified stream. Behaviour and parameters are otherwise the same as with ``loads`` """ strconfig=(py2str_as_py3str, py3str_as_py2str) return Unserializer(io, strconfig=strconfig).load(versioned=True) def loads_internal(bytestring, channelfactory=None, strconfig=None): io = BytesIO(bytestring) return Unserializer(io, channelfactory, strconfig).load() def dumps_internal(obj): return _Serializer().save(obj) class _Serializer(object): _dispatch = {} def __init__(self, write=None): if write is None: self._streamlist = [] write = self._streamlist.append self._write = write def save(self, obj, versioned=False): # calling here is not re-entrant but multiple instances # may write to the same stream because of the common platform # atomic-write guaruantee (concurrent writes each happen atomicly) if versioned: self._write(DUMPFORMAT_VERSION) self._save(obj) self._write(opcode.STOP) try: streamlist = self._streamlist except AttributeError: return None return type(streamlist[0])().join(streamlist) def _save(self, obj): tp = type(obj) try: dispatch = self._dispatch[tp] except KeyError: methodname = 'save_' + tp.__name__ meth = getattr(self.__class__, methodname, None) if meth is None: raise DumpError("can't serialize %s" % (tp,)) dispatch = self._dispatch[tp] = meth dispatch(self, obj) def save_NoneType(self, non): self._write(opcode.NONE) def save_bool(self, boolean): if boolean: self._write(opcode.TRUE) else: self._write(opcode.FALSE) def save_bytes(self, bytes_): self._write(opcode.BYTES) self._write_byte_sequence(bytes_) if ISPY3: def save_str(self, s): self._write(opcode.PY3STRING) self._write_unicode_string(s) else: def save_str(self, s): self._write(opcode.PY2STRING) self._write_byte_sequence(s) def save_unicode(self, s): self._write(opcode.UNICODE) self._write_unicode_string(s) def _write_unicode_string(self, s): try: as_bytes = s.encode("utf-8") except UnicodeEncodeError: raise DumpError("strings must be utf-8 encodable") self._write_byte_sequence(as_bytes) def _write_byte_sequence(self, bytes_): self._write_int4(len(bytes_), "string is too long") self._write(bytes_) def _save_integral(self, i, short_op, long_op): if i <= FOUR_BYTE_INT_MAX: self._write(short_op) self._write_int4(i) else: self._write(long_op) self._write_byte_sequence(str(i).rstrip("L").encode("ascii")) def save_int(self, i): self._save_integral(i, opcode.INT, opcode.LONGINT) def save_long(self, l): self._save_integral(l, opcode.LONG, opcode.LONGLONG) def save_float(self, flt): self._write(opcode.FLOAT) self._write(struct.pack(FLOAT_FORMAT, flt)) def _write_int4(self, i, error="int must be less than %i" % (FOUR_BYTE_INT_MAX,)): if i > FOUR_BYTE_INT_MAX: raise DumpError(error) self._write(struct.pack("!i", i)) def save_list(self, L): self._write(opcode.NEWLIST) self._write_int4(len(L), "list is too long") for i, item in enumerate(L): self._write_setitem(i, item) def _write_setitem(self, key, value): self._save(key) self._save(value) self._write(opcode.SETITEM) def save_dict(self, d): self._write(opcode.NEWDICT) for key, value in d.items(): self._write_setitem(key, value) def save_tuple(self, tup): for item in tup: self._save(item) self._write(opcode.BUILDTUPLE) self._write_int4(len(tup), "tuple is too long") def _write_set(self, s, op): for item in s: self._save(item) self._write(op) self._write_int4(len(s), "set is too long") def save_set(self, s): self._write_set(s, opcode.SET) def save_frozenset(self, s): self._write_set(s, opcode.FROZENSET) def save_Channel(self, channel): self._write(opcode.CHANNEL) self._write_int4(channel.id) def init_popen_io(execmodel): if not hasattr(os, 'dup'): # jython io = Popen2IO(sys.stdout, sys.stdin, execmodel) import tempfile sys.stdin = tempfile.TemporaryFile('r') sys.stdout = tempfile.TemporaryFile('w') else: try: devnull = os.devnull except AttributeError: if os.name == 'nt': devnull = 'NUL' else: devnull = '/dev/null' # stdin stdin = execmodel.fdopen(os.dup(0), 'r', 1) fd = os.open(devnull, os.O_RDONLY) os.dup2(fd, 0) os.close(fd) # stdout stdout = execmodel.fdopen(os.dup(1), 'w', 1) fd = os.open(devnull, os.O_WRONLY) os.dup2(fd, 1) # stderr for win32 if os.name == 'nt': sys.stderr = execmodel.fdopen(os.dup(2), 'w', 1) os.dup2(fd, 2) os.close(fd) io = Popen2IO(stdout, stdin, execmodel) sys.stdin = execmodel.fdopen(0, 'r', 1) sys.stdout = execmodel.fdopen(1, 'w', 1) return io def serve(io, id): trace("creating slavegateway on %r" %(io,)) SlaveGateway(io=io, id=id, _startcount=2).serve() import socket class SocketIO: def __init__(self, sock, execmodel): self.sock = sock self.execmodel = execmodel socket = execmodel.socket try: sock.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)# IPTOS_LOWDELAY sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) except (AttributeError, socket.error): sys.stderr.write("WARNING: cannot set socketoption") def read(self, numbytes): "Read exactly 'bytes' bytes from the socket." buf = bytes() while len(buf) < numbytes: t = self.sock.recv(numbytes - len(buf)) if not t: raise EOFError buf += t return buf def write(self, data): self.sock.sendall(data) def close_read(self): try: self.sock.shutdown(0) except self.execmodel.socket.error: pass def close_write(self): try: self.sock.shutdown(1) except self.execmodel.socket.error: pass def wait(self): pass def kill(self): pass try: execmodel except NameError: execmodel = get_execmodel('thread') io = SocketIO(clientsock, execmodel) io.write('1'.encode('ascii')) serve(io, id='socket=127.0.0.1:8888-slave')", line 1039, in executetask File "", line 1, in do_exec File "", line 143, in File "", line 120, in remote_initconfig File "/private/tmp/pyexecnetcache/_pytest/config.py", line 672, in fromdictargs config._preparse(args, addopts=False) File "/private/tmp/pyexecnetcache/_pytest/config.py", line 718, in _preparse args=args, parser=self._parser) File "/private/tmp/pyexecnetcache/_pytest/core.py", line 521, in __call__ return self._docall(self.methods, kwargs) File "/private/tmp/pyexecnetcache/_pytest/core.py", line 528, in _docall firstresult=self.firstresult).execute() File "/private/tmp/pyexecnetcache/_pytest/core.py", line 393, in execute return wrapped_call(method(*args), self.execute) File "/private/tmp/pyexecnetcache/_pytest/core.py", line 113, in wrapped_call return call_outcome.get_result() File "/private/tmp/pyexecnetcache/_pytest/core.py", line 137, in get_result raise ex[1].with_traceback(ex[2]) File "/private/tmp/pyexecnetcache/_pytest/core.py", line 123, in __init__ self.result = func() File "/private/tmp/pyexecnetcache/_pytest/core.py", line 394, in execute res = method(*args) File "/private/tmp/pyexecnetcache/_pytest/config.py", line 695, in pytest_load_initial_conftests self._conftest.setinitial(early_config.known_args_namespace) File "/private/tmp/pyexecnetcache/_pytest/config.py", line 498, in setinitial self._try_load_conftest(anchor) File "/private/tmp/pyexecnetcache/_pytest/config.py", line 504, in _try_load_conftest self.getconftestmodules(anchor) File "/private/tmp/pyexecnetcache/_pytest/config.py", line 521, in getconftestmodules mod = self.importconftest(conftestpath) File "/private/tmp/pyexecnetcache/_pytest/config.py", line 545, in importconftest raise ConftestImportFailure(conftestpath, sys.exc_info()) _pytest.config.ConftestImportFailure: (local('/private/tmp/pyexecnetcache/tests/conftest.py'), (, AttributeError("'module' object has no attribute 'TCPServer'",), )) Replacing failed node gw0 ``` Let me know if I can give you more informations From issues-reply at bitbucket.org Tue Apr 28 09:29:37 2015 From: issues-reply at bitbucket.org (Daniel Hahler) Date: Tue, 28 Apr 2015 07:29:37 -0000 Subject: [Pytest-commit] Issue #240: Empty "{posargs:foo}" gets replaced by "." (hpk42/tox) Message-ID: <20150428072937.22340.61542@app08.ash-private.bitbucket.org> New issue 240: Empty "{posargs:foo}" gets replaced by "." https://bitbucket.org/hpk42/tox/issue/240/empty-posargs-foo-gets-replaced-by Daniel Hahler: Given the following tox.ini: ``` #!ini [tox] skipsdist = true [testenv] commands = echo {posargs:foo} whitelist_externals = echo ``` Just using `tox` outputs `foo`, but when trying to pass no positional arguments `.` (a single dot) get passed: ``` % tox -- "" python runtests: PYTHONHASHSEED='456438988' python runtests: commands[0] | echo . . _____________________________________ summary ______________________________________ python: commands succeeded congratulations :) ``` From issues-reply at bitbucket.org Tue Apr 28 10:58:16 2015 From: issues-reply at bitbucket.org (adrianmoisey) Date: Tue, 28 Apr 2015 08:58:16 -0000 Subject: [Pytest-commit] Issue #241: Pypi server with auth (hpk42/tox) Message-ID: <20150428085816.4410.40857@app05.ash-private.bitbucket.org> New issue 241: Pypi server with auth https://bitbucket.org/hpk42/tox/issue/241/pypi-server-with-auth adrianmoisey: I'm trying to set another Pypi server in tox, but this server requires auth. I can't seem to find a way to get tox to use auth. If I include the username/password in the URL it doesn't work. From commits-noreply at bitbucket.org Tue Apr 28 11:52:58 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 28 Apr 2015 09:52:58 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in hpk42/pytest-patches/plugtestfix (pull request #283) Message-ID: <20150428095258.12454.57725@app02.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/a2dfd7c1fb40/ Changeset: a2dfd7c1fb40 User: hpk42 Date: 2015-04-28 09:52:53+00:00 Summary: Merged in hpk42/pytest-patches/plugtestfix (pull request #283) make test suite more tolerable against xdist causing warnings itself Affected #: 7 files diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -238,6 +238,13 @@ d[cat] = int(num) return d + def assertoutcome(self, passed=0, skipped=0, failed=0): + d = self.parseoutcomes() + assert passed == d.get("passed", 0) + assert skipped == d.get("skipped", 0) + assert failed == d.get("failed", 0) + + class TmpTestdir: """Temporary test directory with tools to test/run py.test itself. @@ -872,6 +879,7 @@ lines1 = val.split("\n") return LineMatcher(lines1).fnmatch_lines(lines2) + class LineMatcher: """Flexible matching of text. diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -58,14 +58,10 @@ class TestClass1: def __init__(self): pass - class TestClass2(object): - def __init__(self): - pass """) result = testdir.runpytest("-rw") - result.stdout.fnmatch_lines(""" + result.stdout.fnmatch_lines_random(""" WC1*test_class_with_init_warning.py*__init__* - *2 warnings* """) def test_class_subclassobject(self, testdir): diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -375,9 +375,7 @@ assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") - result.stdout.fnmatch_lines([ - "*2 passed in*", - ]) + result.assertoutcome(passed=2) def test_addcall_with_two_funcargs_generators(self, testdir): testdir.makeconftest(""" diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -368,9 +368,8 @@ def pytest_configure(config): config.warn("C1", "hello") def pytest_logwarning(code, message): - assert code == "C1" - assert message == "hello" - l.append(1) + if message == "hello" and code == "C1": + l.append(1) """) testdir.makepyfile(""" def test_proper(pytestconfig): @@ -391,15 +390,13 @@ pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines(""" - *1 warning* - """) + assert result.parseoutcomes()["warnings"] > 0 assert "hello" not in result.stdout.str() + result = testdir.runpytest("-rw") result.stdout.fnmatch_lines(""" ===*warning summary*=== *WT1*test_warn_on_test_item*:5*hello* - *1 warning* """) class TestRootdir: diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -339,9 +339,7 @@ assert False """) result = testdir.runpytest(p) - outcome = result.parseoutcomes() - outcome.pop('seconds') - assert outcome == dict(skipped=1) + result.assertoutcome(skipped=1) def test_SkipTest_in_test(testdir): diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/test_terminal.py --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -412,7 +412,7 @@ py.std.sys.platform, verinfo, py.__version__, pytest.__version__), "*test_header_trailer_info.py .", - "=* 1 passed in *.[0-9][0-9] seconds *=", + "=* 1 passed*in *.[0-9][0-9] seconds *=", ]) if pytest.config.pluginmanager._plugin_distinfo: result.stdout.fnmatch_lines([ diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e tox.ini --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ deps={[testenv:py27-xdist]deps} commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml testing + --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing} [testenv:py27-pexpect] changedir=testing Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Apr 28 11:52:57 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 28 Apr 2015 09:52:57 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150428095257.29060.62524@app04.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/156f6edf7067/ Changeset: 156f6edf7067 Branch: plugtestfix User: hpk42 Date: 2015-04-27 13:06:47+00:00 Summary: make test suite more tolerable against xdist causing warnings itself (which it does currently) Affected #: 7 files diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -238,6 +238,13 @@ d[cat] = int(num) return d + def assertoutcome(self, passed=0, skipped=0, failed=0): + d = self.parseoutcomes() + assert passed == d.get("passed", 0) + assert skipped == d.get("skipped", 0) + assert failed == d.get("failed", 0) + + class TmpTestdir: """Temporary test directory with tools to test/run py.test itself. @@ -872,6 +879,7 @@ lines1 = val.split("\n") return LineMatcher(lines1).fnmatch_lines(lines2) + class LineMatcher: """Flexible matching of text. diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -58,14 +58,10 @@ class TestClass1: def __init__(self): pass - class TestClass2(object): - def __init__(self): - pass """) result = testdir.runpytest("-rw") - result.stdout.fnmatch_lines(""" + result.stdout.fnmatch_lines_random(""" WC1*test_class_with_init_warning.py*__init__* - *2 warnings* """) def test_class_subclassobject(self, testdir): diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -375,9 +375,7 @@ assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") - result.stdout.fnmatch_lines([ - "*2 passed in*", - ]) + result.assertoutcome(passed=2) def test_addcall_with_two_funcargs_generators(self, testdir): testdir.makeconftest(""" diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -368,9 +368,8 @@ def pytest_configure(config): config.warn("C1", "hello") def pytest_logwarning(code, message): - assert code == "C1" - assert message == "hello" - l.append(1) + if message == "hello" and code == "C1": + l.append(1) """) testdir.makepyfile(""" def test_proper(pytestconfig): @@ -391,15 +390,13 @@ pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines(""" - *1 warning* - """) + assert result.parseoutcomes()["warnings"] > 0 assert "hello" not in result.stdout.str() + result = testdir.runpytest("-rw") result.stdout.fnmatch_lines(""" ===*warning summary*=== *WT1*test_warn_on_test_item*:5*hello* - *1 warning* """) class TestRootdir: diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -339,9 +339,7 @@ assert False """) result = testdir.runpytest(p) - outcome = result.parseoutcomes() - outcome.pop('seconds') - assert outcome == dict(skipped=1) + result.assertoutcome(skipped=1) def test_SkipTest_in_test(testdir): diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 testing/test_terminal.py --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -412,7 +412,7 @@ py.std.sys.platform, verinfo, py.__version__, pytest.__version__), "*test_header_trailer_info.py .", - "=* 1 passed in *.[0-9][0-9] seconds *=", + "=* 1 passed*in *.[0-9][0-9] seconds *=", ]) if pytest.config.pluginmanager._plugin_distinfo: result.stdout.fnmatch_lines([ diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r 156f6edf7067d395e77fa81742e56833066d3f37 tox.ini --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ deps={[testenv:py27-xdist]deps} commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml testing + --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing} [testenv:py27-pexpect] changedir=testing https://bitbucket.org/pytest-dev/pytest/commits/a2dfd7c1fb40/ Changeset: a2dfd7c1fb40 User: hpk42 Date: 2015-04-28 09:52:53+00:00 Summary: Merged in hpk42/pytest-patches/plugtestfix (pull request #283) make test suite more tolerable against xdist causing warnings itself Affected #: 7 files diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -238,6 +238,13 @@ d[cat] = int(num) return d + def assertoutcome(self, passed=0, skipped=0, failed=0): + d = self.parseoutcomes() + assert passed == d.get("passed", 0) + assert skipped == d.get("skipped", 0) + assert failed == d.get("failed", 0) + + class TmpTestdir: """Temporary test directory with tools to test/run py.test itself. @@ -872,6 +879,7 @@ lines1 = val.split("\n") return LineMatcher(lines1).fnmatch_lines(lines2) + class LineMatcher: """Flexible matching of text. diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -58,14 +58,10 @@ class TestClass1: def __init__(self): pass - class TestClass2(object): - def __init__(self): - pass """) result = testdir.runpytest("-rw") - result.stdout.fnmatch_lines(""" + result.stdout.fnmatch_lines_random(""" WC1*test_class_with_init_warning.py*__init__* - *2 warnings* """) def test_class_subclassobject(self, testdir): diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -375,9 +375,7 @@ assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") - result.stdout.fnmatch_lines([ - "*2 passed in*", - ]) + result.assertoutcome(passed=2) def test_addcall_with_two_funcargs_generators(self, testdir): testdir.makeconftest(""" diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -368,9 +368,8 @@ def pytest_configure(config): config.warn("C1", "hello") def pytest_logwarning(code, message): - assert code == "C1" - assert message == "hello" - l.append(1) + if message == "hello" and code == "C1": + l.append(1) """) testdir.makepyfile(""" def test_proper(pytestconfig): @@ -391,15 +390,13 @@ pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines(""" - *1 warning* - """) + assert result.parseoutcomes()["warnings"] > 0 assert "hello" not in result.stdout.str() + result = testdir.runpytest("-rw") result.stdout.fnmatch_lines(""" ===*warning summary*=== *WT1*test_warn_on_test_item*:5*hello* - *1 warning* """) class TestRootdir: diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -339,9 +339,7 @@ assert False """) result = testdir.runpytest(p) - outcome = result.parseoutcomes() - outcome.pop('seconds') - assert outcome == dict(skipped=1) + result.assertoutcome(skipped=1) def test_SkipTest_in_test(testdir): diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e testing/test_terminal.py --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -412,7 +412,7 @@ py.std.sys.platform, verinfo, py.__version__, pytest.__version__), "*test_header_trailer_info.py .", - "=* 1 passed in *.[0-9][0-9] seconds *=", + "=* 1 passed*in *.[0-9][0-9] seconds *=", ]) if pytest.config.pluginmanager._plugin_distinfo: result.stdout.fnmatch_lines([ diff -r 24f4d48abeeb28d15a7a43249c7329dbea2df1d8 -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e tox.ini --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ deps={[testenv:py27-xdist]deps} commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml testing + --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing} [testenv:py27-pexpect] changedir=testing Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Tue Apr 28 11:54:08 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 28 Apr 2015 09:54:08 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 99 Message-ID: <20150428095408.22927.47980@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/99 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4208:a2dfd7c1fb40 Author : holger krekel Branch : default Message: Merged in hpk42/pytest-patches/plugtestfix (pull request #283) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Tue Apr 28 11:55:13 2015 From: builds at drone.io (Drone.io Build) Date: Tue, 28 Apr 2015 09:55:13 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 100 Message-ID: <20150428095512.85268.15096@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/100 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4208:a2dfd7c1fb40 Author : holger krekel Branch : default Message: Merged in hpk42/pytest-patches/plugtestfix (pull request #283) -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Tue Apr 28 12:17:56 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 28 Apr 2015 10:17:56 -0000 Subject: [Pytest-commit] commit/tox: blueyed: getcommandpath: mention whitelist_externals option with warning Message-ID: <20150428101756.30064.78827@app03.ash-private.bitbucket.org> 1 new commit in tox: https://bitbucket.org/hpk42/tox/commits/d75d1f353d0a/ Changeset: d75d1f353d0a User: blueyed Date: 2015-04-28 07:34:20+00:00 Summary: getcommandpath: mention whitelist_externals option with warning Affected #: 1 file diff -r 9383b904df1808de2e546000e49f8231c2f94f79 -r d75d1f353d0a442384156b1ec2e92fd1bde6fa9f tox/_venv.py --- a/tox/_venv.py +++ b/tox/_venv.py @@ -92,7 +92,9 @@ "test command found but not installed in testenv\n" " cmd: %s\n" " env: %s\n" - "Maybe forgot to specify a dependency?" % (p, self.envconfig.envdir)) + "Maybe you forgot to specify a dependency? " + "See also the whitelist_externals envconfig setting." % ( + p, self.envconfig.envdir)) return str(p) # will not be rewritten for reporting def is_allowed_external(self, p): Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Apr 28 12:21:36 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 28 Apr 2015 10:21:36 -0000 Subject: [Pytest-commit] commit/tox: 2 new changesets Message-ID: <20150428102136.3342.31915@app02.ash-private.bitbucket.org> 2 new commits in tox: https://bitbucket.org/hpk42/tox/commits/790d8f87af00/ Changeset: 790d8f87af00 User: hpk42 Date: 2015-04-28 10:20:07+00:00 Summary: fix issue240: allow to specify empty argument list without it being rewritten to ".". Thanks Daniel Hahler. Affected #: 3 files diff -r 9383b904df1808de2e546000e49f8231c2f94f79 -r 790d8f87af00c7caf12c72376459f8607a06752d CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,9 @@ - tox has now somewhat pep8 clean code, thanks to Volodymyr Vitvitski. +- fix issue240: allow to specify empty argument list without it being + rewritten to ".". Thanks Daniel Hahler. + 1.9.2 ----------- diff -r 9383b904df1808de2e546000e49f8231c2f94f79 -r 790d8f87af00c7caf12c72376459f8607a06752d tests/test_config.py --- a/tests/test_config.py +++ b/tests/test_config.py @@ -863,6 +863,15 @@ assert argv[0] == ["cmd1", "[hello]", "world"] assert argv[1] == ["cmd1", "brave", "new", "world"] + def test_substitution_noargs_issue240(self, newconfig): + inisource = """ + [testenv] + commands = echo {posargs:foo} + """ + conf = newconfig([""], inisource).envconfigs['python'] + argv = conf.commands + assert argv[0] == ["echo"] + def test_posargs_backslashed_or_quoted(self, tmpdir, newconfig): inisource = """ [testenv:py24] diff -r 9383b904df1808de2e546000e49f8231c2f94f79 -r 790d8f87af00c7caf12c72376459f8607a06752d tox/_config.py --- a/tox/_config.py +++ b/tox/_config.py @@ -373,9 +373,10 @@ if vc.args_are_paths: args = [] for arg in config.option.args: - origpath = config.invocationcwd.join(arg, abs=True) - if origpath.check(): - arg = vc.changedir.bestrelpath(origpath) + if arg: + origpath = config.invocationcwd.join(arg, abs=True) + if origpath.check(): + arg = vc.changedir.bestrelpath(origpath) args.append(arg) reader.addsubstitutions(args) setenv = {} https://bitbucket.org/hpk42/tox/commits/273d12589a25/ Changeset: 273d12589a25 User: hpk42 Date: 2015-04-28 10:21:19+00:00 Summary: merge Affected #: 1 file diff -r 790d8f87af00c7caf12c72376459f8607a06752d -r 273d12589a2548f4a0603da9d82ade8a284d4196 tox/_venv.py --- a/tox/_venv.py +++ b/tox/_venv.py @@ -92,7 +92,9 @@ "test command found but not installed in testenv\n" " cmd: %s\n" " env: %s\n" - "Maybe forgot to specify a dependency?" % (p, self.envconfig.envdir)) + "Maybe you forgot to specify a dependency? " + "See also the whitelist_externals envconfig setting." % ( + p, self.envconfig.envdir)) return str(p) # will not be rewritten for reporting def is_allowed_external(self, p): Repository URL: https://bitbucket.org/hpk42/tox/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From issues-reply at bitbucket.org Wed Apr 29 14:29:35 2015 From: issues-reply at bitbucket.org (Adrian Teng) Date: Wed, 29 Apr 2015 12:29:35 -0000 Subject: [Pytest-commit] Issue #734: Release pytest-xdist (pytest-dev/pytest) Message-ID: <20150429122935.16450.20460@app12.ash-private.bitbucket.org> New issue 734: Release pytest-xdist https://bitbucket.org/pytest-dev/pytest/issue/734/release-pytest-xdist Adrian Teng: Would anyone mind releasing a new version of pytest-xdist? [This issue](https://bitbucket.org/pytest-dev/pytest/issue/594/pytest-with-xdist-is-not-executing-tests) has been fixed for a while now but no release was done since then. From commits-noreply at bitbucket.org Wed Apr 29 16:32:35 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 29 Apr 2015 14:32:35 -0000 Subject: [Pytest-commit] commit/pytest: hpk42: Merged in hpk42/pytest-patches/testrefactor (pull request #284) Message-ID: <20150429143235.14694.12634@app05.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/7d4a0b78d19b/ Changeset: 7d4a0b78d19b User: hpk42 Date: 2015-04-29 14:32:28+00:00 Summary: Merged in hpk42/pytest-patches/testrefactor (pull request #284) majorly refactor pytester and speed/streamline tests Affected #: 25 files diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,14 @@ implementations. Use the ``hookwrapper`` mechanism instead already introduced with pytest-2.7. +- speed up pytest's own test suite considerably by using inprocess + tests by default (testrun can be modified with --runpytest=subprocess + to create subprocesses in many places instead). The main + APIs to run pytest in a test is "runpytest()" or "runpytest_subprocess" + and "runpytest_inprocess" if you need a particular way of running + the test. In all cases you get back a RunResult but the inprocess + one will also have a "reprec" attribute with the recorded events/reports. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -29,17 +29,24 @@ initialization. """ try: - config = _prepareconfig(args, plugins) - except ConftestImportFailure: - e = sys.exc_info()[1] - tw = py.io.TerminalWriter(sys.stderr) - for line in traceback.format_exception(*e.excinfo): - tw.line(line.rstrip(), red=True) - tw.line("ERROR: could not load %s\n" % (e.path), red=True) + try: + config = _prepareconfig(args, plugins) + except ConftestImportFailure as e: + tw = py.io.TerminalWriter(sys.stderr) + for line in traceback.format_exception(*e.excinfo): + tw.line(line.rstrip(), red=True) + tw.line("ERROR: could not load %s\n" % (e.path), red=True) + return 4 + else: + try: + config.pluginmanager.check_pending() + return config.hook.pytest_cmdline_main(config=config) + finally: + config._ensure_unconfigure() + except UsageError as e: + for msg in e.args: + sys.stderr.write("ERROR: %s\n" %(msg,)) return 4 - else: - config.pluginmanager.check_pending() - return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace main = staticmethod(main) @@ -81,12 +88,18 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_config().pluginmanager - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) + config = get_config() + pluginmanager = config.pluginmanager + try: + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + except BaseException: + config._ensure_unconfigure() + raise + def exclude_pytest_names(name): return not name.startswith(name) or name == "pytest_plugins" or \ @@ -259,7 +272,10 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - self.set_blocked(arg[3:]) + name = arg[3:] + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) else: self.import_plugin(arg) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -83,10 +83,7 @@ initstate = 2 doit(config, session) except pytest.UsageError: - args = sys.exc_info()[1].args - for msg in args: - sys.stderr.write("ERROR: %s\n" %(msg,)) - session.exitstatus = EXIT_USAGEERROR + raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,5 +1,7 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ +import gc import sys +import traceback import os import codecs import re @@ -15,6 +17,136 @@ from _pytest.main import Session, EXIT_OK + +def pytest_addoption(parser): + # group = parser.getgroup("pytester", "pytester (self-tests) options") + parser.addoption('--lsof', + action="store_true", dest="lsof", default=False, + help=("run FD checks if lsof is available")) + + parser.addoption('--runpytest', default="inprocess", dest="runpytest", + choices=("inprocess", "subprocess", ), + help=("run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method")) + + +def pytest_configure(config): + # This might be called multiple times. Only take the first. + global _pytest_fullpath + try: + _pytest_fullpath + except NameError: + _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) + _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") + + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + + +class LsofFdLeakChecker(object): + def get_open_files(self): + out = self._exec_lsof() + open_files = self._parse_lsof_output(out) + return open_files + + def _exec_lsof(self): + pid = os.getpid() + return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) + + def _parse_lsof_output(self, out): + def isopen(line): + return line.startswith('f') and ("deleted" not in line and + 'mem' not in line and "txt" not in line and 'cwd' not in line) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split('\0') + fd = fields[0][1:] + filename = fields[1][1:] + if filename.startswith('/'): + open_files.append((fd, filename)) + + return open_files + + def matching_platform(self): + try: + py.process.cmdexec("lsof -v") + except py.process.cmdexec.Error: + return False + else: + return True + + @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True) + def pytest_runtest_item(self, item): + lines1 = self.get_open_files() + yield + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1]) + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [] + error.append("***** %s FD leakage detected" % len(leaked_files)) + error.extend([str(f) for f in leaked_files]) + error.append("*** Before:") + error.extend([str(f) for f in lines1]) + error.append("*** After:") + error.extend([str(f) for f in lines2]) + error.append(error[0]) + error.append("*** function %s:%s: %s " % item.location) + pytest.fail("\n".join(error), pytrace=False) + + +# XXX copied from execnet's conftest.py - needs to be merged +winpymap = { + 'python2.7': r'C:\Python27\python.exe', + 'python2.6': r'C:\Python26\python.exe', + 'python3.1': r'C:\Python31\python.exe', + 'python3.2': r'C:\Python32\python.exe', + 'python3.3': r'C:\Python33\python.exe', + 'python3.4': r'C:\Python34\python.exe', + 'python3.5': r'C:\Python35\python.exe', +} + +def getexecutable(name, cache={}): + try: + return cache[name] + except KeyError: + executable = py.path.local.sysfind(name) + if executable: + if name == "jython": + import subprocess + popen = subprocess.Popen([str(executable), "--version"], + universal_newlines=True, stderr=subprocess.PIPE) + out, err = popen.communicate() + if not err or "2.5" not in err: + executable = None + if "2.5.2" in err: + executable = None # http://bugs.jython.org/issue1790 + cache[name] = executable + return executable + + at pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", + 'pypy', 'pypy3']) +def anypython(request): + name = request.param + executable = getexecutable(name) + if executable is None: + if sys.platform == "win32": + executable = winpymap.get(name, None) + if executable: + executable = py.path.local(executable) + if executable.check(): + return executable + pytest.skip("no suitable %s found" % (name,)) + return executable + # used at least by pytest-xdist plugin @pytest.fixture def _pytest(request): @@ -39,23 +171,6 @@ return [x for x in l if x[0] != "_"] -def pytest_addoption(parser): - group = parser.getgroup("pylib") - group.addoption('--no-tools-on-path', - action="store_true", dest="notoolsonpath", default=False, - help=("discover tools on PATH instead of going through py.cmdline.") - ) - -def pytest_configure(config): - # This might be called multiple times. Only take the first. - global _pytest_fullpath - try: - _pytest_fullpath - except NameError: - _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) - _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") - - class ParsedCall: def __init__(self, name, kwargs): self.__dict__.update(kwargs) @@ -201,9 +316,11 @@ return LineMatcher def pytest_funcarg__testdir(request): - tmptestdir = TmpTestdir(request) + tmptestdir = Testdir(request) return tmptestdir + + rex_outcome = re.compile("(\d+) (\w+)") class RunResult: """The result of running a command. @@ -213,10 +330,10 @@ :ret: The return value. :outlines: List of lines captured from stdout. :errlines: List of lines captures from stderr. - :stdout: LineMatcher of stdout, use ``stdout.str()`` to + :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` method. - :stderrr: LineMatcher of stderr. + :stderrr: :py:class:`LineMatcher` of stderr. :duration: Duration in seconds. """ @@ -229,6 +346,8 @@ self.duration = duration def parseoutcomes(self): + """ Return a dictionary of outcomestring->num from parsing + the terminal output that the test process produced.""" for line in reversed(self.outlines): if 'seconds' in line: outcomes = rex_outcome.findall(line) @@ -238,14 +357,17 @@ d[cat] = int(num) return d - def assertoutcome(self, passed=0, skipped=0, failed=0): + def assert_outcomes(self, passed=0, skipped=0, failed=0): + """ assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" d = self.parseoutcomes() assert passed == d.get("passed", 0) assert skipped == d.get("skipped", 0) assert failed == d.get("failed", 0) -class TmpTestdir: + +class Testdir: """Temporary test directory with tools to test/run py.test itself. This is based on the ``tmpdir`` fixture but provides a number of @@ -268,7 +390,6 @@ def __init__(self, request): self.request = request - self.Config = request.config.__class__ # XXX remove duplication with tmpdir plugin basetmp = request.config._tmpdirhandler.ensuretemp("testdir") name = request.function.__name__ @@ -280,12 +401,18 @@ break self.tmpdir = tmpdir self.plugins = [] - self._savesyspath = list(sys.path) + self._savesyspath = (list(sys.path), list(sys.meta_path)) + self._savemodulekeys = set(sys.modules) self.chdir() # always chdir self.request.addfinalizer(self.finalize) + method = self.request.config.getoption("--runpytest") + if method == "inprocess": + self._runpytest_method = self.runpytest_inprocess + elif method == "subprocess": + self._runpytest_method = self.runpytest_subprocess def __repr__(self): - return "" % (self.tmpdir,) + return "" % (self.tmpdir,) def finalize(self): """Clean up global state artifacts. @@ -296,23 +423,22 @@ has finished. """ - sys.path[:] = self._savesyspath + sys.path[:], sys.meta_path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() self.delete_loaded_modules() def delete_loaded_modules(self): - """Delete modules that have been loaded from tmpdir. + """Delete modules that have been loaded during a test. This allows the interpreter to catch module changes in case the module is re-imported. - """ - for name, mod in list(sys.modules.items()): - if mod: - fn = getattr(mod, '__file__', None) - if fn and fn.startswith(str(self.tmpdir)): - del sys.modules[name] + for name in set(sys.modules).difference(self._savemodulekeys): + # it seems zope.interfaces is keeping some state + # (used by twisted related tests) + if name != "zope.interface": + del sys.modules[name] def make_hook_recorder(self, pluginmanager): """Create a new :py:class:`HookRecorder` for a PluginManager.""" @@ -503,43 +629,19 @@ l = list(cmdlineargs) + [p] return self.inline_run(*l) - def inline_runsource1(self, *args): - """Run a test module in process using ``pytest.main()``. - - This behaves exactly like :py:meth:`inline_runsource` and - takes identical arguments. However the return value is a list - of the reports created by the pytest_runtest_logreport hook - during the run. - - """ - args = list(args) - source = args.pop() - p = self.makepyfile(source) - l = list(args) + [p] - reprec = self.inline_run(*l) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3, reports # setup/call/teardown - return reports[1] - def inline_genitems(self, *args): """Run ``pytest.main(['--collectonly'])`` in-process. Retuns a tuple of the collected items and a :py:class:`HookRecorder` instance. - """ - return self.inprocess_run(list(args) + ['--collectonly']) - - def inprocess_run(self, args, plugins=()): - """Run ``pytest.main()`` in-process, return Items and a HookRecorder. - This runs the :py:func:`pytest.main` function to run all of py.test inside the test process itself like :py:meth:`inline_run`. However the return value is a tuple of the collection items and a :py:class:`HookRecorder` instance. """ - rec = self.inline_run(*args, plugins=plugins) + rec = self.inline_run("--collect-only", *args) items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec @@ -568,12 +670,50 @@ plugins = kwargs.get("plugins") or [] plugins.append(Collect()) ret = pytest.main(list(args), plugins=plugins) - assert len(rec) == 1 - reprec = rec[0] + self.delete_loaded_modules() + if len(rec) == 1: + reprec = rec.pop() + else: + class reprec: + pass reprec.ret = ret - self.delete_loaded_modules() return reprec + def runpytest_inprocess(self, *args, **kwargs): + """ Return result of running pytest in-process, providing a similar + interface to what self.runpytest() provides. """ + if kwargs.get("syspathinsert"): + self.syspathinsert() + now = time.time() + capture = py.io.StdCapture() + try: + try: + reprec = self.inline_run(*args) + except SystemExit as e: + class reprec: + ret = e.args[0] + except Exception: + traceback.print_exc() + class reprec: + ret = 3 + finally: + out, err = capture.reset() + sys.stdout.write(out) + sys.stderr.write(err) + + res = RunResult(reprec.ret, + out.split("\n"), err.split("\n"), + time.time()-now) + res.reprec = reprec + return res + + def runpytest(self, *args, **kwargs): + """ Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`RunResult`. + + """ + return self._runpytest_method(*args, **kwargs) + def parseconfig(self, *args): """Return a new py.test Config instance from given commandline args. @@ -745,57 +885,23 @@ except UnicodeEncodeError: print("couldn't print to %s because of encoding" % (fp,)) - def runpybin(self, scriptname, *args): - """Run a py.* tool with arguments. + def _getpytestargs(self): + # we cannot use "(sys.executable,script)" + # because on windows the script is e.g. a py.test.exe + return (sys.executable, _pytest_fullpath,) # noqa - This can realy only be used to run py.test, you probably want - :py:meth:`runpytest` instead. + def runpython(self, script): + """Run a python script using sys.executable as interpreter. Returns a :py:class:`RunResult`. - """ - fullargs = self._getpybinargs(scriptname) + args - return self.run(*fullargs) - - def _getpybinargs(self, scriptname): - if not self.request.config.getvalue("notoolsonpath"): - # XXX we rely on script referring to the correct environment - # we cannot use "(sys.executable,script)" - # because on windows the script is e.g. a py.test.exe - return (sys.executable, _pytest_fullpath,) # noqa - else: - pytest.skip("cannot run %r with --no-tools-on-path" % scriptname) - - def runpython(self, script, prepend=True): - """Run a python script. - - If ``prepend`` is True then the directory from which the py - package has been imported will be prepended to sys.path. - - Returns a :py:class:`RunResult`. - - """ - # XXX The prepend feature is probably not very useful since the - # split of py and pytest. - if prepend: - s = self._getsysprepend() - if s: - script.write(s + "\n" + script.read()) return self.run(sys.executable, script) - def _getsysprepend(self): - if self.request.config.getvalue("notoolsonpath"): - s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) - else: - s = "" - return s - def runpython_c(self, command): """Run python -c "command", return a :py:class:`RunResult`.""" - command = self._getsysprepend() + command return self.run(sys.executable, "-c", command) - def runpytest(self, *args): + def runpytest_subprocess(self, *args, **kwargs): """Run py.test as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will added @@ -820,7 +926,8 @@ plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args - return self.runpybin("py.test", *args) + args = self._getpytestargs() + args + return self.run(*args) def spawn_pytest(self, string, expect_timeout=10.0): """Run py.test using pexpect. @@ -831,10 +938,8 @@ The pexpect child is returned. """ - if self.request.config.getvalue("notoolsonpath"): - pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") - invoke = " ".join(map(str, self._getpybinargs("py.test"))) + invoke = " ".join(map(str, self._getpytestargs())) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c doc/en/example/assertion/test_failures.py --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -7,7 +7,7 @@ target = testdir.tmpdir.join(failure_demo.basename) failure_demo.copy(target) failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) - result = testdir.runpytest(target) + result = testdir.runpytest(target, syspathinsert=True) result.stdout.fnmatch_lines([ "*42 failed*" ]) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c doc/en/writing_plugins.txt --- a/doc/en/writing_plugins.txt +++ b/doc/en/writing_plugins.txt @@ -186,12 +186,44 @@ If you want to look at the names of existing plugins, use the ``--traceconfig`` option. +Testing plugins +--------------- + +pytest comes with some facilities that you can enable for testing your +plugin. Given that you have an installed plugin you can enable the +:py:class:`testdir <_pytest.pytester.Testdir>` fixture via specifying a +command line option to include the pytester plugin (``-p pytester``) or +by putting ``pytest_plugins = pytester`` into your test or +``conftest.py`` file. You then will have a ``testdir`` fixure which you +can use like this:: + + # content of test_myplugin.py + + pytest_plugins = pytester # to get testdir fixture + + def test_myplugin(testdir): + testdir.makepyfile(""" + def test_example(): + pass + """) + result = testdir.runpytest("--verbose") + result.fnmatch_lines(""" + test_example* + """) + +Note that by default ``testdir.runpytest()`` will perform a pytest +in-process. You can pass the command line option ``--runpytest=subprocess`` +to have it happen in a subprocess. + +Also see the :py:class:`RunResult <_pytest.pytester.RunResult>` for more +methods of the result object that you get from a call to ``runpytest``. .. _`writinghooks`: Writing hook functions ====================== + .. _validation: hook function validation and execution @@ -493,3 +525,13 @@ .. autoclass:: _pytest.core.CallOutcome() :members: +.. currentmodule:: _pytest.pytester + +.. autoclass:: Testdir() + :members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile + +.. autoclass:: RunResult() + :members: + +.. autoclass:: LineMatcher() + :members: diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -82,7 +82,7 @@ def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) - result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123") + result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ '*1 passed*', @@ -203,7 +203,7 @@ os.chdir(os.path.dirname(os.getcwd())) print (py.log) """)) - result = testdir.runpython(p, prepend=False) + result = testdir.runpython(p) assert not result.ret def test_issue109_sibling_conftests_not_loaded(self, testdir): @@ -353,7 +353,8 @@ *unrecognized* """) - def test_getsourcelines_error_issue553(self, testdir): + def test_getsourcelines_error_issue553(self, testdir, monkeypatch): + monkeypatch.setattr("inspect.getsourcelines", None) p = testdir.makepyfile(""" def raise_error(obj): raise IOError('source code not available') diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/conftest.py --- a/testing/conftest.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -import sys - -pytest_plugins = "pytester", - -import os, py - -class LsofFdLeakChecker(object): - def get_open_files(self): - out = self._exec_lsof() - open_files = self._parse_lsof_output(out) - return open_files - - def _exec_lsof(self): - pid = os.getpid() - return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) - - def _parse_lsof_output(self, out): - def isopen(line): - return line.startswith('f') and ( - "deleted" not in line and 'mem' not in line and "txt" not in line and 'cwd' not in line) - - open_files = [] - - for line in out.split("\n"): - if isopen(line): - fields = line.split('\0') - fd = fields[0][1:] - filename = fields[1][1:] - if filename.startswith('/'): - open_files.append((fd, filename)) - - return open_files - - -def pytest_addoption(parser): - parser.addoption('--lsof', - action="store_true", dest="lsof", default=False, - help=("run FD checks if lsof is available")) - -def pytest_runtest_setup(item): - config = item.config - config._basedir = py.path.local() - if config.getvalue("lsof"): - try: - config._fd_leak_checker = LsofFdLeakChecker() - config._openfiles = config._fd_leak_checker.get_open_files() - except py.process.cmdexec.Error: - pass - -#def pytest_report_header(): -# return "pid: %s" % os.getpid() - -def check_open_files(config): - lines2 = config._fd_leak_checker.get_open_files() - new_fds = set([t[0] for t in lines2]) - set([t[0] for t in config._openfiles]) - open_files = [t for t in lines2 if t[0] in new_fds] - if open_files: - error = [] - error.append("***** %s FD leakage detected" % len(open_files)) - error.extend([str(f) for f in open_files]) - error.append("*** Before:") - error.extend([str(f) for f in config._openfiles]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - raise AssertionError("\n".join(error)) - - at pytest.hookimpl_opts(hookwrapper=True, trylast=True) -def pytest_runtest_teardown(item): - yield - item.config._basedir.chdir() - if hasattr(item.config, '_openfiles'): - check_open_files(item.config) - -# XXX copied from execnet's conftest.py - needs to be merged -winpymap = { - 'python2.7': r'C:\Python27\python.exe', - 'python2.6': r'C:\Python26\python.exe', - 'python3.1': r'C:\Python31\python.exe', - 'python3.2': r'C:\Python32\python.exe', - 'python3.3': r'C:\Python33\python.exe', - 'python3.4': r'C:\Python34\python.exe', - 'python3.5': r'C:\Python35\python.exe', -} - -def getexecutable(name, cache={}): - try: - return cache[name] - except KeyError: - executable = py.path.local.sysfind(name) - if executable: - if name == "jython": - import subprocess - popen = subprocess.Popen([str(executable), "--version"], - universal_newlines=True, stderr=subprocess.PIPE) - out, err = popen.communicate() - if not err or "2.5" not in err: - executable = None - if "2.5.2" in err: - executable = None # http://bugs.jython.org/issue1790 - cache[name] = executable - return executable - - at pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", - 'pypy', 'pypy3']) -def anypython(request): - name = request.param - executable = getexecutable(name) - if executable is None: - if sys.platform == "win32": - executable = winpymap.get(name, None) - if executable: - executable = py.path.local(executable) - if executable.check(): - return executable - pytest.skip("no suitable %s found" % (name,)) - return executable diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -627,9 +627,7 @@ sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") result = testdir.runpytest("-v", "-s") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -100,9 +100,7 @@ sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") sub2.join("test_in_sub2.py").write("def test_2(arg2): pass") result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_extend_fixture_module_class(self, testdir): testfile = testdir.makepyfile(""" diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -292,9 +292,7 @@ """) result = testdir.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*6 fail*", - ]) + result.assert_outcomes(failed=6) def test_parametrize_CSV(self, testdir): testdir.makepyfile(""" @@ -375,7 +373,7 @@ assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") - result.assertoutcome(passed=2) + result.assert_outcomes(passed=2) def test_addcall_with_two_funcargs_generators(self, testdir): testdir.makeconftest(""" @@ -430,9 +428,7 @@ pass """) result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 pass*", - ]) + result.assert_outcomes(passed=1) def test_generate_plugin_and_module(self, testdir): @@ -506,9 +502,7 @@ self.val = 1 """) result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 pass*", - ]) + result.assert_outcomes(passed=1) def test_parametrize_functional2(self, testdir): testdir.makepyfile(""" @@ -653,8 +647,8 @@ def test_function(): pass """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=1) def test_generate_tests_only_done_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") @@ -670,9 +664,7 @@ sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") result = testdir.runpytest("-v", "-s", sub1, sub2, sub1) - result.stdout.fnmatch_lines([ - "*3 passed*" - ]) + result.assert_outcomes(passed=3) def test_generate_same_function_names_issue403(self, testdir): testdir.makepyfile(""" @@ -687,8 +679,8 @@ test_x = make_tests() test_y = make_tests() """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=4) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=4) @pytest.mark.issue463 def test_parameterize_misspelling(self, testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_assertion.py --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -461,7 +461,7 @@ ("--assert=plain", "--nomagic"), ("--assert=plain", "--no-assert", "--nomagic")) for opt in off_options: - result = testdir.runpytest(*opt) + result = testdir.runpytest_subprocess(*opt) assert "3 == 4" not in result.stdout.str() def test_old_assert_mode(testdir): @@ -469,7 +469,7 @@ def test_in_old_mode(): assert "@py_builtins" not in globals() """) - result = testdir.runpytest("--assert=reinterp") + result = testdir.runpytest_subprocess("--assert=reinterp") assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -453,7 +453,7 @@ assert not os.path.exists(__cached__) assert not os.path.exists(os.path.dirname(__cached__))""") monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") - assert testdir.runpytest().ret == 0 + assert testdir.runpytest_subprocess().ret == 0 @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): @@ -468,12 +468,12 @@ tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - assert testdir.runpybin("py.test", tmp).ret == 0 + assert testdir.runpytest_subprocess(tmp).ret == 0 tagged = "test_pyc_vs_pyo." + PYTEST_TAG assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.undo() monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - assert testdir.runpybin("py.test", tmp).ret == 1 + assert testdir.runpytest_subprocess(tmp).ret == 1 assert tagged + ".pyc" in os.listdir("__pycache__") def test_package(self, testdir): @@ -615,10 +615,8 @@ testdir.makepyfile(**contents) testdir.maketxtfile(**{'testpkg/resource': "Load me please."}) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '* 1 passed*', - ]) + result = testdir.runpytest_subprocess() + result.assert_outcomes(passed=1) def test_read_pyc(self, tmpdir): """ diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_capture.py --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -282,7 +282,7 @@ logging.basicConfig(stream=stream) stream.close() # to free memory/release resources """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stderr.str().find("atexit") == -1 def test_logging_and_immediate_setupteardown(self, testdir): @@ -301,7 +301,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors show first! @@ -327,7 +327,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors come first @@ -348,7 +348,7 @@ logging.warn("hello432") assert 0 """) - result = testdir.runpytest( + result = testdir.runpytest_subprocess( p, "--traceconfig", "-p", "no:capturelog") assert result.ret != 0 @@ -364,7 +364,7 @@ logging.warn("hello435") """) # make sure that logging is still captured in tests - result = testdir.runpytest("-s", "-p", "no:capturelog") + result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") assert result.ret == 0 result.stderr.fnmatch_lines([ "WARNING*hello435*", @@ -383,7 +383,7 @@ logging.warn("hello433") assert 0 """) - result = testdir.runpytest(p, "-p", "no:capturelog") + result = testdir.runpytest_subprocess(p, "-p", "no:capturelog") assert result.ret != 0 result.stdout.fnmatch_lines([ "WARNING*hello433*", @@ -461,7 +461,7 @@ os.write(1, str(42).encode('ascii')) raise KeyboardInterrupt() """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*KeyboardInterrupt*" ]) @@ -474,7 +474,7 @@ def test_log(capsys): logging.error('x') """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) assert 'closed' not in result.stderr.str() @@ -500,7 +500,7 @@ def test_hello(capfd): pass """) - result = testdir.runpytest("--capture=no") + result = testdir.runpytest_subprocess("--capture=no") result.stdout.fnmatch_lines([ "*1 skipped*" ]) @@ -563,9 +563,7 @@ test_foo() """) result = testdir.runpytest('--assert=plain') - result.stdout.fnmatch_lines([ - '*2 passed*', - ]) + result.assert_outcomes(passed=2) class TestTextIO: @@ -885,7 +883,7 @@ os.write(1, "hello\\n".encode("ascii")) assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_x* *assert 0* @@ -936,7 +934,7 @@ cap = StdCaptureFD(out=False, err=False, in_=True) cap.stop_capturing() """) - result = testdir.runpytest("--capture=fd") + result = testdir.runpytest_subprocess("--capture=fd") assert result.ret == 0 assert result.parseoutcomes()['passed'] == 3 @@ -971,7 +969,7 @@ os.write(1, b"hello\\n") assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_capture_again* *assert 0* diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_collection.py --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -296,7 +296,6 @@ subdir.ensure("__init__.py") target = subdir.join(p.basename) p.move(target) - testdir.chdir() subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Session(config=config) @@ -313,7 +312,7 @@ def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - # XXX migrate to inline_genitems? (see below) + # XXX migrate to collectonly? (see below) config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Session(config) @@ -470,7 +469,6 @@ assert col.config is config def test_pkgfile(self, testdir): - testdir.chdir() tmpdir = testdir.tmpdir subdir = tmpdir.join("subdir") x = subdir.ensure("x.py") diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -75,7 +75,7 @@ [pytest] addopts = --qwe """) - result = testdir.runpytest("--confcutdir=.") + result = testdir.inline_run("--confcutdir=.") assert result.ret == 0 class TestConfigCmdlineParsing: diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -961,7 +961,7 @@ """) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ "WI1*skipped plugin*skipping1*hello*", @@ -990,7 +990,7 @@ assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) + result = testdir.runpytest(p, syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_doctest.py --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,5 @@ from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile -import py, pytest +import py class TestDoctests: @@ -75,8 +75,6 @@ assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - @pytest.mark.xfail('hasattr(sys, "pypy_version_info")', reason= - "pypy leaks one FD") def test_simple_doctestfile(self, testdir): p = testdir.maketxtfile(test_doc=""" >>> x = 1 diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_genscript.py --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -16,7 +16,6 @@ assert self.script.check() def run(self, anypython, testdir, *args): - testdir.chdir() return testdir._run(anypython, self.script, *args) def test_gen(testdir, anypython, standalone): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_helpconfig.py --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -53,14 +53,14 @@ ]) def test_debug(testdir, monkeypatch): - result = testdir.runpytest("--debug") + result = testdir.runpytest_subprocess("--debug") assert result.ret == 0 p = testdir.tmpdir.join("pytestdebug.log") assert "pytest_sessionstart" in p.read() def test_PYTEST_DEBUG(testdir, monkeypatch): monkeypatch.setenv("PYTEST_DEBUG", "1") - result = testdir.runpytest() + result = testdir.runpytest_subprocess() assert result.ret == 0 result.stderr.fnmatch_lines([ "*pytest_plugin_registered*", diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -19,9 +19,7 @@ test_hello.teardown = lambda: l.append(2) """) result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_setup_func_with_setup_decorator(): @@ -66,9 +64,7 @@ """) result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_nose_setup_func_failure(testdir): @@ -302,7 +298,7 @@ pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result.assert_outcomes(passed=1) @pytest.mark.skipif("sys.version_info < (2,6)") def test_setup_teardown_linking_issue265(testdir): @@ -327,8 +323,8 @@ """Undoes the setup.""" raise Exception("should not call teardown for skipped tests") ''') - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1, skipped=1) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=1, skipped=1) def test_SkipTest_during_collection(testdir): @@ -339,7 +335,7 @@ assert False """) result = testdir.runpytest(p) - result.assertoutcome(skipped=1) + result.assert_outcomes(skipped=1) def test_SkipTest_in_test(testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_pdb.py --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,6 +2,13 @@ import py import sys +def runpdb_and_get_report(testdir, source): + p = testdir.makepyfile(source) + result = testdir.runpytest_inprocess("--pdb", p) + reports = result.reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3, reports # setup/call/teardown + return reports[1] + class TestPDB: def pytest_funcarg__pdblist(self, request): @@ -14,7 +21,7 @@ return pdblist def test_pdb_on_fail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ def test_func(): assert 0 """) @@ -24,7 +31,7 @@ assert tb[-1].name == "test_func" def test_pdb_on_xfail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest @pytest.mark.xfail def test_func(): @@ -34,7 +41,7 @@ assert not pdblist def test_pdb_on_skip(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest def test_func(): pytest.skip("hello") @@ -43,7 +50,7 @@ assert len(pdblist) == 0 def test_pdb_on_BdbQuit(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import bdb def test_func(): raise bdb.BdbQuit @@ -260,7 +267,7 @@ def test_pdb_collection_failure_is_shown(self, testdir): p1 = testdir.makepyfile("""xxx """) - result = testdir.runpytest("--pdb", p1) + result = testdir.runpytest_subprocess("--pdb", p1) result.stdout.fnmatch_lines([ "*NameError*xxx*", "*1 error*", diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -69,9 +69,7 @@ assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) + result.assert_outcomes(passed=1) def make_holder(): @@ -114,16 +112,6 @@ unichr = chr testdir.makepyfile(unichr(0xfffd)) -def test_inprocess_plugins(testdir): - class Plugin(object): - configured = False - def pytest_configure(self, config): - self.configured = True - plugin = Plugin() - testdir.inprocess_run([], [plugin]) - - assert plugin.configured - def test_inline_run_clean_modules(testdir): test_mod = testdir.makepyfile("def test_foo(): assert True") result = testdir.inline_run(str(test_mod)) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -203,7 +203,6 @@ def test_plugin_specify(testdir): - testdir.chdir() pytest.raises(ImportError, """ testdir.parseconfig("-p", "nqweotexistent") """) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c tox.ini --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,py27-trial,py33-trial,doctesting,py27-cxfreeze +envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,{py27,py33}-trial,py27-subprocess,doctesting,py27-cxfreeze [testenv] changedir=testing @@ -9,6 +9,15 @@ nose mock +[testenv:py27-subprocess] +changedir=. +basepython=python2.7 +deps=pytest-xdist + mock + nose +commands= + py.test -n3 -rfsxX --runpytest=subprocess {posargs:testing} + [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 @@ -136,7 +145,7 @@ minversion=2.0 plugins=pytester #--pyargs --doctest-modules --ignore=.tox -addopts= -rxsX +addopts= -rxsX -p pytester rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Wed Apr 29 16:33:49 2015 From: builds at drone.io (Drone.io Build) Date: Wed, 29 Apr 2015 14:33:49 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 101 Message-ID: <20150429143349.7798.49035@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/101 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4214:7d4a0b78d19b Author : holger krekel Branch : default Message: Merged in hpk42/pytest-patches/testrefactor (pull request #284) -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Wed Apr 29 16:34:56 2015 From: builds at drone.io (Drone.io Build) Date: Wed, 29 Apr 2015 14:34:56 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 102 Message-ID: <20150429143455.85165.26792@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/102 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4214:7d4a0b78d19b Author : holger krekel Branch : default Message: Merged in hpk42/pytest-patches/testrefactor (pull request #284) -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Wed Apr 29 23:38:02 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 29 Apr 2015 21:38:02 -0000 Subject: [Pytest-commit] commit/pytest: 2 new changesets Message-ID: <20150429213802.6940.50058@app07.ash-private.bitbucket.org> 2 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/6e682dd54d0c/ Changeset: 6e682dd54d0c Branch: testrefactor User: flub Date: 2015-04-29 21:36:39+00:00 Summary: closing merged feature branch Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/589ea2984ab7/ Changeset: 589ea2984ab7 Branch: plugtestfix User: flub Date: 2015-04-29 21:36:51+00:00 Summary: closing merged feature branch Affected #: 0 files Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Wed Apr 29 23:39:13 2015 From: builds at drone.io (Drone.io Build) Date: Wed, 29 Apr 2015 21:39:13 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 103 Message-ID: <20150429213913.1917.87407@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/103 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4039:589ea2984ab7 Author : Floris Bruynooghe Branch : plugtestfix Message: closing merged feature branch -------------- next part -------------- An HTML attachment was scrubbed... URL: From issues-reply at bitbucket.org Thu Apr 30 09:04:05 2015 From: issues-reply at bitbucket.org (David MacIver) Date: Thu, 30 Apr 2015 07:04:05 -0000 Subject: [Pytest-commit] Issue #735: Assertion rewriting triggers internal assert in compile.c on debug builds (pytest-dev/pytest) Message-ID: <20150430070405.18914.24406@app04.ash-private.bitbucket.org> New issue 735: Assertion rewriting triggers internal assert in compile.c on debug builds https://bitbucket.org/pytest-dev/pytest/issue/735/assertion-rewriting-triggers-internal David MacIver: When running pytest-2.7.0 on a debug build of Python 3.4.3 against the following code, I get an internal assert triggered in Python: ``` #!python def test_trivial(): assert True ``` The following is the error: ``` $ ~/snakepit/python3.4-debug -m pytest test_rewriting.py --capture=no ============================================= test session starts ============================================= platform linux -- Python 3.4.3 -- py-1.4.26 -- pytest-2.7.0 rootdir: /home/david/projects/hypothesis, inifile: collecting 0 itemspython3.4-debug: Python/compile.c:2733: compiler_nameop: Assertion `PyUnicode_CompareWithASCIIString(name, "None") && PyUnicode_CompareWithASCIIString(name, "True") && PyUnicode_CompareWithASCIIString(name, "False")' failed. Aborted ``` If I switch pytest to --assert=plain the error goes away. It seems likely there's some sort of Python bug underlying this, but I figured you'd want to know about the problem anyway and I don't have a minimization smaller than "all of pytest" so don't feel able to report this to cpython myself. From commits-noreply at bitbucket.org Thu Apr 30 22:11:40 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 30 Apr 2015 20:11:40 -0000 Subject: [Pytest-commit] commit/pytest: flub: Fix collapse false to look at unescaped braces only Message-ID: <20150430201140.3339.63665@app13.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/219483189eb7/ Changeset: 219483189eb7 Branch: pytest-2.7 User: flub Date: 2015-04-30 01:31:12+00:00 Summary: Fix collapse false to look at unescaped braces only Sometimes the repr of an object can contain the "\n{" sequence which is used as a formatting language, so they are escaped to "\\n{". But the collapse-false code needs to look for the real "\n{" token instead of simply "{" as otherwise it may get unbalanced braces from the object's repr (sometimes caused by the collapsing of long reprs by saferepr). Fixes issue #731. Affected #: 3 files diff -r 1fa7f14142010fb83f00dd46dd4dbc5daaeb4910 -r 219483189eb71cca40376c15fb6ffebd492a3d12 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ 2.7.1.dev (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 + explanations. Thanks Carl Meyer for the report and test case. + - fix issue553: properly handling inspect.getsourcelines failures in FixtureLookupError which would lead to to an internal error, obfuscating the original problem. Thanks talljosh for initial diff -r 1fa7f14142010fb83f00dd46dd4dbc5daaeb4910 -r 219483189eb71cca40376c15fb6ffebd492a3d12 _pytest/assertion/util.py --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -45,13 +45,15 @@ if where == -1: break level = 0 + prev_c = explanation[start] for i, c in enumerate(explanation[start:]): - if c == "{": + if prev_c + c == "\n{": level += 1 - elif c == "}": + elif prev_c + c == "\n}": level -= 1 if not level: break + prev_c = c else: raise AssertionError("unbalanced braces: %r" % (explanation,)) end = start + i diff -r 1fa7f14142010fb83f00dd46dd4dbc5daaeb4910 -r 219483189eb71cca40376c15fb6ffebd492a3d12 testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -665,3 +665,24 @@ result.stdout.fnmatch_lines([ "* 1 passed*", ]) + + +def test_issue731(testdir): + testdir.makepyfile(""" + class LongReprWithBraces(object): + def __repr__(self): + return 'LongReprWithBraces({' + ('a' * 80) + '}' + ('a' * 120) + ')' + + def some_method(self): + return False + + def test_long_repr(): + obj = LongReprWithBraces() + assert obj.some_method() + """) + result = testdir.runpytest() + assert 'unbalanced braces' not in result.stdout.str() + + +def test_collapse_false_unbalanced_braces(): + util._collapse_false('some text{ False\n{False = some more text\n}') Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Apr 30 22:13:29 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 30 Apr 2015 20:13:29 -0000 Subject: [Pytest-commit] commit/pytest: flub: Merge fix for issue 731 from pytest-2.7 Message-ID: <20150430201329.24382.91833@app06.ash-private.bitbucket.org> 1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/ce106625d6b5/ Changeset: ce106625d6b5 User: flub Date: 2015-04-30 20:13:03+00:00 Summary: Merge fix for issue 731 from pytest-2.7 Affected #: 3 files diff -r 7d4a0b78d19b985ccca88827129d825151ad494c -r ce106625d6b53f0ff6e9dce8a1cf0c761341b2f4 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,10 @@ 2.7.1.dev (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 + explanations. Thanks Carl Meyer for the report and test case. + - fix issue553: properly handling inspect.getsourcelines failures in FixtureLookupError which would lead to to an internal error, obfuscating the original problem. Thanks talljosh for initial diff -r 7d4a0b78d19b985ccca88827129d825151ad494c -r ce106625d6b53f0ff6e9dce8a1cf0c761341b2f4 _pytest/assertion/util.py --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -45,13 +45,15 @@ if where == -1: break level = 0 + prev_c = explanation[start] for i, c in enumerate(explanation[start:]): - if c == "{": + if prev_c + c == "\n{": level += 1 - elif c == "}": + elif prev_c + c == "\n}": level -= 1 if not level: break + prev_c = c else: raise AssertionError("unbalanced braces: %r" % (explanation,)) end = start + i diff -r 7d4a0b78d19b985ccca88827129d825151ad494c -r ce106625d6b53f0ff6e9dce8a1cf0c761341b2f4 testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -663,3 +663,24 @@ result.stdout.fnmatch_lines([ "* 1 passed*", ]) + + +def test_issue731(testdir): + testdir.makepyfile(""" + class LongReprWithBraces(object): + def __repr__(self): + return 'LongReprWithBraces({' + ('a' * 80) + '}' + ('a' * 120) + ')' + + def some_method(self): + return False + + def test_long_repr(): + obj = LongReprWithBraces() + assert obj.some_method() + """) + result = testdir.runpytest() + assert 'unbalanced braces' not in result.stdout.str() + + +def test_collapse_false_unbalanced_braces(): + util._collapse_false('some text{ False\n{False = some more text\n}') Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From builds at drone.io Thu Apr 30 22:22:24 2015 From: builds at drone.io (Drone.io Build) Date: Thu, 30 Apr 2015 20:22:24 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 104 Message-ID: <20150430202224.25451.93428@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/104 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 3972:219483189eb7 Author : Floris Bruynooghe Branch : pytest-2.7 Message: Fix collapse false to look at unescaped braces only -------------- next part -------------- An HTML attachment was scrubbed... URL: From builds at drone.io Thu Apr 30 22:23:32 2015 From: builds at drone.io (Drone.io Build) Date: Thu, 30 Apr 2015 20:23:32 +0000 Subject: [Pytest-commit] [FAIL] pytest - # 105 Message-ID: <20150430202331.85169.71014@drone.io> Build Failed Build : https://drone.io/bitbucket.org/pytest-dev/pytest/105 Project : https://drone.io/bitbucket.org/pytest-dev/pytest Repository : https://bitbucket.org/pytest-dev/pytest Version : 4218:ce106625d6b5 Author : Floris Bruynooghe Branch : default Message: Merge fix for issue 731 from pytest-2.7 -------------- next part -------------- An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Wed Apr 29 16:32:36 2015 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 29 Apr 2015 14:32:36 -0000 Subject: [Pytest-commit] commit/pytest: 6 new changesets Message-ID: <20150429143236.6128.56084@app05.ash-private.bitbucket.org> 6 new commits in pytest: https://bitbucket.org/pytest-dev/pytest/commits/d7f0b42aa824/ Changeset: d7f0b42aa824 Branch: testrefactor User: hpk42 Date: 2015-04-28 09:54:45+00:00 Summary: - make API between runpytest() and inline_run() more similar - shift a number of tests to become inline_run() tests Affected #: 9 files diff -r 156f6edf7067d395e77fa81742e56833066d3f37 -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -29,17 +29,21 @@ initialization. """ try: - config = _prepareconfig(args, plugins) - except ConftestImportFailure: - e = sys.exc_info()[1] - tw = py.io.TerminalWriter(sys.stderr) - for line in traceback.format_exception(*e.excinfo): - tw.line(line.rstrip(), red=True) - tw.line("ERROR: could not load %s\n" % (e.path), red=True) + try: + config = _prepareconfig(args, plugins) + except ConftestImportFailure as e: + tw = py.io.TerminalWriter(sys.stderr) + for line in traceback.format_exception(*e.excinfo): + tw.line(line.rstrip(), red=True) + tw.line("ERROR: could not load %s\n" % (e.path), red=True) + return 4 + else: + config.pluginmanager.check_pending() + return config.hook.pytest_cmdline_main(config=config) + except UsageError as e: + for msg in e.args: + sys.stderr.write("ERROR: %s\n" %(msg,)) return 4 - else: - config.pluginmanager.check_pending() - return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace main = staticmethod(main) diff -r 156f6edf7067d395e77fa81742e56833066d3f37 -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -83,10 +83,7 @@ initstate = 2 doit(config, session) except pytest.UsageError: - args = sys.exc_info()[1].args - for msg in args: - sys.stderr.write("ERROR: %s\n" %(msg,)) - session.exitstatus = EXIT_USAGEERROR + raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) diff -r 156f6edf7067d395e77fa81742e56833066d3f37 -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -204,6 +204,8 @@ tmptestdir = TmpTestdir(request) return tmptestdir + + rex_outcome = re.compile("(\d+) (\w+)") class RunResult: """The result of running a command. @@ -229,6 +231,8 @@ self.duration = duration def parseoutcomes(self): + """ Return a dictionary of outcomestring->num from parsing + the terminal output that the test process produced.""" for line in reversed(self.outlines): if 'seconds' in line: outcomes = rex_outcome.findall(line) @@ -238,13 +242,16 @@ d[cat] = int(num) return d - def assertoutcome(self, passed=0, skipped=0, failed=0): + def assert_outcomes(self, passed=0, skipped=0, failed=0): + """ assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" d = self.parseoutcomes() assert passed == d.get("passed", 0) assert skipped == d.get("skipped", 0) assert failed == d.get("failed", 0) + class TmpTestdir: """Temporary test directory with tools to test/run py.test itself. @@ -568,12 +575,32 @@ plugins = kwargs.get("plugins") or [] plugins.append(Collect()) ret = pytest.main(list(args), plugins=plugins) - assert len(rec) == 1 - reprec = rec[0] + self.delete_loaded_modules() + if len(rec) == 1: + reprec = rec.pop() + else: + class reprec: + pass reprec.ret = ret - self.delete_loaded_modules() return reprec + def inline_runpytest(self, *args): + """ Return result of running pytest in-process, providing a similar + interface to what self.runpytest() provides. """ + now = time.time() + capture = py.io.StdCaptureFD() + try: + reprec = self.inline_run(*args) + finally: + out, err = capture.reset() + assert out or err + + res = RunResult(reprec.ret, + out.split("\n"), err.split("\n"), + time.time()-now) + res.reprec = reprec + return res + def parseconfig(self, *args): """Return a new py.test Config instance from given commandline args. diff -r 156f6edf7067d395e77fa81742e56833066d3f37 -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -15,7 +15,7 @@ p.pyimport() del py.std.sys.modules['test_whatever'] b.ensure("test_whatever.py") - result = testdir.runpytest() + result = testdir.inline_runpytest() result.stdout.fnmatch_lines([ "*import*mismatch*", "*imported*test_whatever*", @@ -59,7 +59,7 @@ def __init__(self): pass """) - result = testdir.runpytest("-rw") + result = testdir.inline_runpytest("-rw") result.stdout.fnmatch_lines_random(""" WC1*test_class_with_init_warning.py*__init__* """) @@ -69,7 +69,7 @@ class test(object): pass """) - result = testdir.runpytest() + result = testdir.inline_runpytest() result.stdout.fnmatch_lines([ "*collected 0*", ]) @@ -86,7 +86,7 @@ def teardown_class(cls): pass """) - result = testdir.runpytest() + result = testdir.inline_runpytest() result.stdout.fnmatch_lines([ "*1 passed*", ]) @@ -534,7 +534,7 @@ """) testdir.makepyfile("def test_some(): pass") testdir.makepyfile(test_xyz="def test_func(): pass") - result = testdir.runpytest("--collect-only") + result = testdir.inline_runpytest("--collect-only") result.stdout.fnmatch_lines([ "* 0 assert "hello" not in result.stdout.str() - result = testdir.runpytest("-rw") + result = testdir.inline_runpytest("-rw") result.stdout.fnmatch_lines(""" ===*warning summary*=== *WT1*test_warn_on_test_item*:5*hello* diff -r 156f6edf7067d395e77fa81742e56833066d3f37 -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -18,10 +18,8 @@ test_hello.setup = lambda: l.append(1) test_hello.teardown = lambda: l.append(2) """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result = testdir.inline_runpytest(p, '-p', 'nose') + result.assert_outcomes(passed=2) def test_setup_func_with_setup_decorator(): @@ -65,10 +63,8 @@ assert l == [1,2] """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result = testdir.inline_runpytest(p, '-p', 'nose') + result.assert_outcomes(passed=2) def test_nose_setup_func_failure(testdir): @@ -89,7 +85,7 @@ assert l == [1,2] """) - result = testdir.runpytest(p, '-p', 'nose') + result = testdir.inline_runpytest(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*TypeError: ()*" ]) @@ -140,7 +136,7 @@ test_hello.setup = my_setup_partial test_hello.teardown = my_teardown_partial """) - result = testdir.runpytest(p, '-p', 'nose') + result = testdir.inline_runpytest(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*" ]) @@ -207,7 +203,7 @@ #expect.append('setup') eq_(self.called, expect) """) - result = testdir.runpytest(p, '-p', 'nose') + result = testdir.inline_runpytest(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*10 passed*" ]) @@ -238,7 +234,7 @@ assert items[2] == 2 assert 1 not in items """) - result = testdir.runpytest('-p', 'nose') + result = testdir.inline_runpytest('-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*", ]) @@ -260,7 +256,7 @@ def test_world(): assert l == [1] """) - result = testdir.runpytest('-p', 'nose') + result = testdir.inline_runpytest('-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*", ]) @@ -276,7 +272,7 @@ def test_first(self): pass """) - result = testdir.runpytest() + result = testdir.inline_runpytest() result.stdout.fnmatch_lines([ "*1 passed*", ]) @@ -301,8 +297,8 @@ def test_fun(self): pass """) - result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result = testdir.inline_runpytest() + result.assert_outcomes(passed=1) @pytest.mark.skipif("sys.version_info < (2,6)") def test_setup_teardown_linking_issue265(testdir): @@ -327,8 +323,8 @@ """Undoes the setup.""" raise Exception("should not call teardown for skipped tests") ''') - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1, skipped=1) + reprec = testdir.inline_runpytest() + reprec.assert_outcomes(passed=1, skipped=1) def test_SkipTest_during_collection(testdir): @@ -338,8 +334,8 @@ def test_failing(): assert False """) - result = testdir.runpytest(p) - result.assertoutcome(skipped=1) + result = testdir.inline_runpytest(p) + result.assert_outcomes(skipped=1) def test_SkipTest_in_test(testdir): https://bitbucket.org/pytest-dev/pytest/commits/dc1c8c7ea818/ Changeset: dc1c8c7ea818 Branch: testrefactor User: hpk42 Date: 2015-04-28 09:54:46+00:00 Summary: - refine lsof checking - make runpytest() create an inline testing process instead of a subprocess one Affected #: 18 files diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -38,8 +38,11 @@ tw.line("ERROR: could not load %s\n" % (e.path), red=True) return 4 else: - config.pluginmanager.check_pending() - return config.hook.pytest_cmdline_main(config=config) + try: + config.pluginmanager.check_pending() + return config.hook.pytest_cmdline_main(config=config) + finally: + config._ensure_unconfigure() except UsageError as e: for msg in e.args: sys.stderr.write("ERROR: %s\n" %(msg,)) @@ -85,12 +88,18 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_config().pluginmanager - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) + config = get_config() + pluginmanager = config.pluginmanager + try: + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + except BaseException: + config._ensure_unconfigure() + raise + def exclude_pytest_names(name): return not name.startswith(name) or name == "pytest_plugins" or \ @@ -263,7 +272,10 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - self.set_blocked(arg[3:]) + name = arg[3:] + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) else: self.import_plugin(arg) diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,5 +1,6 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ import sys +import traceback import os import codecs import re @@ -287,7 +288,8 @@ break self.tmpdir = tmpdir self.plugins = [] - self._savesyspath = list(sys.path) + self._savesyspath = (list(sys.path), list(sys.meta_path)) + self._savemodulekeys = set(sys.modules) self.chdir() # always chdir self.request.addfinalizer(self.finalize) @@ -303,23 +305,23 @@ has finished. """ - sys.path[:] = self._savesyspath + sys.path[:], sys.meta_path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() self.delete_loaded_modules() def delete_loaded_modules(self): - """Delete modules that have been loaded from tmpdir. + """Delete modules that have been loaded during a test. This allows the interpreter to catch module changes in case the module is re-imported. """ - for name, mod in list(sys.modules.items()): - if mod: - fn = getattr(mod, '__file__', None) - if fn and fn.startswith(str(self.tmpdir)): - del sys.modules[name] + for name in set(sys.modules).difference(self._savemodulekeys): + # it seems zope.interfaces is keeping some state + # (used by twisted related tests) + if name != "zope.interface": + del sys.modules[name] def make_hook_recorder(self, pluginmanager): """Create a new :py:class:`HookRecorder` for a PluginManager.""" @@ -584,16 +586,27 @@ reprec.ret = ret return reprec - def inline_runpytest(self, *args): + def inline_runpytest(self, *args, **kwargs): """ Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides. """ + if kwargs.get("syspathinsert"): + self.syspathinsert() now = time.time() - capture = py.io.StdCaptureFD() + capture = py.io.StdCapture() try: - reprec = self.inline_run(*args) + try: + reprec = self.inline_run(*args) + except SystemExit as e: + class reprec: + ret = e.args[0] + except Exception: + traceback.print_exc() + class reprec: + ret = 3 finally: out, err = capture.reset() - assert out or err + sys.stdout.write(out) + sys.stderr.write(err) res = RunResult(reprec.ret, out.split("\n"), err.split("\n"), @@ -601,6 +614,9 @@ res.reprec = reprec return res + def runpytest(self, *args, **kwargs): + return self.inline_runpytest(*args, **kwargs) + def parseconfig(self, *args): """Return a new py.test Config instance from given commandline args. @@ -822,7 +838,7 @@ command = self._getsysprepend() + command return self.run(sys.executable, "-c", command) - def runpytest(self, *args): + def runpytest_subprocess(self, *args): """Run py.test as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will added diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 doc/en/example/assertion/test_failures.py --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -7,7 +7,7 @@ target = testdir.tmpdir.join(failure_demo.basename) failure_demo.copy(target) failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) - result = testdir.runpytest(target) + result = testdir.runpytest(target, syspathinsert=True) result.stdout.fnmatch_lines([ "*42 failed*" ]) diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -82,7 +82,7 @@ def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) - result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123") + result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ '*1 passed*', @@ -353,7 +353,8 @@ *unrecognized* """) - def test_getsourcelines_error_issue553(self, testdir): + def test_getsourcelines_error_issue553(self, testdir, monkeypatch): + monkeypatch.setattr("inspect.getsourcelines", None) p = testdir.makepyfile(""" def raise_error(obj): raise IOError('source code not available') diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/conftest.py --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,5 +1,6 @@ import pytest import sys +import gc pytest_plugins = "pytester", @@ -17,8 +18,8 @@ def _parse_lsof_output(self, out): def isopen(line): - return line.startswith('f') and ( - "deleted" not in line and 'mem' not in line and "txt" not in line and 'cwd' not in line) + return line.startswith('f') and ("deleted" not in line and + 'mem' not in line and "txt" not in line and 'cwd' not in line) open_files = [] @@ -32,46 +33,49 @@ return open_files + def matching_platform(self): + try: + py.process.cmdexec("lsof -v") + except py.process.cmdexec.Error: + return False + else: + return True + + @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True) + def pytest_runtest_item(self, item): + lines1 = self.get_open_files() + yield + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1]) + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [] + error.append("***** %s FD leakage detected" % len(leaked_files)) + error.extend([str(f) for f in leaked_files]) + error.append("*** Before:") + error.extend([str(f) for f in lines1]) + error.append("*** After:") + error.extend([str(f) for f in lines2]) + error.append(error[0]) + error.append("*** function %s:%s: %s " % item.location) + pytest.fail("\n".join(error), pytrace=False) + def pytest_addoption(parser): parser.addoption('--lsof', action="store_true", dest="lsof", default=False, help=("run FD checks if lsof is available")) -def pytest_runtest_setup(item): - config = item.config - config._basedir = py.path.local() + +def pytest_configure(config): if config.getvalue("lsof"): - try: - config._fd_leak_checker = LsofFdLeakChecker() - config._openfiles = config._fd_leak_checker.get_open_files() - except py.process.cmdexec.Error: - pass + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) -#def pytest_report_header(): -# return "pid: %s" % os.getpid() - -def check_open_files(config): - lines2 = config._fd_leak_checker.get_open_files() - new_fds = set([t[0] for t in lines2]) - set([t[0] for t in config._openfiles]) - open_files = [t for t in lines2 if t[0] in new_fds] - if open_files: - error = [] - error.append("***** %s FD leakage detected" % len(open_files)) - error.extend([str(f) for f in open_files]) - error.append("*** Before:") - error.extend([str(f) for f in config._openfiles]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - raise AssertionError("\n".join(error)) - - at pytest.hookimpl_opts(hookwrapper=True, trylast=True) -def pytest_runtest_teardown(item): - yield - item.config._basedir.chdir() - if hasattr(item.config, '_openfiles'): - check_open_files(item.config) # XXX copied from execnet's conftest.py - needs to be merged winpymap = { diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -626,10 +626,8 @@ """)) sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") - result = testdir.inline_runpytest("-v", "-s") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result = testdir.runpytest("-v", "-s") + result.assert_outcomes(passed=2) def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -99,10 +99,8 @@ sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") sub2.join("test_in_sub2.py").write("def test_2(arg2): pass") - result = testdir.inline_runpytest("-v") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result = testdir.runpytest("-v") + result.assert_outcomes(passed=2) def test_extend_fixture_module_class(self, testdir): testfile = testdir.makepyfile(""" diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -663,7 +663,7 @@ """)) sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") - result = testdir.inline_runpytest("-v", "-s", sub1, sub2, sub1) + result = testdir.runpytest("-v", "-s", sub1, sub2, sub1) result.assert_outcomes(passed=3) def test_generate_same_function_names_issue403(self, testdir): diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_assertion.py --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -451,7 +451,7 @@ x = 3 assert x == 4 """) - result = testdir.inline_runpytest() + result = testdir.runpytest() assert "3 == 4" in result.stdout.str() off_options = (("--no-assert",), ("--nomagic",), @@ -461,7 +461,7 @@ ("--assert=plain", "--nomagic"), ("--assert=plain", "--no-assert", "--nomagic")) for opt in off_options: - result = testdir.runpytest(*opt) + result = testdir.runpytest_subprocess(*opt) assert "3 == 4" not in result.stdout.str() def test_old_assert_mode(testdir): @@ -469,7 +469,7 @@ def test_in_old_mode(): assert "@py_builtins" not in globals() """) - result = testdir.runpytest("--assert=reinterp") + result = testdir.runpytest_subprocess("--assert=reinterp") assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -453,7 +453,7 @@ assert not os.path.exists(__cached__) assert not os.path.exists(os.path.dirname(__cached__))""") monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") - assert testdir.runpytest().ret == 0 + assert testdir.runpytest_subprocess().ret == 0 @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): @@ -615,10 +615,8 @@ testdir.makepyfile(**contents) testdir.maketxtfile(**{'testpkg/resource': "Load me please."}) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '* 1 passed*', - ]) + result = testdir.runpytest_subprocess() + result.assert_outcomes(passed=1) def test_read_pyc(self, tmpdir): """ diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_capture.py --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -121,7 +121,7 @@ print (sys.stdout) print (%s) """ % obj) - result = testdir.runpytest("--capture=%s" % method) + result = testdir.runpytest_subprocess("--capture=%s" % method) result.stdout.fnmatch_lines([ "*1 passed*" ]) @@ -133,7 +133,7 @@ def test_unicode(): print ('b\\u00f6y') """) - result = testdir.runpytest("--capture=%s" % method) + result = testdir.runpytest_subprocess("--capture=%s" % method) result.stdout.fnmatch_lines([ "*1 passed*" ]) @@ -144,7 +144,7 @@ print ("collect %s failure" % 13) import xyz42123 """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*Captured stdout*", "*collect 13 failure*", @@ -165,7 +165,7 @@ print ("in func2") assert 0 """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "setup module*", "setup test_func1*", @@ -188,7 +188,7 @@ def teardown_function(func): print ("in teardown") """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*test_func():*", "*Captured stdout during setup*", @@ -206,7 +206,7 @@ print ("in func2") assert 0 """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) s = result.stdout.str() assert "in func1" not in s assert "in func2" in s @@ -222,7 +222,7 @@ print ("in func1") pass """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ '*teardown_function*', '*Captured stdout*', @@ -240,7 +240,7 @@ def test_func(): pass """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*def teardown_module(mod):*", "*Captured stdout*", @@ -259,7 +259,7 @@ sys.stderr.write(str(2)) raise ValueError """) - result = testdir.runpytest(p1) + result = testdir.runpytest_subprocess(p1) result.stdout.fnmatch_lines([ "*test_capturing_outerr.py .F", "====* FAILURES *====", @@ -282,7 +282,7 @@ logging.basicConfig(stream=stream) stream.close() # to free memory/release resources """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stderr.str().find("atexit") == -1 def test_logging_and_immediate_setupteardown(self, testdir): @@ -301,7 +301,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors show first! @@ -327,7 +327,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors come first @@ -348,7 +348,7 @@ logging.warn("hello432") assert 0 """) - result = testdir.runpytest( + result = testdir.runpytest_subprocess( p, "--traceconfig", "-p", "no:capturelog") assert result.ret != 0 @@ -364,7 +364,7 @@ logging.warn("hello435") """) # make sure that logging is still captured in tests - result = testdir.runpytest("-s", "-p", "no:capturelog") + result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") assert result.ret == 0 result.stderr.fnmatch_lines([ "WARNING*hello435*", @@ -383,7 +383,7 @@ logging.warn("hello433") assert 0 """) - result = testdir.runpytest(p, "-p", "no:capturelog") + result = testdir.runpytest_subprocess(p, "-p", "no:capturelog") assert result.ret != 0 result.stdout.fnmatch_lines([ "WARNING*hello433*", @@ -410,7 +410,7 @@ def test_two(capfd, capsys): pass """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*ERROR*setup*test_one*", "*capsys*capfd*same*time*", @@ -425,7 +425,7 @@ print ("xxx42xxx") assert 0 """ % method) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "xxx42xxx", ]) @@ -447,7 +447,7 @@ def test_hello(capsys, missingarg): pass """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*test_partial_setup_failure*", "*1 error*", @@ -461,7 +461,7 @@ os.write(1, str(42).encode('ascii')) raise KeyboardInterrupt() """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*KeyboardInterrupt*" ]) @@ -474,7 +474,7 @@ def test_log(capsys): logging.error('x') """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) assert 'closed' not in result.stderr.str() @@ -485,7 +485,7 @@ raise ValueError(42) """)) sub1.join("test_mod.py").write("def test_func1(): pass") - result = testdir.runpytest(testdir.tmpdir, '--traceconfig') + result = testdir.runpytest_subprocess(testdir.tmpdir, '--traceconfig') result.stdout.fnmatch_lines([ "*ValueError(42)*", "*1 error*" @@ -500,7 +500,7 @@ def test_hello(capfd): pass """) - result = testdir.runpytest("--capture=no") + result = testdir.runpytest_subprocess("--capture=no") result.stdout.fnmatch_lines([ "*1 skipped*" ]) @@ -512,7 +512,7 @@ print ("hello19") """) testdir.makepyfile("def test_func(): pass") - result = testdir.runpytest() + result = testdir.runpytest_subprocess() assert result.ret == 0 assert 'hello19' not in result.stdout.str() @@ -526,7 +526,7 @@ os.write(1, omg) assert 0 """) - result = testdir.runpytest('--cap=fd') + result = testdir.runpytest_subprocess('--cap=fd') result.stdout.fnmatch_lines(''' *def test_func* *assert 0* @@ -541,7 +541,7 @@ print ("hello19") """) testdir.makepyfile("def test_func(): pass") - result = testdir.runpytest("-vs") + result = testdir.runpytest_subprocess("-vs") assert result.ret == 0 assert 'hello19' in result.stdout.str() @@ -562,7 +562,7 @@ if __name__ == '__main__': test_foo() """) - result = testdir.runpytest('--assert=plain') + result = testdir.runpytest_subprocess('--assert=plain') result.stdout.fnmatch_lines([ '*2 passed*', ]) @@ -885,7 +885,7 @@ os.write(1, "hello\\n".encode("ascii")) assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_x* *assert 0* @@ -936,7 +936,7 @@ cap = StdCaptureFD(out=False, err=False, in_=True) cap.stop_capturing() """) - result = testdir.runpytest("--capture=fd") + result = testdir.runpytest_subprocess("--capture=fd") assert result.ret == 0 assert result.parseoutcomes()['passed'] == 3 @@ -971,7 +971,7 @@ os.write(1, b"hello\\n") assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_capture_again* *assert 0* diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_collection.py --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -296,7 +296,6 @@ subdir.ensure("__init__.py") target = subdir.join(p.basename) p.move(target) - testdir.chdir() subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Session(config=config) @@ -470,7 +469,6 @@ assert col.config is config def test_pkgfile(self, testdir): - testdir.chdir() tmpdir = testdir.tmpdir subdir = tmpdir.join("subdir") x = subdir.ensure("x.py") diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -961,7 +961,7 @@ """) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ "WI1*skipped plugin*skipping1*hello*", @@ -990,7 +990,7 @@ assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) + result = testdir.runpytest(p, syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_doctest.py --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -75,8 +75,6 @@ assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - @pytest.mark.xfail('hasattr(sys, "pypy_version_info")', reason= - "pypy leaks one FD") def test_simple_doctestfile(self, testdir): p = testdir.maketxtfile(test_doc=""" >>> x = 1 diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_genscript.py --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -16,7 +16,6 @@ assert self.script.check() def run(self, anypython, testdir, *args): - testdir.chdir() return testdir._run(anypython, self.script, *args) def test_gen(testdir, anypython, standalone): diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_helpconfig.py --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -53,14 +53,14 @@ ]) def test_debug(testdir, monkeypatch): - result = testdir.runpytest("--debug") + result = testdir.runpytest_subprocess("--debug") assert result.ret == 0 p = testdir.tmpdir.join("pytestdebug.log") assert "pytest_sessionstart" in p.read() def test_PYTEST_DEBUG(testdir, monkeypatch): monkeypatch.setenv("PYTEST_DEBUG", "1") - result = testdir.runpytest() + result = testdir.runpytest_subprocess() assert result.ret == 0 result.stderr.fnmatch_lines([ "*pytest_plugin_registered*", diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_pdb.py --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -260,7 +260,7 @@ def test_pdb_collection_failure_is_shown(self, testdir): p1 = testdir.makepyfile("""xxx """) - result = testdir.runpytest("--pdb", p1) + result = testdir.runpytest_subprocess("--pdb", p1) result.stdout.fnmatch_lines([ "*NameError*xxx*", "*1 error*", diff -r d7f0b42aa8240f6031ca6b45e5a6a8a491060d2b -r dc1c8c7ea818036073356c01ed74608f0671fc73 testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -203,7 +203,6 @@ def test_plugin_specify(testdir): - testdir.chdir() pytest.raises(ImportError, """ testdir.parseconfig("-p", "nqweotexistent") """) https://bitbucket.org/pytest-dev/pytest/commits/390fe4614bd4/ Changeset: 390fe4614bd4 Branch: testrefactor User: hpk42 Date: 2015-04-28 09:54:53+00:00 Summary: streamline pytester API majorly: - integrate conftest into pytester plugin - introduce runpytest() to either call runpytest_inline (default) or runpytest_subprocess (python -m pytest) - move testdir.inline_runsource1 to pdb tests - strike some unneccessary methods. - a new section "writing plugins" and some better pytester docs Affected #: 17 files diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,14 @@ implementations. Use the ``hookwrapper`` mechanism instead already introduced with pytest-2.7. +- speed up pytest's own test suite considerably by using inprocess + tests by default (testrun can be modified with --runpytest=subprocess + to create subprocesses in many places instead). The main + APIs to run pytest in a test is "runpytest()" or "runpytest_subprocess" + and "runpytest_inprocess" if you need a particular way of running + the test. In all cases you get back a RunResult but the inprocess + one will also have a "reprec" attribute with the recorded events/reports. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,4 +1,5 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ +import gc import sys import traceback import os @@ -16,6 +17,136 @@ from _pytest.main import Session, EXIT_OK + +def pytest_addoption(parser): + # group = parser.getgroup("pytester", "pytester (self-tests) options") + parser.addoption('--lsof', + action="store_true", dest="lsof", default=False, + help=("run FD checks if lsof is available")) + + parser.addoption('--runpytest', default="inprocess", dest="runpytest", + choices=("inprocess", "subprocess", ), + help=("run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method")) + + +def pytest_configure(config): + # This might be called multiple times. Only take the first. + global _pytest_fullpath + try: + _pytest_fullpath + except NameError: + _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) + _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") + + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + + +class LsofFdLeakChecker(object): + def get_open_files(self): + out = self._exec_lsof() + open_files = self._parse_lsof_output(out) + return open_files + + def _exec_lsof(self): + pid = os.getpid() + return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) + + def _parse_lsof_output(self, out): + def isopen(line): + return line.startswith('f') and ("deleted" not in line and + 'mem' not in line and "txt" not in line and 'cwd' not in line) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split('\0') + fd = fields[0][1:] + filename = fields[1][1:] + if filename.startswith('/'): + open_files.append((fd, filename)) + + return open_files + + def matching_platform(self): + try: + py.process.cmdexec("lsof -v") + except py.process.cmdexec.Error: + return False + else: + return True + + @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True) + def pytest_runtest_item(self, item): + lines1 = self.get_open_files() + yield + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1]) + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [] + error.append("***** %s FD leakage detected" % len(leaked_files)) + error.extend([str(f) for f in leaked_files]) + error.append("*** Before:") + error.extend([str(f) for f in lines1]) + error.append("*** After:") + error.extend([str(f) for f in lines2]) + error.append(error[0]) + error.append("*** function %s:%s: %s " % item.location) + pytest.fail("\n".join(error), pytrace=False) + + +# XXX copied from execnet's conftest.py - needs to be merged +winpymap = { + 'python2.7': r'C:\Python27\python.exe', + 'python2.6': r'C:\Python26\python.exe', + 'python3.1': r'C:\Python31\python.exe', + 'python3.2': r'C:\Python32\python.exe', + 'python3.3': r'C:\Python33\python.exe', + 'python3.4': r'C:\Python34\python.exe', + 'python3.5': r'C:\Python35\python.exe', +} + +def getexecutable(name, cache={}): + try: + return cache[name] + except KeyError: + executable = py.path.local.sysfind(name) + if executable: + if name == "jython": + import subprocess + popen = subprocess.Popen([str(executable), "--version"], + universal_newlines=True, stderr=subprocess.PIPE) + out, err = popen.communicate() + if not err or "2.5" not in err: + executable = None + if "2.5.2" in err: + executable = None # http://bugs.jython.org/issue1790 + cache[name] = executable + return executable + + at pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", + 'pypy', 'pypy3']) +def anypython(request): + name = request.param + executable = getexecutable(name) + if executable is None: + if sys.platform == "win32": + executable = winpymap.get(name, None) + if executable: + executable = py.path.local(executable) + if executable.check(): + return executable + pytest.skip("no suitable %s found" % (name,)) + return executable + # used at least by pytest-xdist plugin @pytest.fixture def _pytest(request): @@ -40,23 +171,6 @@ return [x for x in l if x[0] != "_"] -def pytest_addoption(parser): - group = parser.getgroup("pylib") - group.addoption('--no-tools-on-path', - action="store_true", dest="notoolsonpath", default=False, - help=("discover tools on PATH instead of going through py.cmdline.") - ) - -def pytest_configure(config): - # This might be called multiple times. Only take the first. - global _pytest_fullpath - try: - _pytest_fullpath - except NameError: - _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) - _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") - - class ParsedCall: def __init__(self, name, kwargs): self.__dict__.update(kwargs) @@ -202,7 +316,7 @@ return LineMatcher def pytest_funcarg__testdir(request): - tmptestdir = TmpTestdir(request) + tmptestdir = Testdir(request) return tmptestdir @@ -216,10 +330,10 @@ :ret: The return value. :outlines: List of lines captured from stdout. :errlines: List of lines captures from stderr. - :stdout: LineMatcher of stdout, use ``stdout.str()`` to + :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` method. - :stderrr: LineMatcher of stderr. + :stderrr: :py:class:`LineMatcher` of stderr. :duration: Duration in seconds. """ @@ -253,7 +367,7 @@ -class TmpTestdir: +class Testdir: """Temporary test directory with tools to test/run py.test itself. This is based on the ``tmpdir`` fixture but provides a number of @@ -276,7 +390,6 @@ def __init__(self, request): self.request = request - self.Config = request.config.__class__ # XXX remove duplication with tmpdir plugin basetmp = request.config._tmpdirhandler.ensuretemp("testdir") name = request.function.__name__ @@ -292,9 +405,14 @@ self._savemodulekeys = set(sys.modules) self.chdir() # always chdir self.request.addfinalizer(self.finalize) + method = self.request.config.getoption("--runpytest") + if method == "inprocess": + self._runpytest_method = self.runpytest_inprocess + elif method == "subprocess": + self._runpytest_method = self.runpytest_subprocess def __repr__(self): - return "" % (self.tmpdir,) + return "" % (self.tmpdir,) def finalize(self): """Clean up global state artifacts. @@ -315,7 +433,6 @@ This allows the interpreter to catch module changes in case the module is re-imported. - """ for name in set(sys.modules).difference(self._savemodulekeys): # it seems zope.interfaces is keeping some state @@ -512,43 +629,19 @@ l = list(cmdlineargs) + [p] return self.inline_run(*l) - def inline_runsource1(self, *args): - """Run a test module in process using ``pytest.main()``. - - This behaves exactly like :py:meth:`inline_runsource` and - takes identical arguments. However the return value is a list - of the reports created by the pytest_runtest_logreport hook - during the run. - - """ - args = list(args) - source = args.pop() - p = self.makepyfile(source) - l = list(args) + [p] - reprec = self.inline_run(*l) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3, reports # setup/call/teardown - return reports[1] - def inline_genitems(self, *args): """Run ``pytest.main(['--collectonly'])`` in-process. Retuns a tuple of the collected items and a :py:class:`HookRecorder` instance. - """ - return self.inprocess_run(list(args) + ['--collectonly']) - - def inprocess_run(self, args, plugins=()): - """Run ``pytest.main()`` in-process, return Items and a HookRecorder. - This runs the :py:func:`pytest.main` function to run all of py.test inside the test process itself like :py:meth:`inline_run`. However the return value is a tuple of the collection items and a :py:class:`HookRecorder` instance. """ - rec = self.inline_run(*args, plugins=plugins) + rec = self.inline_run("--collect-only", *args) items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec @@ -586,7 +679,7 @@ reprec.ret = ret return reprec - def inline_runpytest(self, *args, **kwargs): + def runpytest_inprocess(self, *args, **kwargs): """ Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides. """ if kwargs.get("syspathinsert"): @@ -615,7 +708,11 @@ return res def runpytest(self, *args, **kwargs): - return self.inline_runpytest(*args, **kwargs) + """ Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`RunResult`. + + """ + return self._runpytest_method(*args, **kwargs) def parseconfig(self, *args): """Return a new py.test Config instance from given commandline args. @@ -788,57 +885,23 @@ except UnicodeEncodeError: print("couldn't print to %s because of encoding" % (fp,)) - def runpybin(self, scriptname, *args): - """Run a py.* tool with arguments. + def _getpytestargs(self): + # we cannot use "(sys.executable,script)" + # because on windows the script is e.g. a py.test.exe + return (sys.executable, _pytest_fullpath,) # noqa - This can realy only be used to run py.test, you probably want - :py:meth:`runpytest` instead. + def runpython(self, script): + """Run a python script using sys.executable as interpreter. Returns a :py:class:`RunResult`. - """ - fullargs = self._getpybinargs(scriptname) + args - return self.run(*fullargs) - - def _getpybinargs(self, scriptname): - if not self.request.config.getvalue("notoolsonpath"): - # XXX we rely on script referring to the correct environment - # we cannot use "(sys.executable,script)" - # because on windows the script is e.g. a py.test.exe - return (sys.executable, _pytest_fullpath,) # noqa - else: - pytest.skip("cannot run %r with --no-tools-on-path" % scriptname) - - def runpython(self, script, prepend=True): - """Run a python script. - - If ``prepend`` is True then the directory from which the py - package has been imported will be prepended to sys.path. - - Returns a :py:class:`RunResult`. - - """ - # XXX The prepend feature is probably not very useful since the - # split of py and pytest. - if prepend: - s = self._getsysprepend() - if s: - script.write(s + "\n" + script.read()) return self.run(sys.executable, script) - def _getsysprepend(self): - if self.request.config.getvalue("notoolsonpath"): - s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) - else: - s = "" - return s - def runpython_c(self, command): """Run python -c "command", return a :py:class:`RunResult`.""" - command = self._getsysprepend() + command return self.run(sys.executable, "-c", command) - def runpytest_subprocess(self, *args): + def runpytest_subprocess(self, *args, **kwargs): """Run py.test as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will added @@ -863,7 +926,8 @@ plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args - return self.runpybin("py.test", *args) + args = self._getpytestargs() + args + return self.run(*args) def spawn_pytest(self, string, expect_timeout=10.0): """Run py.test using pexpect. @@ -874,10 +938,8 @@ The pexpect child is returned. """ - if self.request.config.getvalue("notoolsonpath"): - pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") - invoke = " ".join(map(str, self._getpybinargs("py.test"))) + invoke = " ".join(map(str, self._getpytestargs())) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee doc/en/writing_plugins.txt --- a/doc/en/writing_plugins.txt +++ b/doc/en/writing_plugins.txt @@ -186,12 +186,44 @@ If you want to look at the names of existing plugins, use the ``--traceconfig`` option. +Testing plugins +--------------- + +pytest comes with some facilities that you can enable for testing your +plugin. Given that you have an installed plugin you can enable the +:py:class:`testdir <_pytest.pytester.Testdir>` fixture via specifying a +command line option to include the pytester plugin (``-p pytester``) or +by putting ``pytest_plugins = pytester`` into your test or +``conftest.py`` file. You then will have a ``testdir`` fixure which you +can use like this:: + + # content of test_myplugin.py + + pytest_plugins = pytester # to get testdir fixture + + def test_myplugin(testdir): + testdir.makepyfile(""" + def test_example(): + pass + """) + result = testdir.runpytest("--verbose") + result.fnmatch_lines(""" + test_example* + """) + +Note that by default ``testdir.runpytest()`` will perform a pytest +in-process. You can pass the command line option ``--runpytest=subprocess`` +to have it happen in a subprocess. + +Also see the :py:class:`RunResult <_pytest.pytester.RunResult>` for more +methods of the result object that you get from a call to ``runpytest``. .. _`writinghooks`: Writing hook functions ====================== + .. _validation: hook function validation and execution @@ -493,3 +525,13 @@ .. autoclass:: _pytest.core.CallOutcome() :members: +.. currentmodule:: _pytest.pytester + +.. autoclass:: Testdir() + :members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile + +.. autoclass:: RunResult() + :members: + +.. autoclass:: LineMatcher() + :members: diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -203,7 +203,7 @@ os.chdir(os.path.dirname(os.getcwd())) print (py.log) """)) - result = testdir.runpython(p, prepend=False) + result = testdir.runpython(p) assert not result.ret def test_issue109_sibling_conftests_not_loaded(self, testdir): diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/conftest.py --- a/testing/conftest.py +++ /dev/null @@ -1,122 +0,0 @@ -import pytest -import sys -import gc - -pytest_plugins = "pytester", - -import os, py - -class LsofFdLeakChecker(object): - def get_open_files(self): - out = self._exec_lsof() - open_files = self._parse_lsof_output(out) - return open_files - - def _exec_lsof(self): - pid = os.getpid() - return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) - - def _parse_lsof_output(self, out): - def isopen(line): - return line.startswith('f') and ("deleted" not in line and - 'mem' not in line and "txt" not in line and 'cwd' not in line) - - open_files = [] - - for line in out.split("\n"): - if isopen(line): - fields = line.split('\0') - fd = fields[0][1:] - filename = fields[1][1:] - if filename.startswith('/'): - open_files.append((fd, filename)) - - return open_files - - def matching_platform(self): - try: - py.process.cmdexec("lsof -v") - except py.process.cmdexec.Error: - return False - else: - return True - - @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True) - def pytest_runtest_item(self, item): - lines1 = self.get_open_files() - yield - if hasattr(sys, "pypy_version_info"): - gc.collect() - lines2 = self.get_open_files() - - new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1]) - leaked_files = [t for t in lines2 if t[0] in new_fds] - if leaked_files: - error = [] - error.append("***** %s FD leakage detected" % len(leaked_files)) - error.extend([str(f) for f in leaked_files]) - error.append("*** Before:") - error.extend([str(f) for f in lines1]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - error.append("*** function %s:%s: %s " % item.location) - pytest.fail("\n".join(error), pytrace=False) - - -def pytest_addoption(parser): - parser.addoption('--lsof', - action="store_true", dest="lsof", default=False, - help=("run FD checks if lsof is available")) - - -def pytest_configure(config): - if config.getvalue("lsof"): - checker = LsofFdLeakChecker() - if checker.matching_platform(): - config.pluginmanager.register(checker) - - -# XXX copied from execnet's conftest.py - needs to be merged -winpymap = { - 'python2.7': r'C:\Python27\python.exe', - 'python2.6': r'C:\Python26\python.exe', - 'python3.1': r'C:\Python31\python.exe', - 'python3.2': r'C:\Python32\python.exe', - 'python3.3': r'C:\Python33\python.exe', - 'python3.4': r'C:\Python34\python.exe', - 'python3.5': r'C:\Python35\python.exe', -} - -def getexecutable(name, cache={}): - try: - return cache[name] - except KeyError: - executable = py.path.local.sysfind(name) - if executable: - if name == "jython": - import subprocess - popen = subprocess.Popen([str(executable), "--version"], - universal_newlines=True, stderr=subprocess.PIPE) - out, err = popen.communicate() - if not err or "2.5" not in err: - executable = None - if "2.5.2" in err: - executable = None # http://bugs.jython.org/issue1790 - cache[name] = executable - return executable - - at pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", - 'pypy', 'pypy3']) -def anypython(request): - name = request.param - executable = getexecutable(name) - if executable is None: - if sys.platform == "win32": - executable = winpymap.get(name, None) - if executable: - executable = py.path.local(executable) - if executable.check(): - return executable - pytest.skip("no suitable %s found" % (name,)) - return executable diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -15,7 +15,7 @@ p.pyimport() del py.std.sys.modules['test_whatever'] b.ensure("test_whatever.py") - result = testdir.inline_runpytest() + result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines([ "*import*mismatch*", "*imported*test_whatever*", @@ -59,7 +59,7 @@ def __init__(self): pass """) - result = testdir.inline_runpytest("-rw") + result = testdir.runpytest_inprocess("-rw") result.stdout.fnmatch_lines_random(""" WC1*test_class_with_init_warning.py*__init__* """) @@ -69,7 +69,7 @@ class test(object): pass """) - result = testdir.inline_runpytest() + result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines([ "*collected 0*", ]) @@ -86,7 +86,7 @@ def teardown_class(cls): pass """) - result = testdir.inline_runpytest() + result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines([ "*1 passed*", ]) @@ -534,7 +534,7 @@ """) testdir.makepyfile("def test_some(): pass") testdir.makepyfile(test_xyz="def test_func(): pass") - result = testdir.inline_runpytest("--collect-only") + result = testdir.runpytest_inprocess("--collect-only") result.stdout.fnmatch_lines([ "* 0 assert "hello" not in result.stdout.str() - result = testdir.inline_runpytest("-rw") + result = testdir.runpytest_inprocess("-rw") result.stdout.fnmatch_lines(""" ===*warning summary*=== *WT1*test_warn_on_test_item*:5*hello* diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/test_doctest.py --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,5 @@ from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile -import py, pytest +import py class TestDoctests: diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -18,7 +18,7 @@ test_hello.setup = lambda: l.append(1) test_hello.teardown = lambda: l.append(2) """) - result = testdir.inline_runpytest(p, '-p', 'nose') + result = testdir.runpytest_inprocess(p, '-p', 'nose') result.assert_outcomes(passed=2) @@ -63,7 +63,7 @@ assert l == [1,2] """) - result = testdir.inline_runpytest(p, '-p', 'nose') + result = testdir.runpytest_inprocess(p, '-p', 'nose') result.assert_outcomes(passed=2) @@ -85,7 +85,7 @@ assert l == [1,2] """) - result = testdir.inline_runpytest(p, '-p', 'nose') + result = testdir.runpytest_inprocess(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*TypeError: ()*" ]) @@ -136,7 +136,7 @@ test_hello.setup = my_setup_partial test_hello.teardown = my_teardown_partial """) - result = testdir.inline_runpytest(p, '-p', 'nose') + result = testdir.runpytest_inprocess(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*" ]) @@ -203,7 +203,7 @@ #expect.append('setup') eq_(self.called, expect) """) - result = testdir.inline_runpytest(p, '-p', 'nose') + result = testdir.runpytest_inprocess(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*10 passed*" ]) @@ -234,7 +234,7 @@ assert items[2] == 2 assert 1 not in items """) - result = testdir.inline_runpytest('-p', 'nose') + result = testdir.runpytest_inprocess('-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*", ]) @@ -256,7 +256,7 @@ def test_world(): assert l == [1] """) - result = testdir.inline_runpytest('-p', 'nose') + result = testdir.runpytest_inprocess('-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*", ]) @@ -272,7 +272,7 @@ def test_first(self): pass """) - result = testdir.inline_runpytest() + result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines([ "*1 passed*", ]) @@ -297,7 +297,7 @@ def test_fun(self): pass """) - result = testdir.inline_runpytest() + result = testdir.runpytest_inprocess() result.assert_outcomes(passed=1) @pytest.mark.skipif("sys.version_info < (2,6)") @@ -323,7 +323,7 @@ """Undoes the setup.""" raise Exception("should not call teardown for skipped tests") ''') - reprec = testdir.inline_runpytest() + reprec = testdir.runpytest_inprocess() reprec.assert_outcomes(passed=1, skipped=1) @@ -334,7 +334,7 @@ def test_failing(): assert False """) - result = testdir.inline_runpytest(p) + result = testdir.runpytest_inprocess(p) result.assert_outcomes(skipped=1) diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/test_pdb.py --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,6 +2,13 @@ import py import sys +def runpdb_and_get_report(testdir, source): + p = testdir.makepyfile(source) + result = testdir.runpytest_inprocess("--pdb", p) + reports = result.reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3, reports # setup/call/teardown + return reports[1] + class TestPDB: def pytest_funcarg__pdblist(self, request): @@ -14,7 +21,7 @@ return pdblist def test_pdb_on_fail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ def test_func(): assert 0 """) @@ -24,7 +31,7 @@ assert tb[-1].name == "test_func" def test_pdb_on_xfail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest @pytest.mark.xfail def test_func(): @@ -34,7 +41,7 @@ assert not pdblist def test_pdb_on_skip(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest def test_func(): pytest.skip("hello") @@ -43,7 +50,7 @@ assert len(pdblist) == 0 def test_pdb_on_BdbQuit(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import bdb def test_func(): raise bdb.BdbQuit diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -69,9 +69,7 @@ assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) + result.assert_outcomes(passed=1) def make_holder(): @@ -114,16 +112,6 @@ unichr = chr testdir.makepyfile(unichr(0xfffd)) -def test_inprocess_plugins(testdir): - class Plugin(object): - configured = False - def pytest_configure(self, config): - self.configured = True - plugin = Plugin() - testdir.inprocess_run([], [plugin]) - - assert plugin.configured - def test_inline_run_clean_modules(testdir): test_mod = testdir.makepyfile("def test_foo(): assert True") result = testdir.inline_run(str(test_mod)) diff -r dc1c8c7ea818036073356c01ed74608f0671fc73 -r 390fe4614bd4dc3cbf6506a0304a2cd11c6c16ee tox.ini --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,py27-trial,py33-trial,doctesting,py27-cxfreeze +envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,{py27,py33}-trial,py27-subprocess,doctesting,py27-cxfreeze [testenv] changedir=testing @@ -9,6 +9,15 @@ nose mock +[testenv:py27-subprocess] +changedir=. +basepython=python2.7 +deps=pytest-xdist + mock + nose +commands= + py.test -n3 -rfsxX --runpytest=subprocess {posargs:testing} + [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 @@ -136,7 +145,7 @@ minversion=2.0 plugins=pytester #--pyargs --doctest-modules --ignore=.tox -addopts= -rxsX +addopts= -rxsX -p pytester rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance https://bitbucket.org/pytest-dev/pytest/commits/8e7a43d5ae0d/ Changeset: 8e7a43d5ae0d Branch: testrefactor User: hpk42 Date: 2015-04-28 09:56:57+00:00 Summary: merge Affected #: 0 files https://bitbucket.org/pytest-dev/pytest/commits/bcbb77bff338/ Changeset: bcbb77bff338 Branch: testrefactor User: hpk42 Date: 2015-04-28 10:05:08+00:00 Summary: use runpytest() instead of runpytest_inprocess if a test can run as subprocess as well Affected #: 5 files diff -r 8e7a43d5ae0d5bcbc23466091db846187291e4d5 -r bcbb77bff338c564d2b28500d0444932a04ad35a testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -15,7 +15,7 @@ p.pyimport() del py.std.sys.modules['test_whatever'] b.ensure("test_whatever.py") - result = testdir.runpytest_inprocess() + result = testdir.runpytest() result.stdout.fnmatch_lines([ "*import*mismatch*", "*imported*test_whatever*", @@ -59,7 +59,7 @@ def __init__(self): pass """) - result = testdir.runpytest_inprocess("-rw") + result = testdir.runpytest("-rw") result.stdout.fnmatch_lines_random(""" WC1*test_class_with_init_warning.py*__init__* """) @@ -69,7 +69,7 @@ class test(object): pass """) - result = testdir.runpytest_inprocess() + result = testdir.runpytest() result.stdout.fnmatch_lines([ "*collected 0*", ]) @@ -86,7 +86,7 @@ def teardown_class(cls): pass """) - result = testdir.runpytest_inprocess() + result = testdir.runpytest() result.stdout.fnmatch_lines([ "*1 passed*", ]) @@ -534,7 +534,7 @@ """) testdir.makepyfile("def test_some(): pass") testdir.makepyfile(test_xyz="def test_func(): pass") - result = testdir.runpytest_inprocess("--collect-only") + result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines([ "* 0 assert "hello" not in result.stdout.str() - result = testdir.runpytest_inprocess("-rw") + result = testdir.runpytest("-rw") result.stdout.fnmatch_lines(""" ===*warning summary*=== *WT1*test_warn_on_test_item*:5*hello* diff -r 8e7a43d5ae0d5bcbc23466091db846187291e4d5 -r bcbb77bff338c564d2b28500d0444932a04ad35a testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -18,7 +18,7 @@ test_hello.setup = lambda: l.append(1) test_hello.teardown = lambda: l.append(2) """) - result = testdir.runpytest_inprocess(p, '-p', 'nose') + result = testdir.runpytest(p, '-p', 'nose') result.assert_outcomes(passed=2) @@ -63,7 +63,7 @@ assert l == [1,2] """) - result = testdir.runpytest_inprocess(p, '-p', 'nose') + result = testdir.runpytest(p, '-p', 'nose') result.assert_outcomes(passed=2) @@ -85,7 +85,7 @@ assert l == [1,2] """) - result = testdir.runpytest_inprocess(p, '-p', 'nose') + result = testdir.runpytest(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*TypeError: ()*" ]) @@ -136,7 +136,7 @@ test_hello.setup = my_setup_partial test_hello.teardown = my_teardown_partial """) - result = testdir.runpytest_inprocess(p, '-p', 'nose') + result = testdir.runpytest(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*" ]) @@ -203,7 +203,7 @@ #expect.append('setup') eq_(self.called, expect) """) - result = testdir.runpytest_inprocess(p, '-p', 'nose') + result = testdir.runpytest(p, '-p', 'nose') result.stdout.fnmatch_lines([ "*10 passed*" ]) @@ -234,7 +234,7 @@ assert items[2] == 2 assert 1 not in items """) - result = testdir.runpytest_inprocess('-p', 'nose') + result = testdir.runpytest('-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*", ]) @@ -256,7 +256,7 @@ def test_world(): assert l == [1] """) - result = testdir.runpytest_inprocess('-p', 'nose') + result = testdir.runpytest('-p', 'nose') result.stdout.fnmatch_lines([ "*2 passed*", ]) @@ -272,7 +272,7 @@ def test_first(self): pass """) - result = testdir.runpytest_inprocess() + result = testdir.runpytest() result.stdout.fnmatch_lines([ "*1 passed*", ]) @@ -297,7 +297,7 @@ def test_fun(self): pass """) - result = testdir.runpytest_inprocess() + result = testdir.runpytest() result.assert_outcomes(passed=1) @pytest.mark.skipif("sys.version_info < (2,6)") @@ -323,7 +323,7 @@ """Undoes the setup.""" raise Exception("should not call teardown for skipped tests") ''') - reprec = testdir.runpytest_inprocess() + reprec = testdir.runpytest() reprec.assert_outcomes(passed=1, skipped=1) @@ -334,7 +334,7 @@ def test_failing(): assert False """) - result = testdir.runpytest_inprocess(p) + result = testdir.runpytest(p) result.assert_outcomes(skipped=1) https://bitbucket.org/pytest-dev/pytest/commits/7d4a0b78d19b/ Changeset: 7d4a0b78d19b User: hpk42 Date: 2015-04-29 14:32:28+00:00 Summary: Merged in hpk42/pytest-patches/testrefactor (pull request #284) majorly refactor pytester and speed/streamline tests Affected #: 25 files diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,14 @@ implementations. Use the ``hookwrapper`` mechanism instead already introduced with pytest-2.7. +- speed up pytest's own test suite considerably by using inprocess + tests by default (testrun can be modified with --runpytest=subprocess + to create subprocesses in many places instead). The main + APIs to run pytest in a test is "runpytest()" or "runpytest_subprocess" + and "runpytest_inprocess" if you need a particular way of running + the test. In all cases you get back a RunResult but the inprocess + one will also have a "reprec" attribute with the recorded events/reports. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -29,17 +29,24 @@ initialization. """ try: - config = _prepareconfig(args, plugins) - except ConftestImportFailure: - e = sys.exc_info()[1] - tw = py.io.TerminalWriter(sys.stderr) - for line in traceback.format_exception(*e.excinfo): - tw.line(line.rstrip(), red=True) - tw.line("ERROR: could not load %s\n" % (e.path), red=True) + try: + config = _prepareconfig(args, plugins) + except ConftestImportFailure as e: + tw = py.io.TerminalWriter(sys.stderr) + for line in traceback.format_exception(*e.excinfo): + tw.line(line.rstrip(), red=True) + tw.line("ERROR: could not load %s\n" % (e.path), red=True) + return 4 + else: + try: + config.pluginmanager.check_pending() + return config.hook.pytest_cmdline_main(config=config) + finally: + config._ensure_unconfigure() + except UsageError as e: + for msg in e.args: + sys.stderr.write("ERROR: %s\n" %(msg,)) return 4 - else: - config.pluginmanager.check_pending() - return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace main = staticmethod(main) @@ -81,12 +88,18 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_config().pluginmanager - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) + config = get_config() + pluginmanager = config.pluginmanager + try: + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + except BaseException: + config._ensure_unconfigure() + raise + def exclude_pytest_names(name): return not name.startswith(name) or name == "pytest_plugins" or \ @@ -259,7 +272,10 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - self.set_blocked(arg[3:]) + name = arg[3:] + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) else: self.import_plugin(arg) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -83,10 +83,7 @@ initstate = 2 doit(config, session) except pytest.UsageError: - args = sys.exc_info()[1].args - for msg in args: - sys.stderr.write("ERROR: %s\n" %(msg,)) - session.exitstatus = EXIT_USAGEERROR + raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,5 +1,7 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ +import gc import sys +import traceback import os import codecs import re @@ -15,6 +17,136 @@ from _pytest.main import Session, EXIT_OK + +def pytest_addoption(parser): + # group = parser.getgroup("pytester", "pytester (self-tests) options") + parser.addoption('--lsof', + action="store_true", dest="lsof", default=False, + help=("run FD checks if lsof is available")) + + parser.addoption('--runpytest', default="inprocess", dest="runpytest", + choices=("inprocess", "subprocess", ), + help=("run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method")) + + +def pytest_configure(config): + # This might be called multiple times. Only take the first. + global _pytest_fullpath + try: + _pytest_fullpath + except NameError: + _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) + _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") + + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + + +class LsofFdLeakChecker(object): + def get_open_files(self): + out = self._exec_lsof() + open_files = self._parse_lsof_output(out) + return open_files + + def _exec_lsof(self): + pid = os.getpid() + return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) + + def _parse_lsof_output(self, out): + def isopen(line): + return line.startswith('f') and ("deleted" not in line and + 'mem' not in line and "txt" not in line and 'cwd' not in line) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split('\0') + fd = fields[0][1:] + filename = fields[1][1:] + if filename.startswith('/'): + open_files.append((fd, filename)) + + return open_files + + def matching_platform(self): + try: + py.process.cmdexec("lsof -v") + except py.process.cmdexec.Error: + return False + else: + return True + + @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True) + def pytest_runtest_item(self, item): + lines1 = self.get_open_files() + yield + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1]) + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [] + error.append("***** %s FD leakage detected" % len(leaked_files)) + error.extend([str(f) for f in leaked_files]) + error.append("*** Before:") + error.extend([str(f) for f in lines1]) + error.append("*** After:") + error.extend([str(f) for f in lines2]) + error.append(error[0]) + error.append("*** function %s:%s: %s " % item.location) + pytest.fail("\n".join(error), pytrace=False) + + +# XXX copied from execnet's conftest.py - needs to be merged +winpymap = { + 'python2.7': r'C:\Python27\python.exe', + 'python2.6': r'C:\Python26\python.exe', + 'python3.1': r'C:\Python31\python.exe', + 'python3.2': r'C:\Python32\python.exe', + 'python3.3': r'C:\Python33\python.exe', + 'python3.4': r'C:\Python34\python.exe', + 'python3.5': r'C:\Python35\python.exe', +} + +def getexecutable(name, cache={}): + try: + return cache[name] + except KeyError: + executable = py.path.local.sysfind(name) + if executable: + if name == "jython": + import subprocess + popen = subprocess.Popen([str(executable), "--version"], + universal_newlines=True, stderr=subprocess.PIPE) + out, err = popen.communicate() + if not err or "2.5" not in err: + executable = None + if "2.5.2" in err: + executable = None # http://bugs.jython.org/issue1790 + cache[name] = executable + return executable + + at pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", + 'pypy', 'pypy3']) +def anypython(request): + name = request.param + executable = getexecutable(name) + if executable is None: + if sys.platform == "win32": + executable = winpymap.get(name, None) + if executable: + executable = py.path.local(executable) + if executable.check(): + return executable + pytest.skip("no suitable %s found" % (name,)) + return executable + # used at least by pytest-xdist plugin @pytest.fixture def _pytest(request): @@ -39,23 +171,6 @@ return [x for x in l if x[0] != "_"] -def pytest_addoption(parser): - group = parser.getgroup("pylib") - group.addoption('--no-tools-on-path', - action="store_true", dest="notoolsonpath", default=False, - help=("discover tools on PATH instead of going through py.cmdline.") - ) - -def pytest_configure(config): - # This might be called multiple times. Only take the first. - global _pytest_fullpath - try: - _pytest_fullpath - except NameError: - _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) - _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") - - class ParsedCall: def __init__(self, name, kwargs): self.__dict__.update(kwargs) @@ -201,9 +316,11 @@ return LineMatcher def pytest_funcarg__testdir(request): - tmptestdir = TmpTestdir(request) + tmptestdir = Testdir(request) return tmptestdir + + rex_outcome = re.compile("(\d+) (\w+)") class RunResult: """The result of running a command. @@ -213,10 +330,10 @@ :ret: The return value. :outlines: List of lines captured from stdout. :errlines: List of lines captures from stderr. - :stdout: LineMatcher of stdout, use ``stdout.str()`` to + :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` method. - :stderrr: LineMatcher of stderr. + :stderrr: :py:class:`LineMatcher` of stderr. :duration: Duration in seconds. """ @@ -229,6 +346,8 @@ self.duration = duration def parseoutcomes(self): + """ Return a dictionary of outcomestring->num from parsing + the terminal output that the test process produced.""" for line in reversed(self.outlines): if 'seconds' in line: outcomes = rex_outcome.findall(line) @@ -238,14 +357,17 @@ d[cat] = int(num) return d - def assertoutcome(self, passed=0, skipped=0, failed=0): + def assert_outcomes(self, passed=0, skipped=0, failed=0): + """ assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" d = self.parseoutcomes() assert passed == d.get("passed", 0) assert skipped == d.get("skipped", 0) assert failed == d.get("failed", 0) -class TmpTestdir: + +class Testdir: """Temporary test directory with tools to test/run py.test itself. This is based on the ``tmpdir`` fixture but provides a number of @@ -268,7 +390,6 @@ def __init__(self, request): self.request = request - self.Config = request.config.__class__ # XXX remove duplication with tmpdir plugin basetmp = request.config._tmpdirhandler.ensuretemp("testdir") name = request.function.__name__ @@ -280,12 +401,18 @@ break self.tmpdir = tmpdir self.plugins = [] - self._savesyspath = list(sys.path) + self._savesyspath = (list(sys.path), list(sys.meta_path)) + self._savemodulekeys = set(sys.modules) self.chdir() # always chdir self.request.addfinalizer(self.finalize) + method = self.request.config.getoption("--runpytest") + if method == "inprocess": + self._runpytest_method = self.runpytest_inprocess + elif method == "subprocess": + self._runpytest_method = self.runpytest_subprocess def __repr__(self): - return "" % (self.tmpdir,) + return "" % (self.tmpdir,) def finalize(self): """Clean up global state artifacts. @@ -296,23 +423,22 @@ has finished. """ - sys.path[:] = self._savesyspath + sys.path[:], sys.meta_path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() self.delete_loaded_modules() def delete_loaded_modules(self): - """Delete modules that have been loaded from tmpdir. + """Delete modules that have been loaded during a test. This allows the interpreter to catch module changes in case the module is re-imported. - """ - for name, mod in list(sys.modules.items()): - if mod: - fn = getattr(mod, '__file__', None) - if fn and fn.startswith(str(self.tmpdir)): - del sys.modules[name] + for name in set(sys.modules).difference(self._savemodulekeys): + # it seems zope.interfaces is keeping some state + # (used by twisted related tests) + if name != "zope.interface": + del sys.modules[name] def make_hook_recorder(self, pluginmanager): """Create a new :py:class:`HookRecorder` for a PluginManager.""" @@ -503,43 +629,19 @@ l = list(cmdlineargs) + [p] return self.inline_run(*l) - def inline_runsource1(self, *args): - """Run a test module in process using ``pytest.main()``. - - This behaves exactly like :py:meth:`inline_runsource` and - takes identical arguments. However the return value is a list - of the reports created by the pytest_runtest_logreport hook - during the run. - - """ - args = list(args) - source = args.pop() - p = self.makepyfile(source) - l = list(args) + [p] - reprec = self.inline_run(*l) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3, reports # setup/call/teardown - return reports[1] - def inline_genitems(self, *args): """Run ``pytest.main(['--collectonly'])`` in-process. Retuns a tuple of the collected items and a :py:class:`HookRecorder` instance. - """ - return self.inprocess_run(list(args) + ['--collectonly']) - - def inprocess_run(self, args, plugins=()): - """Run ``pytest.main()`` in-process, return Items and a HookRecorder. - This runs the :py:func:`pytest.main` function to run all of py.test inside the test process itself like :py:meth:`inline_run`. However the return value is a tuple of the collection items and a :py:class:`HookRecorder` instance. """ - rec = self.inline_run(*args, plugins=plugins) + rec = self.inline_run("--collect-only", *args) items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec @@ -568,12 +670,50 @@ plugins = kwargs.get("plugins") or [] plugins.append(Collect()) ret = pytest.main(list(args), plugins=plugins) - assert len(rec) == 1 - reprec = rec[0] + self.delete_loaded_modules() + if len(rec) == 1: + reprec = rec.pop() + else: + class reprec: + pass reprec.ret = ret - self.delete_loaded_modules() return reprec + def runpytest_inprocess(self, *args, **kwargs): + """ Return result of running pytest in-process, providing a similar + interface to what self.runpytest() provides. """ + if kwargs.get("syspathinsert"): + self.syspathinsert() + now = time.time() + capture = py.io.StdCapture() + try: + try: + reprec = self.inline_run(*args) + except SystemExit as e: + class reprec: + ret = e.args[0] + except Exception: + traceback.print_exc() + class reprec: + ret = 3 + finally: + out, err = capture.reset() + sys.stdout.write(out) + sys.stderr.write(err) + + res = RunResult(reprec.ret, + out.split("\n"), err.split("\n"), + time.time()-now) + res.reprec = reprec + return res + + def runpytest(self, *args, **kwargs): + """ Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`RunResult`. + + """ + return self._runpytest_method(*args, **kwargs) + def parseconfig(self, *args): """Return a new py.test Config instance from given commandline args. @@ -745,57 +885,23 @@ except UnicodeEncodeError: print("couldn't print to %s because of encoding" % (fp,)) - def runpybin(self, scriptname, *args): - """Run a py.* tool with arguments. + def _getpytestargs(self): + # we cannot use "(sys.executable,script)" + # because on windows the script is e.g. a py.test.exe + return (sys.executable, _pytest_fullpath,) # noqa - This can realy only be used to run py.test, you probably want - :py:meth:`runpytest` instead. + def runpython(self, script): + """Run a python script using sys.executable as interpreter. Returns a :py:class:`RunResult`. - """ - fullargs = self._getpybinargs(scriptname) + args - return self.run(*fullargs) - - def _getpybinargs(self, scriptname): - if not self.request.config.getvalue("notoolsonpath"): - # XXX we rely on script referring to the correct environment - # we cannot use "(sys.executable,script)" - # because on windows the script is e.g. a py.test.exe - return (sys.executable, _pytest_fullpath,) # noqa - else: - pytest.skip("cannot run %r with --no-tools-on-path" % scriptname) - - def runpython(self, script, prepend=True): - """Run a python script. - - If ``prepend`` is True then the directory from which the py - package has been imported will be prepended to sys.path. - - Returns a :py:class:`RunResult`. - - """ - # XXX The prepend feature is probably not very useful since the - # split of py and pytest. - if prepend: - s = self._getsysprepend() - if s: - script.write(s + "\n" + script.read()) return self.run(sys.executable, script) - def _getsysprepend(self): - if self.request.config.getvalue("notoolsonpath"): - s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) - else: - s = "" - return s - def runpython_c(self, command): """Run python -c "command", return a :py:class:`RunResult`.""" - command = self._getsysprepend() + command return self.run(sys.executable, "-c", command) - def runpytest(self, *args): + def runpytest_subprocess(self, *args, **kwargs): """Run py.test as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will added @@ -820,7 +926,8 @@ plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args - return self.runpybin("py.test", *args) + args = self._getpytestargs() + args + return self.run(*args) def spawn_pytest(self, string, expect_timeout=10.0): """Run py.test using pexpect. @@ -831,10 +938,8 @@ The pexpect child is returned. """ - if self.request.config.getvalue("notoolsonpath"): - pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") - invoke = " ".join(map(str, self._getpybinargs("py.test"))) + invoke = " ".join(map(str, self._getpytestargs())) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c doc/en/example/assertion/test_failures.py --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -7,7 +7,7 @@ target = testdir.tmpdir.join(failure_demo.basename) failure_demo.copy(target) failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) - result = testdir.runpytest(target) + result = testdir.runpytest(target, syspathinsert=True) result.stdout.fnmatch_lines([ "*42 failed*" ]) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c doc/en/writing_plugins.txt --- a/doc/en/writing_plugins.txt +++ b/doc/en/writing_plugins.txt @@ -186,12 +186,44 @@ If you want to look at the names of existing plugins, use the ``--traceconfig`` option. +Testing plugins +--------------- + +pytest comes with some facilities that you can enable for testing your +plugin. Given that you have an installed plugin you can enable the +:py:class:`testdir <_pytest.pytester.Testdir>` fixture via specifying a +command line option to include the pytester plugin (``-p pytester``) or +by putting ``pytest_plugins = pytester`` into your test or +``conftest.py`` file. You then will have a ``testdir`` fixure which you +can use like this:: + + # content of test_myplugin.py + + pytest_plugins = pytester # to get testdir fixture + + def test_myplugin(testdir): + testdir.makepyfile(""" + def test_example(): + pass + """) + result = testdir.runpytest("--verbose") + result.fnmatch_lines(""" + test_example* + """) + +Note that by default ``testdir.runpytest()`` will perform a pytest +in-process. You can pass the command line option ``--runpytest=subprocess`` +to have it happen in a subprocess. + +Also see the :py:class:`RunResult <_pytest.pytester.RunResult>` for more +methods of the result object that you get from a call to ``runpytest``. .. _`writinghooks`: Writing hook functions ====================== + .. _validation: hook function validation and execution @@ -493,3 +525,13 @@ .. autoclass:: _pytest.core.CallOutcome() :members: +.. currentmodule:: _pytest.pytester + +.. autoclass:: Testdir() + :members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile + +.. autoclass:: RunResult() + :members: + +.. autoclass:: LineMatcher() + :members: diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -82,7 +82,7 @@ def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) - result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123") + result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ '*1 passed*', @@ -203,7 +203,7 @@ os.chdir(os.path.dirname(os.getcwd())) print (py.log) """)) - result = testdir.runpython(p, prepend=False) + result = testdir.runpython(p) assert not result.ret def test_issue109_sibling_conftests_not_loaded(self, testdir): @@ -353,7 +353,8 @@ *unrecognized* """) - def test_getsourcelines_error_issue553(self, testdir): + def test_getsourcelines_error_issue553(self, testdir, monkeypatch): + monkeypatch.setattr("inspect.getsourcelines", None) p = testdir.makepyfile(""" def raise_error(obj): raise IOError('source code not available') diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/conftest.py --- a/testing/conftest.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -import sys - -pytest_plugins = "pytester", - -import os, py - -class LsofFdLeakChecker(object): - def get_open_files(self): - out = self._exec_lsof() - open_files = self._parse_lsof_output(out) - return open_files - - def _exec_lsof(self): - pid = os.getpid() - return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) - - def _parse_lsof_output(self, out): - def isopen(line): - return line.startswith('f') and ( - "deleted" not in line and 'mem' not in line and "txt" not in line and 'cwd' not in line) - - open_files = [] - - for line in out.split("\n"): - if isopen(line): - fields = line.split('\0') - fd = fields[0][1:] - filename = fields[1][1:] - if filename.startswith('/'): - open_files.append((fd, filename)) - - return open_files - - -def pytest_addoption(parser): - parser.addoption('--lsof', - action="store_true", dest="lsof", default=False, - help=("run FD checks if lsof is available")) - -def pytest_runtest_setup(item): - config = item.config - config._basedir = py.path.local() - if config.getvalue("lsof"): - try: - config._fd_leak_checker = LsofFdLeakChecker() - config._openfiles = config._fd_leak_checker.get_open_files() - except py.process.cmdexec.Error: - pass - -#def pytest_report_header(): -# return "pid: %s" % os.getpid() - -def check_open_files(config): - lines2 = config._fd_leak_checker.get_open_files() - new_fds = set([t[0] for t in lines2]) - set([t[0] for t in config._openfiles]) - open_files = [t for t in lines2 if t[0] in new_fds] - if open_files: - error = [] - error.append("***** %s FD leakage detected" % len(open_files)) - error.extend([str(f) for f in open_files]) - error.append("*** Before:") - error.extend([str(f) for f in config._openfiles]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - raise AssertionError("\n".join(error)) - - at pytest.hookimpl_opts(hookwrapper=True, trylast=True) -def pytest_runtest_teardown(item): - yield - item.config._basedir.chdir() - if hasattr(item.config, '_openfiles'): - check_open_files(item.config) - -# XXX copied from execnet's conftest.py - needs to be merged -winpymap = { - 'python2.7': r'C:\Python27\python.exe', - 'python2.6': r'C:\Python26\python.exe', - 'python3.1': r'C:\Python31\python.exe', - 'python3.2': r'C:\Python32\python.exe', - 'python3.3': r'C:\Python33\python.exe', - 'python3.4': r'C:\Python34\python.exe', - 'python3.5': r'C:\Python35\python.exe', -} - -def getexecutable(name, cache={}): - try: - return cache[name] - except KeyError: - executable = py.path.local.sysfind(name) - if executable: - if name == "jython": - import subprocess - popen = subprocess.Popen([str(executable), "--version"], - universal_newlines=True, stderr=subprocess.PIPE) - out, err = popen.communicate() - if not err or "2.5" not in err: - executable = None - if "2.5.2" in err: - executable = None # http://bugs.jython.org/issue1790 - cache[name] = executable - return executable - - at pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", - 'pypy', 'pypy3']) -def anypython(request): - name = request.param - executable = getexecutable(name) - if executable is None: - if sys.platform == "win32": - executable = winpymap.get(name, None) - if executable: - executable = py.path.local(executable) - if executable.check(): - return executable - pytest.skip("no suitable %s found" % (name,)) - return executable diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -627,9 +627,7 @@ sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") result = testdir.runpytest("-v", "-s") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -100,9 +100,7 @@ sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") sub2.join("test_in_sub2.py").write("def test_2(arg2): pass") result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_extend_fixture_module_class(self, testdir): testfile = testdir.makepyfile(""" diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -292,9 +292,7 @@ """) result = testdir.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*6 fail*", - ]) + result.assert_outcomes(failed=6) def test_parametrize_CSV(self, testdir): testdir.makepyfile(""" @@ -375,7 +373,7 @@ assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") - result.assertoutcome(passed=2) + result.assert_outcomes(passed=2) def test_addcall_with_two_funcargs_generators(self, testdir): testdir.makeconftest(""" @@ -430,9 +428,7 @@ pass """) result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 pass*", - ]) + result.assert_outcomes(passed=1) def test_generate_plugin_and_module(self, testdir): @@ -506,9 +502,7 @@ self.val = 1 """) result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 pass*", - ]) + result.assert_outcomes(passed=1) def test_parametrize_functional2(self, testdir): testdir.makepyfile(""" @@ -653,8 +647,8 @@ def test_function(): pass """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=1) def test_generate_tests_only_done_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") @@ -670,9 +664,7 @@ sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") result = testdir.runpytest("-v", "-s", sub1, sub2, sub1) - result.stdout.fnmatch_lines([ - "*3 passed*" - ]) + result.assert_outcomes(passed=3) def test_generate_same_function_names_issue403(self, testdir): testdir.makepyfile(""" @@ -687,8 +679,8 @@ test_x = make_tests() test_y = make_tests() """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=4) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=4) @pytest.mark.issue463 def test_parameterize_misspelling(self, testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_assertion.py --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -461,7 +461,7 @@ ("--assert=plain", "--nomagic"), ("--assert=plain", "--no-assert", "--nomagic")) for opt in off_options: - result = testdir.runpytest(*opt) + result = testdir.runpytest_subprocess(*opt) assert "3 == 4" not in result.stdout.str() def test_old_assert_mode(testdir): @@ -469,7 +469,7 @@ def test_in_old_mode(): assert "@py_builtins" not in globals() """) - result = testdir.runpytest("--assert=reinterp") + result = testdir.runpytest_subprocess("--assert=reinterp") assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -453,7 +453,7 @@ assert not os.path.exists(__cached__) assert not os.path.exists(os.path.dirname(__cached__))""") monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") - assert testdir.runpytest().ret == 0 + assert testdir.runpytest_subprocess().ret == 0 @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): @@ -468,12 +468,12 @@ tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - assert testdir.runpybin("py.test", tmp).ret == 0 + assert testdir.runpytest_subprocess(tmp).ret == 0 tagged = "test_pyc_vs_pyo." + PYTEST_TAG assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.undo() monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - assert testdir.runpybin("py.test", tmp).ret == 1 + assert testdir.runpytest_subprocess(tmp).ret == 1 assert tagged + ".pyc" in os.listdir("__pycache__") def test_package(self, testdir): @@ -615,10 +615,8 @@ testdir.makepyfile(**contents) testdir.maketxtfile(**{'testpkg/resource': "Load me please."}) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '* 1 passed*', - ]) + result = testdir.runpytest_subprocess() + result.assert_outcomes(passed=1) def test_read_pyc(self, tmpdir): """ diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_capture.py --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -282,7 +282,7 @@ logging.basicConfig(stream=stream) stream.close() # to free memory/release resources """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stderr.str().find("atexit") == -1 def test_logging_and_immediate_setupteardown(self, testdir): @@ -301,7 +301,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors show first! @@ -327,7 +327,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors come first @@ -348,7 +348,7 @@ logging.warn("hello432") assert 0 """) - result = testdir.runpytest( + result = testdir.runpytest_subprocess( p, "--traceconfig", "-p", "no:capturelog") assert result.ret != 0 @@ -364,7 +364,7 @@ logging.warn("hello435") """) # make sure that logging is still captured in tests - result = testdir.runpytest("-s", "-p", "no:capturelog") + result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") assert result.ret == 0 result.stderr.fnmatch_lines([ "WARNING*hello435*", @@ -383,7 +383,7 @@ logging.warn("hello433") assert 0 """) - result = testdir.runpytest(p, "-p", "no:capturelog") + result = testdir.runpytest_subprocess(p, "-p", "no:capturelog") assert result.ret != 0 result.stdout.fnmatch_lines([ "WARNING*hello433*", @@ -461,7 +461,7 @@ os.write(1, str(42).encode('ascii')) raise KeyboardInterrupt() """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*KeyboardInterrupt*" ]) @@ -474,7 +474,7 @@ def test_log(capsys): logging.error('x') """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) assert 'closed' not in result.stderr.str() @@ -500,7 +500,7 @@ def test_hello(capfd): pass """) - result = testdir.runpytest("--capture=no") + result = testdir.runpytest_subprocess("--capture=no") result.stdout.fnmatch_lines([ "*1 skipped*" ]) @@ -563,9 +563,7 @@ test_foo() """) result = testdir.runpytest('--assert=plain') - result.stdout.fnmatch_lines([ - '*2 passed*', - ]) + result.assert_outcomes(passed=2) class TestTextIO: @@ -885,7 +883,7 @@ os.write(1, "hello\\n".encode("ascii")) assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_x* *assert 0* @@ -936,7 +934,7 @@ cap = StdCaptureFD(out=False, err=False, in_=True) cap.stop_capturing() """) - result = testdir.runpytest("--capture=fd") + result = testdir.runpytest_subprocess("--capture=fd") assert result.ret == 0 assert result.parseoutcomes()['passed'] == 3 @@ -971,7 +969,7 @@ os.write(1, b"hello\\n") assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_capture_again* *assert 0* diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_collection.py --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -296,7 +296,6 @@ subdir.ensure("__init__.py") target = subdir.join(p.basename) p.move(target) - testdir.chdir() subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Session(config=config) @@ -313,7 +312,7 @@ def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - # XXX migrate to inline_genitems? (see below) + # XXX migrate to collectonly? (see below) config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Session(config) @@ -470,7 +469,6 @@ assert col.config is config def test_pkgfile(self, testdir): - testdir.chdir() tmpdir = testdir.tmpdir subdir = tmpdir.join("subdir") x = subdir.ensure("x.py") diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -75,7 +75,7 @@ [pytest] addopts = --qwe """) - result = testdir.runpytest("--confcutdir=.") + result = testdir.inline_run("--confcutdir=.") assert result.ret == 0 class TestConfigCmdlineParsing: diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -961,7 +961,7 @@ """) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ "WI1*skipped plugin*skipping1*hello*", @@ -990,7 +990,7 @@ assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) + result = testdir.runpytest(p, syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_doctest.py --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,5 @@ from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile -import py, pytest +import py class TestDoctests: @@ -75,8 +75,6 @@ assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - @pytest.mark.xfail('hasattr(sys, "pypy_version_info")', reason= - "pypy leaks one FD") def test_simple_doctestfile(self, testdir): p = testdir.maketxtfile(test_doc=""" >>> x = 1 diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_genscript.py --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -16,7 +16,6 @@ assert self.script.check() def run(self, anypython, testdir, *args): - testdir.chdir() return testdir._run(anypython, self.script, *args) def test_gen(testdir, anypython, standalone): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_helpconfig.py --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -53,14 +53,14 @@ ]) def test_debug(testdir, monkeypatch): - result = testdir.runpytest("--debug") + result = testdir.runpytest_subprocess("--debug") assert result.ret == 0 p = testdir.tmpdir.join("pytestdebug.log") assert "pytest_sessionstart" in p.read() def test_PYTEST_DEBUG(testdir, monkeypatch): monkeypatch.setenv("PYTEST_DEBUG", "1") - result = testdir.runpytest() + result = testdir.runpytest_subprocess() assert result.ret == 0 result.stderr.fnmatch_lines([ "*pytest_plugin_registered*", diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -19,9 +19,7 @@ test_hello.teardown = lambda: l.append(2) """) result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_setup_func_with_setup_decorator(): @@ -66,9 +64,7 @@ """) result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_nose_setup_func_failure(testdir): @@ -302,7 +298,7 @@ pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result.assert_outcomes(passed=1) @pytest.mark.skipif("sys.version_info < (2,6)") def test_setup_teardown_linking_issue265(testdir): @@ -327,8 +323,8 @@ """Undoes the setup.""" raise Exception("should not call teardown for skipped tests") ''') - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1, skipped=1) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=1, skipped=1) def test_SkipTest_during_collection(testdir): @@ -339,7 +335,7 @@ assert False """) result = testdir.runpytest(p) - result.assertoutcome(skipped=1) + result.assert_outcomes(skipped=1) def test_SkipTest_in_test(testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_pdb.py --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,6 +2,13 @@ import py import sys +def runpdb_and_get_report(testdir, source): + p = testdir.makepyfile(source) + result = testdir.runpytest_inprocess("--pdb", p) + reports = result.reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3, reports # setup/call/teardown + return reports[1] + class TestPDB: def pytest_funcarg__pdblist(self, request): @@ -14,7 +21,7 @@ return pdblist def test_pdb_on_fail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ def test_func(): assert 0 """) @@ -24,7 +31,7 @@ assert tb[-1].name == "test_func" def test_pdb_on_xfail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest @pytest.mark.xfail def test_func(): @@ -34,7 +41,7 @@ assert not pdblist def test_pdb_on_skip(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest def test_func(): pytest.skip("hello") @@ -43,7 +50,7 @@ assert len(pdblist) == 0 def test_pdb_on_BdbQuit(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import bdb def test_func(): raise bdb.BdbQuit @@ -260,7 +267,7 @@ def test_pdb_collection_failure_is_shown(self, testdir): p1 = testdir.makepyfile("""xxx """) - result = testdir.runpytest("--pdb", p1) + result = testdir.runpytest_subprocess("--pdb", p1) result.stdout.fnmatch_lines([ "*NameError*xxx*", "*1 error*", diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -69,9 +69,7 @@ assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) + result.assert_outcomes(passed=1) def make_holder(): @@ -114,16 +112,6 @@ unichr = chr testdir.makepyfile(unichr(0xfffd)) -def test_inprocess_plugins(testdir): - class Plugin(object): - configured = False - def pytest_configure(self, config): - self.configured = True - plugin = Plugin() - testdir.inprocess_run([], [plugin]) - - assert plugin.configured - def test_inline_run_clean_modules(testdir): test_mod = testdir.makepyfile("def test_foo(): assert True") result = testdir.inline_run(str(test_mod)) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -203,7 +203,6 @@ def test_plugin_specify(testdir): - testdir.chdir() pytest.raises(ImportError, """ testdir.parseconfig("-p", "nqweotexistent") """) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c tox.ini --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,py27-trial,py33-trial,doctesting,py27-cxfreeze +envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,{py27,py33}-trial,py27-subprocess,doctesting,py27-cxfreeze [testenv] changedir=testing @@ -9,6 +9,15 @@ nose mock +[testenv:py27-subprocess] +changedir=. +basepython=python2.7 +deps=pytest-xdist + mock + nose +commands= + py.test -n3 -rfsxX --runpytest=subprocess {posargs:testing} + [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 @@ -136,7 +145,7 @@ minversion=2.0 plugins=pytester #--pyargs --doctest-modules --ignore=.tox -addopts= -rxsX +addopts= -rxsX -p pytester rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.