Merge branch 'pytest-dev'

# Conflicts:
#	AUTHORS
This commit is contained in:
elizabeth 2015-08-24 22:55:11 +03:00
commit e67d66a5d4
149 changed files with 1334 additions and 427 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
CHANGELOG merge=union

5
.gitignore vendored
View File

@ -17,6 +17,11 @@ include/
*.orig *.orig
*~ *~
.eggs/
# this file is managed by setuptools_scm
_pytest/__init__.py
doc/*/_build doc/*/_build
build/ build/
dist/ dist/

View File

@ -28,7 +28,7 @@ env:
- TESTENV=py35 - TESTENV=py35
- TESTENV=pypy - TESTENV=pypy
script: tox --recreate -i ALL=https://devpi.net/hpk/dev/ -e $TESTENV script: tox --recreate -e $TESTENV
notifications: notifications:
irc: irc:

View File

@ -3,6 +3,7 @@ merlinux GmbH, Germany, office at merlinux eu
Contributors include:: Contributors include::
Abhijeet Kasurde
Anatoly Bubenkoff Anatoly Bubenkoff
Andreas Zeidler Andreas Zeidler
Andy Freeland Andy Freeland
@ -14,6 +15,7 @@ Bob Ippolito
Brian Dorsey Brian Dorsey
Brian Okken Brian Okken
Brianna Laugher Brianna Laugher
Bruno Oliveira
Carl Friedrich Bolz Carl Friedrich Bolz
Charles Cloud Charles Cloud
Chris Lamb Chris Lamb
@ -24,11 +26,12 @@ Daniel Grana
Daniel Nuri Daniel Nuri
Dave Hunt Dave Hunt
David Mohr David Mohr
Edison Gustavo Muenz
Eduardo Schettino Eduardo Schettino
Elizaveta Shashkova Elizaveta Shashkova
Eric Hunsberger
Eric Siegerman Eric Siegerman
Florian Bruhin Florian Bruhin
Edison Gustavo Muenz
Floris Bruynooghe Floris Bruynooghe
Graham Horler Graham Horler
Grig Gheorghiu Grig Gheorghiu
@ -46,6 +49,7 @@ Maciek Fijalkowski
Maho Maho
Marc Schlaich Marc Schlaich
Mark Abramowitz Mark Abramowitz
Markus Unterwaditzer
Martijn Faassen Martijn Faassen
Nicolas Delaby Nicolas Delaby
Pieter Mulder Pieter Mulder
@ -58,3 +62,4 @@ Samuele Pedroni
Tom Viner Tom Viner
Trevor Bekolay Trevor Bekolay
Wouter van Ackooy Wouter van Ackooy
David Díaz-Barquero

View File

@ -1,6 +1,40 @@
2.8.0.dev (compared to 2.7.X) 2.8.0.dev (compared to 2.7.X)
----------------------------- -----------------------------
- Fix #562: @nose.tools.istest now fully respected.
- Fix issue736: Fix a bug where fixture params would be discarded when combined
with parametrization markers.
Thanks to Markus Unterwaditzer for the PR.
- fix issue710: introduce ALLOW_UNICODE doctest option: when enabled, the
``u`` prefix is stripped from unicode strings in expected doctest output. This
allows doctests which use unicode to run in Python 2 and 3 unchanged.
Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR.
- parametrize now also generates meaningful test IDs for enum, regex and class
objects (as opposed to class instances).
Thanks to Florian Bruhin for the PR.
- Add 'warns' to assert that warnings are thrown (like 'raises').
Thanks to Eric Hunsberger for the PR.
- Fix issue683: Do not apply an already applied mark. Thanks ojake for the PR.
- Deal with capturing failures better so fewer exceptions get lost to
/dev/null. Thanks David Szotten for the PR.
- fix issue730: deprecate and warn about the --genscript option.
Thanks Ronny Pfannschmidt for the report and Christian Pommranz for the PR.
- fix issue751: multiple parametrize with ids bug if it parametrizes class with
two or more test methods. Thanks Sergey Chipiga for reporting and Jan
Bednarik for PR.
- fix issue82: avoid loading conftest files from setup.cfg/pytest.ini/tox.ini
files and upwards by default (--confcutdir can still be set to override this).
Thanks Bruno Oliveira for the PR.
- fix issue768: docstrings found in python modules were not setting up session - fix issue768: docstrings found in python modules were not setting up session
fixtures. Thanks Jason R. Coombs for reporting and Bruno Oliveira for the PR. fixtures. Thanks Jason R. Coombs for reporting and Bruno Oliveira for the PR.
@ -11,7 +45,7 @@
deprecated. deprecated.
Thanks Bruno Oliveira for the PR. Thanks Bruno Oliveira for the PR.
- fix issue 808: pytest's internal assertion rewrite hook now implements the - fix issue808: pytest's internal assertion rewrite hook now implements the
optional PEP302 get_data API so tests can access data files next to them. optional PEP302 get_data API so tests can access data files next to them.
Thanks xmo-odoo for request and example and Bruno Oliveira for Thanks xmo-odoo for request and example and Bruno Oliveira for
the PR. the PR.
@ -24,6 +58,15 @@
- Summary bar now is colored yellow for warning - Summary bar now is colored yellow for warning
situations such as: all tests either were skipped or xpass/xfailed, situations such as: all tests either were skipped or xpass/xfailed,
or no tests were run at all (this is a partial fix for issue500). or no tests were run at all (this is a partial fix for issue500).
- fix issue812: pytest now exits with status code 5 in situations where no
tests were run at all, such as the directory given in the command line does
not contain any tests or as result of a command line option filters
all out all tests (-k for example).
Thanks Eric Siegerman (issue812) and Bruno Oliveira for the PR.
- Summary bar now is colored yellow for warning
situations such as: all tests either were skipped or xpass/xfailed,
or no tests were run at all (related to issue500).
Thanks Eric Siegerman. Thanks Eric Siegerman.
- New `testpaths` ini option: list of directories to search for tests - New `testpaths` ini option: list of directories to search for tests
@ -103,6 +146,11 @@
- fix issue714: add ability to apply indirect=True parameter on particular argnames. - fix issue714: add ability to apply indirect=True parameter on particular argnames.
- fix issue890: changed extension of all documentation files from ``txt`` to
``rst``. Thanks to Abhijeet for the PR.
- issue951: add new record_xml_property fixture, that supports logging
additional information on xml output. Thanks David Diaz for the PR.
2.7.3 (compared to 2.7.2) 2.7.3 (compared to 2.7.2)
----------------------------- -----------------------------

View File

@ -21,7 +21,7 @@ in repositories living under:
- `the pytest-dev bitbucket team <https://bitbucket.org/pytest-dev>`_ - `the pytest-dev bitbucket team <https://bitbucket.org/pytest-dev>`_
All pytest-dev team members have write access to all contained All pytest-dev Contributors team members have write access to all contained
repositories. pytest core and plugins are generally developed repositories. pytest core and plugins are generally developed
using `pull requests`_ to respective repositories. using `pull requests`_ to respective repositories.
@ -46,9 +46,9 @@ the following:
If no contributor strongly objects and two agree, the repo will be If no contributor strongly objects and two agree, the repo will be
transferred to the ``pytest-dev`` organisation and you'll become a transferred to the ``pytest-dev`` organisation and you'll become a
member of the ``pytest-dev`` team, with commit rights to all projects. member of the ``pytest-dev Contributors`` team, with commit rights
We recommend that each plugin has at least three people who have the to all projects. We recommend that each plugin has at least three
right to release to pypi. people who have the right to release to pypi.
.. _reportbugs: .. _reportbugs:
@ -66,6 +66,10 @@ If you are reporting a bug, please include:
installed libraries and pytest version. installed libraries and pytest version.
* Detailed steps to reproduce the bug. * Detailed steps to reproduce the bug.
If you can write a demonstration test that currently fails but should pass (xfail),
that is a very useful commit to make as well, even if you can't find how
to fix the bug yet.
.. _submitfeedback: .. _submitfeedback:
Submit feedback for developers Submit feedback for developers
@ -93,6 +97,8 @@ https://github.com/pytest-dev/pytest/labels/bug
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. :ref:`Talk <contact>` to developers to find out how you can fix specific bugs.
Don't forget to check the issue trackers of your favourite plugins, too!
.. _writeplugins: .. _writeplugins:
Implement features Implement features
@ -111,10 +117,14 @@ Write documentation
pytest could always use more documentation. What exactly is needed? pytest could always use more documentation. What exactly is needed?
* More complementary documentation. Have you perhaps found something unclear? * More complementary documentation. Have you perhaps found something unclear?
* Documentation translations. We currently have English and Japanese versions. * Documentation translations. We currently have only English.
* Docstrings. There's never too much of them. * Docstrings. There's never too much of them.
* Blog posts, articles and such -- they're all very appreciated. * Blog posts, articles and such -- they're all very appreciated.
You can also edit documentation files directly in the Github web interface
without needing to make a fork and local copy. This can be convenient for
small fixes.
.. _`pull requests`: .. _`pull requests`:
.. _pull-requests: .. _pull-requests:
@ -181,9 +191,13 @@ but here is a simple overview:
$ git commit -a -m "<commit message>" $ git commit -a -m "<commit message>"
$ git push -u $ git push -u
Make sure you add a CHANGELOG message, and add yourself to AUTHORS. If you
are unsure about either of these steps, submit your pull request and we'll
help you fix it up.
#. Finally, submit a pull request through the GitHub website: #. Finally, submit a pull request through the GitHub website:
.. image:: img/pullrequest.png .. image:: doc/en/img/pullrequest.png
:width: 700px :width: 700px
:align: center :align: center

View File

@ -1,58 +1,87 @@
How to release pytest
How to release pytest (draft)
-------------------------------------------- --------------------------------------------
1. bump version numbers in _pytest/__init__.py (setup.py reads it) Note: this assumes you have already registered on pypi.
2. check and finalize CHANGELOG 1. Bump version numbers in _pytest/__init__.py (setup.py reads it)
3. write doc/en/announce/release-VERSION.txt and include 2. Check and finalize CHANGELOG
3. Write doc/en/announce/release-VERSION.txt and include
it in doc/en/announce/index.txt it in doc/en/announce/index.txt
4. use devpi for uploading a release tarball to a staging area: 4. Use devpi for uploading a release tarball to a staging area:
- ``devpi use https://devpi.net/USER/dev``
- ``devpi upload --formats sdist,bdist_wheel``
5. run from multiple machines: ``devpi use https://devpi.net/USER/dev``
- ``devpi use https://devpi.net/USER/dev`` ``devpi upload --formats sdist,bdist_wheel``
- ``devpi test pytest==VERSION``
5. Run from multiple machines:
``devpi use https://devpi.net/USER/dev``
``devpi test pytest==VERSION``
6. Check that tests pass for relevant combinations with
``devpi list pytest``
6. check that tests pass for relevant combinations with
``devpi list pytest``
or look at failures with "devpi list -f pytest". or look at failures with "devpi list -f pytest".
There will be some failed environments like e.g. the py33-trial There will be some failed environments like e.g. the py33-trial
or py27-pexpect tox environments on Win32 platforms or py27-pexpect tox environments on Win32 platforms
which is ok (tox does not support skipping on which is ok (tox does not support skipping on
per-platform basis yet). per-platform basis yet).
7. Regenerate the docs examples using tox:: 7. Regenerate the docs examples using tox, and check for regressions::
# Create and activate a virtualenv with regendoc installed
# (currently needs revision 4a9ec1035734)
tox -e regen
8. Build the docs, you need a virtualenv with, py and sphinx tox -e regen
git diff
8. Build the docs, you need a virtualenv with py and sphinx
installed:: installed::
cd docs/en
cd doc/en
make html make html
9. Tag the release:: Commit any changes before tagging the release.
hg tag VERSION
9. Tag the release::
git tag VERSION
git push
10. Upload the docs using doc/en/Makefile::
cd doc/en
make install # or "installall" if you have LaTeX installed for PDF
10. Upload the docs using docs/en/Makefile::
cd docs/en
make install # or "installall" if you have LaTeX installed
This requires ssh-login permission on pytest.org because it uses This requires ssh-login permission on pytest.org because it uses
rsync. rsync.
Note that the "install" target of doc/en/Makefile defines where the Note that the "install" target of doc/en/Makefile defines where the
rsync goes to, typically to the "latest" section of pytest.org. rsync goes to, typically to the "latest" section of pytest.org.
11. publish to pypi "devpi push pytest-VERSION pypi:NAME" where NAME If you are making a minor release (e.g. 5.4), you also need to manually
is the name of pypi.python.org as configured in your create a symlink for "latest"::
~/.pypirc file -- it's the same you would use with
"setup.py upload -r NAME"
12. send release announcement to mailing lists: ssh pytest-dev@pytest.org
ln -s 5.4 latest
pytest-dev Browse to pytest.org to verify.
testing-in-python
python-announce-list@python.org 11. Publish to pypi::
devpi push pytest-VERSION pypi:NAME
where NAME is the name of pypi.python.org as configured in your
~/.pypirc file `for devpi <http://doc.devpi.net/latest/quickstart-releaseprocess.html?highlight=pypirc#devpi-push-releasing-to-an-external-index>`_.
12. Send release announcement to mailing lists:
- pytest-dev
- testing-in-python
- python-announce-list@python.org
13. **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``).

View File

@ -21,11 +21,11 @@ clean:
# generate documentation # generate documentation
docs: develop docs: develop
find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} find doc/en -name '*.rst' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS}
cd doc/en; make html cd doc/en; make html
# upload documentation # upload documentation
upload-docs: develop upload-docs: develop
find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} --update find doc/en -name '*.rst' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc ${REGENDOC_ARGS} --update
#cd doc/en; make install #cd doc/en; make install

View File

@ -1,2 +0,0 @@
#
__version__ = '2.8.0.dev4'

View File

@ -86,8 +86,10 @@ class CaptureManager:
self.deactivate_funcargs() self.deactivate_funcargs()
cap = getattr(self, "_capturing", None) cap = getattr(self, "_capturing", None)
if cap is not None: if cap is not None:
outerr = cap.readouterr() try:
cap.suspend_capturing(in_=in_) outerr = cap.readouterr()
finally:
cap.suspend_capturing(in_=in_)
return outerr return outerr
def activate_funcargs(self, pyfuncitem): def activate_funcargs(self, pyfuncitem):

View File

@ -211,6 +211,10 @@ class PytestPluginManager(PluginManager):
# support deprecated naming because plugins (xdist e.g.) use it # support deprecated naming because plugins (xdist e.g.) use it
return self.get_plugin(name) return self.get_plugin(name)
def hasplugin(self, name):
"""Return True if the plugin with the given name is registered."""
return bool(self.get_plugin(name))
def pytest_configure(self, config): def pytest_configure(self, config):
# XXX now that the pluginmanager exposes hookimpl(tryfirst...) # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
# we should remove tryfirst/trylast as markers # we should remove tryfirst/trylast as markers
@ -897,6 +901,9 @@ class Config(object):
self.warn("I2", "could not load setuptools entry import: %s" % (e,)) self.warn("I2", "could not load setuptools entry import: %s" % (e,))
self.pluginmanager.consider_env() self.pluginmanager.consider_env()
self.known_args_namespace = ns = self._parser.parse_known_args(args) self.known_args_namespace = ns = self._parser.parse_known_args(args)
if self.known_args_namespace.confcutdir is None and self.inifile:
confcutdir = py.path.local(self.inifile).dirname
self.known_args_namespace.confcutdir = confcutdir
try: try:
self.hook.pytest_load_initial_conftests(early_config=self, self.hook.pytest_load_initial_conftests(early_config=self,
args=args, parser=self._parser) args=args, parser=self._parser)

View File

@ -63,7 +63,7 @@ class DoctestItem(pytest.Item):
lineno = test.lineno + example.lineno + 1 lineno = test.lineno + example.lineno + 1
message = excinfo.type.__name__ message = excinfo.type.__name__
reprlocation = ReprFileLocation(filename, lineno, message) reprlocation = ReprFileLocation(filename, lineno, message)
checker = doctest.OutputChecker() checker = _get_unicode_checker()
REPORT_UDIFF = doctest.REPORT_UDIFF REPORT_UDIFF = doctest.REPORT_UDIFF
filelines = py.path.local(filename).readlines(cr=0) filelines = py.path.local(filename).readlines(cr=0)
lines = [] lines = []
@ -100,7 +100,8 @@ def _get_flag_lookup():
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
ELLIPSIS=doctest.ELLIPSIS, ELLIPSIS=doctest.ELLIPSIS,
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS) COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
ALLOW_UNICODE=_get_allow_unicode_flag())
def get_optionflags(parent): def get_optionflags(parent):
optionflags_str = parent.config.getini("doctest_optionflags") optionflags_str = parent.config.getini("doctest_optionflags")
@ -110,15 +111,30 @@ def get_optionflags(parent):
flag_acc |= flag_lookup_table[flag] flag_acc |= flag_lookup_table[flag]
return flag_acc return flag_acc
class DoctestTextfile(DoctestItem, pytest.File): class DoctestTextfile(DoctestItem, pytest.File):
def runtest(self): def runtest(self):
import doctest import doctest
fixture_request = _setup_fixtures(self) fixture_request = _setup_fixtures(self)
failed, tot = doctest.testfile(
str(self.fspath), module_relative=False, # inspired by doctest.testfile; ideally we would use it directly,
optionflags=get_optionflags(self), # but it doesn't support passing a custom checker
extraglobs=dict(getfixture=fixture_request.getfuncargvalue), text = self.fspath.read()
raise_on_error=True, verbose=0) filename = str(self.fspath)
name = self.fspath.basename
globs = dict(getfixture=fixture_request.getfuncargvalue)
if '__name__' not in globs:
globs['__name__'] = '__main__'
optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_unicode_checker())
parser = doctest.DocTestParser()
test = parser.get_doctest(text, globs, name, filename, 0)
runner.run(test)
class DoctestModule(pytest.File): class DoctestModule(pytest.File):
def collect(self): def collect(self):
@ -139,7 +155,8 @@ class DoctestModule(pytest.File):
# uses internal doctest module parsing mechanism # uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder() finder = doctest.DocTestFinder()
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_unicode_checker())
for test in finder.find(module, module.__name__, for test in finder.find(module, module.__name__,
extraglobs=doctest_globals): extraglobs=doctest_globals):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests
@ -160,3 +177,59 @@ def _setup_fixtures(doctest_item):
fixture_request = FixtureRequest(doctest_item) fixture_request = FixtureRequest(doctest_item)
fixture_request._fillfixtures() fixture_request._fillfixtures()
return fixture_request return fixture_request
def _get_unicode_checker():
"""
Returns a doctest.OutputChecker subclass that takes in account the
ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful
when the same doctest should run in Python 2 and Python 3.
An inner class is used to avoid importing "doctest" at the module
level.
"""
if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'):
return _get_unicode_checker.UnicodeOutputChecker()
import doctest
import re
class UnicodeOutputChecker(doctest.OutputChecker):
"""
Copied from doctest_nose_plugin.py from the nltk project:
https://github.com/nltk/nltk
"""
_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
def check_output(self, want, got, optionflags):
res = doctest.OutputChecker.check_output(self, want, got,
optionflags)
if res:
return True
if not (optionflags & _get_allow_unicode_flag()):
return False
else: # pragma: no cover
# the code below will end up executed only in Python 2 in
# our tests, and our coverage check runs in Python 3 only
def remove_u_prefixes(txt):
return re.sub(self._literal_re, r'\1\2', txt)
want = remove_u_prefixes(want)
got = remove_u_prefixes(got)
res = doctest.OutputChecker.check_output(self, want, got,
optionflags)
return res
_get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker
return _get_unicode_checker.UnicodeOutputChecker()
def _get_allow_unicode_flag():
"""
Registers and returns the ALLOW_UNICODE flag.
"""
import doctest
return doctest.register_optionflag('ALLOW_UNICODE')

View File

@ -1,4 +1,4 @@
""" generate a single-file self-contained version of pytest """ """ (deprecated) generate a single-file self-contained version of pytest """
import os import os
import sys import sys
import pkgutil import pkgutil
@ -31,7 +31,12 @@ def pkg_to_mapping(name):
else: # package else: # package
for pyfile in toplevel.visit('*.py'): for pyfile in toplevel.visit('*.py'):
pkg = pkgname(name, toplevel, pyfile) pkg = pkgname(name, toplevel, pyfile)
name2src[pkg] = pyfile.read() if pkg == '_pytest.__init__':
# remove the coding comment line to avoid python bug
lines = pyfile.read().splitlines(True)
name2src[pkg] = ''.join(lines[1:])
else:
name2src[pkg] = pyfile.read()
# with wheels py source code might be not be installed # with wheels py source code might be not be installed
# and the resulting genscript is useless, just bail out. # and the resulting genscript is useless, just bail out.
assert name2src, "no source code found for %r at %r" %(name, toplevel) assert name2src, "no source code found for %r at %r" %(name, toplevel)
@ -72,6 +77,8 @@ def pytest_cmdline_main(config):
genscript = config.getvalue("genscript") genscript = config.getvalue("genscript")
if genscript: if genscript:
tw = _pytest.config.create_terminal_writer(config) tw = _pytest.config.create_terminal_writer(config)
tw.line("WARNING: usage of genscript is deprecated.",
red=True)
deps = ['py', 'pluggy', '_pytest', 'pytest'] deps = ['py', 'pluggy', '_pytest', 'pytest']
if sys.version_info < (2,7): if sys.version_info < (2,7):
deps.append("argparse") deps.append("argparse")

View File

@ -9,6 +9,7 @@ import os
import re import re
import sys import sys
import time import time
import pytest
# Python 2.X and 3.X compatibility # Python 2.X and 3.X compatibility
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
@ -53,6 +54,20 @@ def bin_xml_escape(arg):
return unicode('#x%04X') % i return unicode('#x%04X') % i
return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg)))
@pytest.fixture
def record_xml_property(request):
"""Fixture that adds extra xml properties to the tag for the calling test.
The fixture is callable with (name, value), with value being automatically
xml-encoded.
"""
def inner(name, value):
if hasattr(request.config, "_xml"):
request.config._xml.add_custom_property(name, value)
msg = 'record_xml_property is an experimental feature'
request.config.warn(code='C3', message=msg,
fslocation=request.node.location[:2])
return inner
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting") group = parser.getgroup("terminal reporting")
group.addoption('--junitxml', '--junit-xml', action="store", group.addoption('--junitxml', '--junit-xml', action="store",
@ -75,7 +90,6 @@ def pytest_unconfigure(config):
del config._xml del config._xml
config.pluginmanager.unregister(xml) config.pluginmanager.unregister(xml)
def mangle_testnames(names): def mangle_testnames(names):
names = [x.replace(".py", "") for x in names if x != '()'] names = [x.replace(".py", "") for x in names if x != '()']
names[0] = names[0].replace("/", '.') names[0] = names[0].replace("/", '.')
@ -89,6 +103,10 @@ class LogXML(object):
self.tests = [] self.tests = []
self.passed = self.skipped = 0 self.passed = self.skipped = 0
self.failed = self.errors = 0 self.failed = self.errors = 0
self.custom_properties = {}
def add_custom_property(self, name, value):
self.custom_properties[str(name)] = bin_xml_escape(str(value))
def _opentestcase(self, report): def _opentestcase(self, report):
names = mangle_testnames(report.nodeid.split("::")) names = mangle_testnames(report.nodeid.split("::"))
@ -118,6 +136,10 @@ class LogXML(object):
def append(self, obj): def append(self, obj):
self.tests[-1].append(obj) self.tests[-1].append(obj)
def append_custom_properties(self):
self.tests[-1].attr.__dict__.update(self.custom_properties)
self.custom_properties.clear()
def append_pass(self, report): def append_pass(self, report):
self.passed += 1 self.passed += 1
self._write_captured_output(report) self._write_captured_output(report)
@ -179,6 +201,7 @@ class LogXML(object):
if report.when == "setup": if report.when == "setup":
self._opentestcase(report) self._opentestcase(report)
self.tests[-1].attr.time += getattr(report, 'duration', 0) self.tests[-1].attr.time += getattr(report, 'duration', 0)
self.append_custom_properties()
if report.passed: if report.passed:
if report.when == "call": # ignore setup/teardown if report.when == "call": # ignore setup/teardown
self.append_pass(report) self.append_pass(report)

View File

@ -19,6 +19,7 @@ EXIT_TESTSFAILED = 1
EXIT_INTERRUPTED = 2 EXIT_INTERRUPTED = 2
EXIT_INTERNALERROR = 3 EXIT_INTERNALERROR = 3
EXIT_USAGEERROR = 4 EXIT_USAGEERROR = 4
EXIT_NOTESTSCOLLECTED = 5
name_re = re.compile("^[a-zA-Z_]\w*$") name_re = re.compile("^[a-zA-Z_]\w*$")
@ -100,8 +101,10 @@ def wrap_session(config, doit):
if excinfo.errisinstance(SystemExit): if excinfo.errisinstance(SystemExit):
sys.stderr.write("mainloop: caught Spurious SystemExit!\n") sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
else: else:
if session._testsfailed: if session.testsfailed:
session.exitstatus = EXIT_TESTSFAILED session.exitstatus = EXIT_TESTSFAILED
elif session.testscollected == 0:
session.exitstatus = EXIT_NOTESTSCOLLECTED
finally: finally:
excinfo = None # Explicitly break reference cycle. excinfo = None # Explicitly break reference cycle.
session.startdir.chdir() session.startdir.chdir()
@ -509,7 +512,8 @@ class Session(FSCollector):
FSCollector.__init__(self, config.rootdir, parent=None, FSCollector.__init__(self, config.rootdir, parent=None,
config=config, session=self) config=config, session=self)
self._fs2hookproxy = {} self._fs2hookproxy = {}
self._testsfailed = 0 self.testsfailed = 0
self.testscollected = 0
self.shouldstop = False self.shouldstop = False
self.trace = config.trace.root.get("collection") self.trace = config.trace.root.get("collection")
self._norecursepatterns = config.getini("norecursedirs") self._norecursepatterns = config.getini("norecursedirs")
@ -527,11 +531,11 @@ class Session(FSCollector):
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'): if report.failed and not hasattr(report, 'wasxfail'):
self._testsfailed += 1 self.testsfailed += 1
maxfail = self.config.getvalue("maxfail") maxfail = self.config.getvalue("maxfail")
if maxfail and self._testsfailed >= maxfail: if maxfail and self.testsfailed >= maxfail:
self.shouldstop = "stopping after %d failures" % ( self.shouldstop = "stopping after %d failures" % (
self._testsfailed) self.testsfailed)
pytest_collectreport = pytest_runtest_logreport pytest_collectreport = pytest_runtest_logreport
def isinitpath(self, path): def isinitpath(self, path):
@ -564,6 +568,7 @@ class Session(FSCollector):
config=self.config, items=items) config=self.config, items=items)
finally: finally:
hook.pytest_collection_finish(session=self) hook.pytest_collection_finish(session=self)
self.testscollected = len(items)
return items return items
def _perform_collect(self, args, genitems): def _perform_collect(self, args, genitems):

View File

@ -291,7 +291,7 @@ class MarkInfo:
#: positional argument list, empty if none specified #: positional argument list, empty if none specified
self.args = args self.args = args
#: keyword argument dictionary, empty if nothing specified #: keyword argument dictionary, empty if nothing specified
self.kwargs = kwargs self.kwargs = kwargs.copy()
self._arglist = [(args, kwargs.copy())] self._arglist = [(args, kwargs.copy())]
def __repr__(self): def __repr__(self):

View File

@ -1,4 +1,5 @@
""" Python test discovery, setup and run of test functions. """ """ Python test discovery, setup and run of test functions. """
import re
import fnmatch import fnmatch
import functools import functools
import py import py
@ -8,6 +9,12 @@ import pytest
from _pytest.mark import MarkDecorator, MarkerError from _pytest.mark import MarkDecorator, MarkerError
from py._code.code import TerminalRepr from py._code.code import TerminalRepr
try:
import enum
except ImportError: # pragma: no cover
# Only available in Python 3.4+ or as a backport
enum = None
import _pytest import _pytest
import pluggy import pluggy
@ -22,13 +29,15 @@ isclass = inspect.isclass
callable = py.builtin.callable callable = py.builtin.callable
# used to work around a python2 exception info leak # used to work around a python2 exception info leak
exc_clear = getattr(sys, 'exc_clear', lambda: None) exc_clear = getattr(sys, 'exc_clear', lambda: None)
# The type of re.compile objects is not exposed in Python.
REGEX_TYPE = type(re.compile(''))
def filter_traceback(entry): def filter_traceback(entry):
return entry.path != cutdir1 and not entry.path.relto(cutdir2) return entry.path != cutdir1 and not entry.path.relto(cutdir2)
def get_real_func(obj): def get_real_func(obj):
"""gets the real function object of the (possibly) wrapped object by """ gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial. functools.wraps or functools.partial.
""" """
while hasattr(obj, "__wrapped__"): while hasattr(obj, "__wrapped__"):
@ -55,6 +64,17 @@ def getimfunc(func):
except AttributeError: except AttributeError:
return func return func
def safe_getattr(object, name, default):
""" Like getattr but return default upon any Exception.
Attribute access can potentially fail for 'evil' Python objects.
See issue214
"""
try:
return getattr(object, name, default)
except Exception:
return default
class FixtureFunctionMarker: class FixtureFunctionMarker:
def __init__(self, scope, params, def __init__(self, scope, params,
@ -160,10 +180,13 @@ def pytest_cmdline_main(config):
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
# this misspelling is common - raise a specific error to alert the user # those alternative spellings are common - raise a specific error to alert
if hasattr(metafunc.function, 'parameterize'): # the user
msg = "{0} has 'parameterize', spelling should be 'parametrize'" alt_spellings = ['parameterize', 'parametrise', 'parameterise']
raise MarkerError(msg.format(metafunc.function.__name__)) for attr in alt_spellings:
if hasattr(metafunc.function, attr):
msg = "{0} has '{1}', spelling should be 'parametrize'"
raise MarkerError(msg.format(metafunc.function.__name__, attr))
try: try:
markers = metafunc.function.parametrize markers = metafunc.function.parametrize
except AttributeError: except AttributeError:
@ -245,11 +268,10 @@ def pytest_pycollect_makeitem(collector, name, obj):
raise StopIteration raise StopIteration
# nothing was collected elsewhere, let's do it here # nothing was collected elsewhere, let's do it here
if isclass(obj): if isclass(obj):
if collector.classnamefilter(name): if collector.istestclass(obj, name):
Class = collector._getcustomclass("Class") Class = collector._getcustomclass("Class")
outcome.force_result(Class(name, parent=collector)) outcome.force_result(Class(name, parent=collector))
elif collector.funcnamefilter(name) and hasattr(obj, "__call__") and\ elif collector.istestfunction(obj, name):
getfixturemarker(obj) is None:
# mock seems to store unbound methods (issue473), normalize it # mock seems to store unbound methods (issue473), normalize it
obj = getattr(obj, "__func__", obj) obj = getattr(obj, "__func__", obj)
if not isfunction(obj): if not isfunction(obj):
@ -335,9 +357,24 @@ class PyCollector(PyobjMixin, pytest.Collector):
def funcnamefilter(self, name): def funcnamefilter(self, name):
return self._matches_prefix_or_glob_option('python_functions', name) return self._matches_prefix_or_glob_option('python_functions', name)
def isnosetest(self, obj):
""" Look for the __test__ attribute, which is applied by the
@nose.tools.istest decorator
"""
return safe_getattr(obj, '__test__', False)
def classnamefilter(self, name): def classnamefilter(self, name):
return self._matches_prefix_or_glob_option('python_classes', name) return self._matches_prefix_or_glob_option('python_classes', name)
def istestfunction(self, obj, name):
return (
(self.funcnamefilter(name) or self.isnosetest(obj))
and safe_getattr(obj, "__call__", False) and getfixturemarker(obj) is None
)
def istestclass(self, obj, name):
return self.classnamefilter(name) or self.isnosetest(obj)
def _matches_prefix_or_glob_option(self, option_name, name): def _matches_prefix_or_glob_option(self, option_name, name):
""" """
checks if the given name matches the prefix or glob-pattern defined checks if the given name matches the prefix or glob-pattern defined
@ -480,6 +517,19 @@ class FuncFixtureInfo:
self.names_closure = names_closure self.names_closure = names_closure
self.name2fixturedefs = name2fixturedefs self.name2fixturedefs = name2fixturedefs
def _marked(func, mark):
""" Returns True if :func: is already marked with :mark:, False otherwise.
This can happen if marker is applied to class and the test file is
invoked more than once.
"""
try:
func_mark = getattr(func, mark.name)
except AttributeError:
return False
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
def transfer_markers(funcobj, cls, mod): def transfer_markers(funcobj, cls, mod):
# XXX this should rather be code in the mark plugin or the mark # XXX this should rather be code in the mark plugin or the mark
# plugin should merge with the python plugin. # plugin should merge with the python plugin.
@ -490,9 +540,11 @@ def transfer_markers(funcobj, cls, mod):
continue continue
if isinstance(pytestmark, list): if isinstance(pytestmark, list):
for mark in pytestmark: for mark in pytestmark:
mark(funcobj) if not _marked(funcobj, mark):
mark(funcobj)
else: else:
pytestmark(funcobj) if not _marked(funcobj, pytestmark):
pytestmark(funcobj)
class Module(pytest.File, PyCollector): class Module(pytest.File, PyCollector):
""" Collector for test classes and functions. """ """ Collector for test classes and functions. """
@ -973,8 +1025,15 @@ def _idval(val, argname, idx, idfn):
return s return s
except Exception: except Exception:
pass pass
if isinstance(val, (float, int, str, bool, NoneType)): if isinstance(val, (float, int, str, bool, NoneType)):
return str(val) return str(val)
elif isinstance(val, REGEX_TYPE):
return val.pattern
elif enum is not None and isinstance(val, enum.Enum):
return str(val)
elif isclass(val) and hasattr(val, '__name__'):
return val.__name__
return str(argname)+str(idx) return str(argname)+str(idx)
def _idvalset(idx, valset, argnames, idfn): def _idvalset(idx, valset, argnames, idfn):
@ -1049,8 +1108,8 @@ def getlocation(function, curdir):
# builtin pytest.raises helper # builtin pytest.raises helper
def raises(ExpectedException, *args, **kwargs): def raises(expected_exception, *args, **kwargs):
""" assert that a code block/function call raises @ExpectedException """ assert that a code block/function call raises @expected_exception
and raise a failure exception otherwise. and raise a failure exception otherwise.
This helper produces a ``py.code.ExceptionInfo()`` object. This helper produces a ``py.code.ExceptionInfo()`` object.
@ -1098,23 +1157,23 @@ def raises(ExpectedException, *args, **kwargs):
""" """
__tracebackhide__ = True __tracebackhide__ = True
if ExpectedException is AssertionError: if expected_exception is AssertionError:
# we want to catch a AssertionError # we want to catch a AssertionError
# replace our subclass with the builtin one # replace our subclass with the builtin one
# see https://github.com/pytest-dev/pytest/issues/176 # see https://github.com/pytest-dev/pytest/issues/176
from _pytest.assertion.util import BuiltinAssertionError \ from _pytest.assertion.util import BuiltinAssertionError \
as ExpectedException as expected_exception
msg = ("exceptions must be old-style classes or" msg = ("exceptions must be old-style classes or"
" derived from BaseException, not %s") " derived from BaseException, not %s")
if isinstance(ExpectedException, tuple): if isinstance(expected_exception, tuple):
for exc in ExpectedException: for exc in expected_exception:
if not inspect.isclass(exc): if not isclass(exc):
raise TypeError(msg % type(exc)) raise TypeError(msg % type(exc))
elif not inspect.isclass(ExpectedException): elif not isclass(expected_exception):
raise TypeError(msg % type(ExpectedException)) raise TypeError(msg % type(expected_exception))
if not args: if not args:
return RaisesContext(ExpectedException) return RaisesContext(expected_exception)
elif isinstance(args[0], str): elif isinstance(args[0], str):
code, = args code, = args
assert isinstance(code, str) assert isinstance(code, str)
@ -1127,19 +1186,19 @@ def raises(ExpectedException, *args, **kwargs):
py.builtin.exec_(code, frame.f_globals, loc) py.builtin.exec_(code, frame.f_globals, loc)
# XXX didn'T mean f_globals == f_locals something special? # XXX didn'T mean f_globals == f_locals something special?
# this is destroyed here ... # this is destroyed here ...
except ExpectedException: except expected_exception:
return py.code.ExceptionInfo() return py.code.ExceptionInfo()
else: else:
func = args[0] func = args[0]
try: try:
func(*args[1:], **kwargs) func(*args[1:], **kwargs)
except ExpectedException: except expected_exception:
return py.code.ExceptionInfo() return py.code.ExceptionInfo()
pytest.fail("DID NOT RAISE") pytest.fail("DID NOT RAISE")
class RaisesContext(object): class RaisesContext(object):
def __init__(self, ExpectedException): def __init__(self, expected_exception):
self.ExpectedException = ExpectedException self.expected_exception = expected_exception
self.excinfo = None self.excinfo = None
def __enter__(self): def __enter__(self):
@ -1158,7 +1217,7 @@ class RaisesContext(object):
exc_type, value, traceback = tp exc_type, value, traceback = tp
tp = exc_type, exc_type(value), traceback tp = exc_type, exc_type(value), traceback
self.excinfo.__init__(tp) self.excinfo.__init__(tp)
return issubclass(self.excinfo.type, self.ExpectedException) return issubclass(self.excinfo.type, self.expected_exception)
# #
# the basic pytest Function item # the basic pytest Function item
@ -1357,7 +1416,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
return self._pyfuncitem.session return self._pyfuncitem.session
def addfinalizer(self, finalizer): def addfinalizer(self, finalizer):
"""add finalizer/teardown function to be called after the """ add finalizer/teardown function to be called after the
last test within the requesting test context finished last test within the requesting test context finished
execution. """ execution. """
# XXX usually this method is shadowed by fixturedef specific ones # XXX usually this method is shadowed by fixturedef specific ones
@ -1771,7 +1830,7 @@ class FixtureManager:
if fixturedef.params is not None: if fixturedef.params is not None:
func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]]) func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]])
# skip directly parametrized arguments # skip directly parametrized arguments
if argname not in func_params and argname not in func_params[0]: if argname not in func_params:
metafunc.parametrize(argname, fixturedef.params, metafunc.parametrize(argname, fixturedef.params,
indirect=True, scope=fixturedef.scope, indirect=True, scope=fixturedef.scope,
ids=fixturedef.ids) ids=fixturedef.ids)
@ -2120,4 +2179,3 @@ def get_scope_node(node, scope):
return node.session return node.session
raise ValueError("unknown scope") raise ValueError("unknown scope")
return node.getparent(cls) return node.getparent(cls)

View File

@ -1,10 +1,14 @@
""" recording warnings during test function execution. """ """ recording warnings during test function execution. """
import inspect
import py
import sys import sys
import warnings import warnings
import pytest
def pytest_funcarg__recwarn(request): @pytest.yield_fixture
def recwarn(request):
"""Return a WarningsRecorder instance that provides these methods: """Return a WarningsRecorder instance that provides these methods:
* ``pop(category=None)``: return last warning matching the category. * ``pop(category=None)``: return last warning matching the category.
@ -13,83 +17,173 @@ def pytest_funcarg__recwarn(request):
See http://docs.python.org/library/warnings.html for information See http://docs.python.org/library/warnings.html for information
on warning categories. on warning categories.
""" """
if sys.version_info >= (2,7):
oldfilters = warnings.filters[:]
warnings.simplefilter('default')
def reset_filters():
warnings.filters[:] = oldfilters
request.addfinalizer(reset_filters)
wrec = WarningsRecorder() wrec = WarningsRecorder()
request.addfinalizer(wrec.finalize) with wrec:
return wrec warnings.simplefilter('default')
yield wrec
def pytest_namespace(): def pytest_namespace():
return {'deprecated_call': deprecated_call} return {'deprecated_call': deprecated_call,
'warns': warns}
def deprecated_call(func, *args, **kwargs): def deprecated_call(func, *args, **kwargs):
""" assert that calling ``func(*args, **kwargs)`` """Assert that ``func(*args, **kwargs)`` triggers a DeprecationWarning.
triggers a DeprecationWarning.
""" """
l = [] wrec = WarningsRecorder()
oldwarn_explicit = getattr(warnings, 'warn_explicit') with wrec:
def warn_explicit(*args, **kwargs): warnings.simplefilter('always') # ensure all warnings are triggered
l.append(args)
oldwarn_explicit(*args, **kwargs)
oldwarn = getattr(warnings, 'warn')
def warn(*args, **kwargs):
l.append(args)
oldwarn(*args, **kwargs)
warnings.warn_explicit = warn_explicit
warnings.warn = warn
try:
ret = func(*args, **kwargs) ret = func(*args, **kwargs)
finally:
warnings.warn_explicit = oldwarn_explicit if not any(r.category is DeprecationWarning for r in wrec):
warnings.warn = oldwarn
if not l:
__tracebackhide__ = True __tracebackhide__ = True
raise AssertionError("%r did not produce DeprecationWarning" %(func,)) raise AssertionError("%r did not produce DeprecationWarning" % (func,))
return ret return ret
class RecordedWarning: def warns(expected_warning, *args, **kwargs):
def __init__(self, message, category, filename, lineno, line): """Assert that code raises a particular class of warning.
Specifically, the input @expected_warning can be a warning class or
tuple of warning classes, and the code must return that warning
(if a single class) or one of those warnings (if a tuple).
This helper produces a list of ``warnings.WarningMessage`` objects,
one for each warning raised.
This function can be used as a context manager, or any of the other ways
``pytest.raises`` can be used::
>>> with warns(RuntimeWarning):
... warnings.warn("my warning", RuntimeWarning)
"""
wcheck = WarningsChecker(expected_warning)
if not args:
return wcheck
elif isinstance(args[0], str):
code, = args
assert isinstance(code, str)
frame = sys._getframe(1)
loc = frame.f_locals.copy()
loc.update(kwargs)
with wcheck:
code = py.code.Source(code).compile()
py.builtin.exec_(code, frame.f_globals, loc)
else:
func = args[0]
with wcheck:
return func(*args[1:], **kwargs)
class RecordedWarning(object):
def __init__(self, message, category, filename, lineno, file, line):
self.message = message self.message = message
self.category = category self.category = category
self.filename = filename self.filename = filename
self.lineno = lineno self.lineno = lineno
self.file = file
self.line = line self.line = line
class WarningsRecorder:
def __init__(self): class WarningsRecorder(object):
self.list = [] """A context manager to record raised warnings.
def showwarning(message, category, filename, lineno, line=0):
self.list.append(RecordedWarning( Adapted from `warnings.catch_warnings`.
message, category, filename, lineno, line)) """
try:
self.old_showwarning(message, category, def __init__(self, module=None):
filename, lineno, line=line) self._module = sys.modules['warnings'] if module is None else module
except TypeError: self._entered = False
# < python2.6 self._list = []
self.old_showwarning(message, category, filename, lineno)
self.old_showwarning = warnings.showwarning @property
warnings.showwarning = showwarning def list(self):
"""The list of recorded warnings."""
return self._list
def __getitem__(self, i):
"""Get a recorded warning by index."""
return self._list[i]
def __iter__(self):
"""Iterate through the recorded warnings."""
return iter(self._list)
def __len__(self):
"""The number of recorded warnings."""
return len(self._list)
def pop(self, cls=Warning): def pop(self, cls=Warning):
""" pop the first recorded warning, raise exception if not exists.""" """Pop the first recorded warning, raise exception if not exists."""
for i, w in enumerate(self.list): for i, w in enumerate(self._list):
if issubclass(w.category, cls): if issubclass(w.category, cls):
return self.list.pop(i) return self._list.pop(i)
__tracebackhide__ = True __tracebackhide__ = True
assert 0, "%r not found in %r" %(cls, self.list) raise AssertionError("%r not found in warning list" % cls)
#def resetregistry(self):
# warnings.onceregistry.clear()
# warnings.__warningregistry__.clear()
def clear(self): def clear(self):
self.list[:] = [] """Clear the list of recorded warnings."""
self._list[:] = []
def finalize(self): def __enter__(self):
warnings.showwarning = self.old_showwarning if self._entered:
__tracebackhide__ = True
raise RuntimeError("Cannot enter %r twice" % self)
self._entered = True
self._filters = self._module.filters
self._module.filters = self._filters[:]
self._showwarning = self._module.showwarning
def showwarning(message, category, filename, lineno,
file=None, line=None):
self._list.append(RecordedWarning(
message, category, filename, lineno, file, line))
# still perform old showwarning functionality
self._showwarning(
message, category, filename, lineno, file=file, line=line)
self._module.showwarning = showwarning
# allow the same warning to be raised more than once
self._module.simplefilter('always', append=True)
return self
def __exit__(self, *exc_info):
if not self._entered:
__tracebackhide__ = True
raise RuntimeError("Cannot exit %r without entering first" % self)
self._module.filters = self._filters
self._module.showwarning = self._showwarning
class WarningsChecker(WarningsRecorder):
def __init__(self, expected_warning=None, module=None):
super(WarningsChecker, self).__init__(module=module)
msg = ("exceptions must be old-style classes or "
"derived from Warning, not %s")
if isinstance(expected_warning, tuple):
for exc in expected_warning:
if not inspect.isclass(exc):
raise TypeError(msg % type(exc))
elif inspect.isclass(expected_warning):
expected_warning = (expected_warning,)
elif expected_warning is not None:
raise TypeError(msg % type(expected_warning))
self.expected_warning = expected_warning
def __exit__(self, *exc_info):
super(WarningsChecker, self).__exit__(*exc_info)
# only check if we're not currently handling an exception
if all(a is None for a in exc_info):
if self.expected_warning is not None:
if not any(r.category in self.expected_warning for r in self):
__tracebackhide__ = True
pytest.fail("DID NOT WARN")

View File

@ -68,6 +68,11 @@ class DictImporter(object):
return res return res
if __name__ == "__main__": if __name__ == "__main__":
try:
import pkg_resources # noqa
except ImportError:
sys.stderr.write("ERROR: setuptools not installed\n")
sys.exit(2)
if sys.version_info >= (3, 0): if sys.version_info >= (3, 0):
exec("def do_exec(co, loc): exec(co, loc)\n") exec("def do_exec(co, loc): exec(co, loc)\n")
import pickle import pickle
@ -80,6 +85,5 @@ if __name__ == "__main__":
importer = DictImporter(sources) importer = DictImporter(sources)
sys.meta_path.insert(0, importer) sys.meta_path.insert(0, importer)
entry = "@ENTRY@" entry = "@ENTRY@"
do_exec(entry, locals()) # noqa do_exec(entry, locals()) # noqa

View File

@ -2,6 +2,8 @@
This is a good source for looking at the various reporting hooks. This is a good source for looking at the various reporting hooks.
""" """
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
import pytest import pytest
import pluggy import pluggy
import py import py
@ -298,13 +300,9 @@ class TerminalReporter:
plugininfo = config.pluginmanager.list_plugin_distinfo() plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo: if plugininfo:
l = []
for plugin, dist in plugininfo: lines.append(
name = dist.project_name "plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
if name.startswith("pytest-"):
name = name[7:]
l.append(name)
lines.append("plugins: %s" % ", ".join(l))
return lines return lines
def pytest_collection_finish(self, session): def pytest_collection_finish(self, session):
@ -359,12 +357,15 @@ class TerminalReporter:
outcome = yield outcome = yield
outcome.get_result() outcome.get_result()
self._tw.line("") self._tw.line("")
if exitstatus in (0, 1, 2, 4): summary_exit_codes = (
EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR,
EXIT_NOTESTSCOLLECTED)
if exitstatus in summary_exit_codes:
self.summary_errors() self.summary_errors()
self.summary_failures() self.summary_failures()
self.summary_warnings() self.summary_warnings()
self.config.hook.pytest_terminal_summary(terminalreporter=self) self.config.hook.pytest_terminal_summary(terminalreporter=self)
if exitstatus == 2: if exitstatus == EXIT_INTERRUPTED:
self._report_keyboardinterrupt() self._report_keyboardinterrupt()
del self._keyboardinterrupt_memo del self._keyboardinterrupt_memo
self.summary_deselected() self.summary_deselected()
@ -549,3 +550,18 @@ def build_summary_stats_line(stats):
color = 'yellow' color = 'yellow'
return (line, color) return (line, color)
def _plugin_nameversions(plugininfo):
l = []
for plugin, dist in plugininfo:
# gets us name and version!
name = '{dist.project_name}-{dist.version}'.format(dist=dist)
# questionable convenience, but it keeps things short
if name.startswith("pytest-"):
name = name[7:]
# we decided to print python package names
# they can have more than one plugin
if name not in l:
l.append(name)
return l

View File

@ -56,7 +56,7 @@ class TempdirFactory:
# make_numbered_dir() call # make_numbered_dir() call
import getpass import getpass
temproot = py.path.local.get_temproot() temproot = py.path.local.get_temproot()
rootdir = temproot.join('pytest-%s' % getpass.getuser()) rootdir = temproot.join('pytest-of-%s' % getpass.getuser())
rootdir.ensure(dir=1) rootdir.ensure(dir=1)
basetemp = py.path.local.make_numbered_dir(prefix='pytest-', basetemp = py.path.local.make_numbered_dir(prefix='pytest-',
rootdir=rootdir) rootdir=rootdir)

View File

@ -46,7 +46,7 @@ installall: clean install installpdf
@echo "done" @echo "done"
regen: regen:
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst
html: html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html

View File

@ -6,7 +6,7 @@ def get_version_string():
fn = py.path.local(__file__).join("..", "..", "..", fn = py.path.local(__file__).join("..", "..", "..",
"_pytest", "__init__.py") "_pytest", "__init__.py")
for line in fn.readlines(): for line in fn.readlines():
if "version" in line: if "version" in line and not line.strip().startswith('#'):
return eval(line.split("=")[-1]) return eval(line.split("=")[-1])
def get_minor_version_string(): def get_minor_version_string():

27
doc/en/apiref.rst Normal file
View File

@ -0,0 +1,27 @@
.. _apiref:
pytest reference documentation
================================================
.. toctree::
:maxdepth: 2
builtin
customize
assert
fixture
yieldfixture
parametrize
xunit_setup
capture
monkeypatch
xdist
tmpdir
mark
skipping
recwarn
unittest
nose
doctest

View File

@ -1,27 +0,0 @@
.. _apiref:
pytest reference documentation
================================================
.. toctree::
:maxdepth: 2
builtin.txt
customize.txt
assert.txt
fixture.txt
yieldfixture.txt
parametrize.txt
xunit_setup.txt
capture.txt
monkeypatch.txt
xdist.txt
tmpdir.txt
mark.txt
skipping.txt
recwarn.txt
unittest.txt
nose.txt
doctest.txt

View File

@ -114,6 +114,16 @@ like documenting unfixed bugs (where the test describes what "should" happen)
or bugs in dependencies. or bugs in dependencies.
.. _`assertwarns`:
Assertions about expected warnings
-----------------------------------------
.. versionadded:: 2.8
You can check that code raises a particular warning using
:ref:`pytest.warns <warns>`.
.. _newreport: .. _newreport:
@ -228,9 +238,7 @@ Reporting details about a failing assertion is achieved either by rewriting
assert statements before they are run or re-evaluating the assert expression and assert statements before they are run or re-evaluating the assert expression and
recording the intermediate values. Which technique is used depends on the recording the intermediate values. Which technique is used depends on the
location of the assert, ``pytest`` configuration, and Python version being used location of the assert, ``pytest`` configuration, and Python version being used
to run ``pytest``. Note that for assert statements with a manually provided to run ``pytest``.
message, i.e. ``assert expr, message``, no assertion introspection takes place
and the manually provided message will be rendered in tracebacks.
By default, if the Python version is greater than or equal to 2.6, ``pytest`` By default, if the Python version is greater than or equal to 2.6, ``pytest``
rewrites assert statements in test modules. Rewritten assert statements put rewrites assert statements in test modules. Rewritten assert statements put

View File

@ -47,7 +47,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.autosummary',
templates_path = ['_templates'] templates_path = ['_templates']
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.txt' source_suffix = '.rst'
# The encoding of source files. # The encoding of source files.
#source_encoding = 'utf-8-sig' #source_encoding = 'utf-8-sig'
@ -73,13 +73,13 @@ copyright = u'2015, holger krekel and pytest-dev team'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*', exclude_patterns = ['links.inc', '_build', 'naming20.rst', 'test/*',
"old_*", "old_*",
'*attic*', '*attic*',
'*/attic*', '*/attic*',
'funcargs.txt', 'funcargs.rst',
'setup.txt', 'setup.rst',
'example/remoteinterp.txt', 'example/remoteinterp.rst',
] ]

View File

@ -17,11 +17,11 @@ Full pytest documentation
example/index example/index
talks talks
contributing contributing
funcarg_compare.txt funcarg_compare
announce/index announce/index
.. toctree:: .. toctree::
:hidden: :hidden:
changelog.txt changelog

View File

@ -219,3 +219,10 @@ Builtin configuration file options
One or more doctest flag names from the standard ``doctest`` module. One or more doctest flag names from the standard ``doctest`` module.
:doc:`See how py.test handles doctests <doctest>`. :doc:`See how py.test handles doctests <doctest>`.
.. confval:: confcutdir
Sets a directory where search upwards for ``conftest.py`` files stops.
By default, pytest will stop searching for ``conftest.py`` files upwards
from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any,
or up to the file-system root.

View File

@ -72,3 +72,18 @@ ignore lengthy exception stack traces you can just write::
# content of pytest.ini # content of pytest.ini
[pytest] [pytest]
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
py.test also introduces a new ``ALLOW_UNICODE`` option flag: when enabled, the
``u`` prefix is stripped from unicode strings in expected doctest output. This
allows doctests which use unicode to run in Python 2 and 3 unchanged.
As with any other option flag, this flag can be enabled in ``pytest.ini`` using
the ``doctest_optionflags`` ini option or by an inline comment in the doc test
itself::
# content of example.rst
>>> get_unicode_greeting() # doctest: +ALLOW_UNICODE
'Hello'

View File

@ -25,10 +25,10 @@ The following examples aim at various use cases you might encounter.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
reportingdemo.txt reportingdemo
simple.txt simple
parametrize.txt parametrize
markers.txt markers
special.txt special
pythoncollection.txt pythoncollection
nonpython.txt nonpython

View File

@ -81,7 +81,7 @@ Numbers, strings, booleans and None will have their usual string representation
used in the test ID. For other objects, pytest will make a string based on used in the test ID. For other objects, pytest will make a string based on
the argument name:: the argument name::
# contents of test_time.py # content of test_time.py
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@ -534,23 +534,24 @@ case we just write some informations out to a ``failures`` file::
import pytest import pytest
import os.path import os.path
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call, __multicall__): def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object # execute all other hooks to obtain the report object
rep = __multicall__.execute() outcome = yield
rep = outcome.get_result()
# we only look at actual failing test calls, not setup/teardown # we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed: if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w" mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f: with open("failures", mode) as f:
# let's also access a fixture for the fun of it # let's also access a fixture for the fun of it
if "tmpdir" in item.funcargs: if "tmpdir" in item.fixturenames:
extra = " (%s)" % item.funcargs["tmpdir"] extra = " (%s)" % item.funcargs["tmpdir"]
else: else:
extra = "" extra = ""
f.write(rep.nodeid + extra + "\n") f.write(rep.nodeid + extra + "\n")
return rep
if you then have failing tests:: if you then have failing tests::
@ -606,16 +607,16 @@ here is a little example implemented via a local plugin::
import pytest import pytest
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call, __multicall__): def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object # execute all other hooks to obtain the report object
rep = __multicall__.execute() outcome = yield
rep = outcome.get_result()
# set an report attribute for each phase of a call, which can # set an report attribute for each phase of a call, which can
# be "setup", "call", "teardown" # be "setup", "call", "teardown"
setattr(item, "rep_" + rep.when, rep) setattr(item, "rep_" + rep.when, rep)
return rep
@pytest.fixture @pytest.fixture
@ -742,5 +743,4 @@ over to ``pytest`` instead. For example::
This makes it convenient to execute your tests from within your frozen This makes it convenient to execute your tests from within your frozen
application, using standard ``py.test`` command-line options:: application, using standard ``py.test`` command-line options::
$ ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/ ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/
/bin/sh: ./app_main: No such file or directory

View File

@ -154,8 +154,8 @@ to create a JUnitXML file that Jenkins_ can pick up and generate reports.
.. _standalone: .. _standalone:
.. _`genscript method`: .. _`genscript method`:
Create a pytest standalone script (deprecated) Create a pytest standalone script
------------------------------------------- -----------------------------------------------
If you are a maintainer or application developer and want people If you are a maintainer or application developer and want people
who don't deal with python much to easily run tests you may generate who don't deal with python much to easily run tests you may generate

View File

@ -5,10 +5,10 @@ Getting started basics
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
index.txt index
getting-started.txt getting-started
usage.txt usage
goodpractises.txt goodpractises
projects.txt projects
faq.txt faq

View File

@ -114,6 +114,18 @@ Let's run this::
The one parameter set which caused a failure previously now The one parameter set which caused a failure previously now
shows up as an "xfailed (expected to fail)" test. shows up as an "xfailed (expected to fail)" test.
To get all combinations of multiple parametrized arguments you can stack
``parametrize`` decorators::
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
This will run the test with the arguments set to x=0/y=2, x=0/y=3, x=1/y=2 and
x=1/y=3.
.. note:: .. note::
In versions prior to 2.4 one needed to specify the argument In versions prior to 2.4 one needed to specify the argument

116
doc/en/recwarn.rst Normal file
View File

@ -0,0 +1,116 @@
Asserting Warnings
=====================================================
.. _warns:
Asserting warnings with the warns function
-----------------------------------------------
.. versionadded:: 2.8
You can check that code raises a particular warning using ``pytest.warns``,
which works in a similar manner to :ref:`raises <assertraises>`::
import warnings
import pytest
def test_warning():
with pytest.warns(UserWarning):
warnings.warn("my warning", UserWarning)
The test will fail if the warning in question is not raised.
You can also call ``pytest.warns`` on a function or code string::
pytest.warns(expected_warning, func, *args, **kwargs)
pytest.warns(expected_warning, "func(*args, **kwargs)")
The function also returns a list of all raised warnings (as
``warnings.WarningMessage`` objects), which you can query for
additional information::
with pytest.warns(RuntimeWarning) as record:
warnings.warn("another warning", RuntimeWarning)
# check that only one warning was raised
assert len(record) == 1
# check that the message matches
assert record[0].message.args[0] == "another warning"
Alternatively, you can examine raised warnings in detail using the
:ref:`recwarn <recwarn>` fixture (see below).
.. _recwarn:
Recording warnings
------------------------
You can record raised warnings either using ``pytest.warns`` or with
the ``recwarn`` fixture.
To record with ``pytest.warns`` without asserting anything about the warnings,
pass ``None`` as the expected warning type::
with pytest.warns(None) as record:
warnings.warn("user", UserWarning)
warnings.warn("runtime", RuntimeWarning)
assert len(record) == 2
assert str(record[0].message) == "user"
assert str(record[1].message) == "runtime"
The ``recwarn`` fixture will record warnings for the whole function::
import warnings
def test_hello(recwarn):
warnings.warn("hello", UserWarning)
assert len(recwarn) == 1
w = recwarn.pop(UserWarning)
assert issubclass(w.category, UserWarning)
assert str(w.message) == "hello"
assert w.filename
assert w.lineno
Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded
warnings: a WarningsRecorder instance. To view the recorded warnings, you can
iterate over this instance, call ``len`` on it to get the number of recorded
warnings, or index into it to get a particular recorded warning. It also
provides these methods:
.. autoclass:: _pytest.recwarn.WarningsRecorder()
:members:
Each recorded warning has the attributes ``message``, ``category``,
``filename``, ``lineno``, ``file``, and ``line``. The ``category`` is the
class of the warning. The ``message`` is the warning itself; calling
``str(message)`` will return the actual message of the warning.
.. _ensuring_function_triggers:
Ensuring a function triggers a deprecation warning
-------------------------------------------------------
You can also call a global helper for checking
that a certain function call triggers a ``DeprecationWarning``::
import pytest
def test_global():
pytest.deprecated_call(myfunction, 17)
By default, deprecation warnings will not be caught when using ``pytest.warns``
or ``recwarn``, since the default Python warnings filters hide
DeprecationWarnings. If you wish to record them in your own code, use the
command ``warnings.simplefilter('always')``::
import warnings
import pytest
def test_deprecation(recwarn):
warnings.simplefilter('always')
warnings.warn("deprecated", DeprecationWarning)
assert len(recwarn) == 1
assert recwarn.pop(DeprecationWarning)

View File

@ -1,46 +0,0 @@
Asserting deprecation and other warnings
=====================================================
.. _function_argument:
The recwarn function argument
------------------------------------
You can use the ``recwarn`` funcarg to assert that code triggers
warnings through the Python warnings system. Here is a simple
self-contained test::
# content of test_recwarn.py
def test_hello(recwarn):
from warnings import warn
warn("hello", DeprecationWarning)
w = recwarn.pop(DeprecationWarning)
assert issubclass(w.category, DeprecationWarning)
assert 'hello' in str(w.message)
assert w.filename
assert w.lineno
The ``recwarn`` function argument provides these methods:
.. method:: pop(category=None)
Return last warning matching the category.
.. method:: clear()
Clear list of warnings
.. _ensuring_function_triggers:
Ensuring a function triggers a deprecation warning
-------------------------------------------------------
You can also call a global helper for checking
that a certain function call triggers a ``DeprecationWarning``::
import pytest
def test_global():
pytest.deprecated_call(myfunction, 17)

View File

@ -1,54 +0,0 @@
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 ``git 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 <http://doc.devpi.net/latest/quickstart-releaseprocess.html>`_. 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@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
git commit -a -m "... finalized pytest-VERSION"
git tag VERSION
git push
9. send out release announcement to pytest-dev@python.org,
testing-in-python@lists.idyll.org and python-announce-list@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 :)

View File

@ -83,7 +83,7 @@ As with all function :ref:`marking <mark>` you can skip test functions at the
`whole class- or module level`_. If your code targets python2.6 or above you `whole class- or module level`_. If your code targets python2.6 or above you
use the skipif decorator (and any other marker) on classes:: use the skipif decorator (and any other marker) on classes::
@pytest.mark.skipif(sys.platform == 'win32', @pytest.mark.skipif(sys.platform != 'win32',
reason="requires windows") reason="requires windows")
class TestPosixCalls: class TestPosixCalls:
@ -97,7 +97,7 @@ If your code targets python2.5 where class-decorators are not available,
you can set the ``pytestmark`` attribute of a class:: you can set the ``pytestmark`` attribute of a class::
class TestPosixCalls: class TestPosixCalls:
pytestmark = pytest.mark.skipif(sys.platform == 'win32', pytestmark = pytest.mark.skipif(sys.platform != 'win32',
reason="requires Windows") reason="requires Windows")
def test_function(self): def test_function(self):

Some files were not shown because too many files have changed in this diff Show More