From commits-noreply at bitbucket.org Mon Nov 1 00:26:27 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 31 Oct 2010 18:26:27 -0500 (CDT) Subject: [py-svn] pytest commit ad3773acb10a: simplify session object and rename some more hooks, not exposed/released yet Message-ID: <20101031232627.113261E103D@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288567632 -3600 # Node ID ad3773acb10a58745b9919cc37d4d720ef2ed971 # Parent 629f096561d6e38a59731a4b10eae36427f5c71d simplify session object and rename some more hooks, not exposed/released yet --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -37,17 +37,17 @@ def pytest_configure(config): def pytest_unconfigure(config): """ called before test process is exited. """ -def pytest_runtest_mainloop(session): +def pytest_runtestloop(session): """ called for performing the main runtest loop (after collection. """ -pytest_runtest_mainloop.firstresult = True +pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- -def pytest_collection_perform(session): +def pytest_collection(session): """ perform the collection protocol for the given session. """ -pytest_collection_perform.firstresult = True +pytest_collection.firstresult = True def pytest_collection_modifyitems(config, items): """ called after collection has been performed, may filter or re-order --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -8,6 +8,13 @@ import py import pytest import os, sys +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 +EXIT_NOHOSTS = 4 + def pytest_addoption(parser): group = parser.getgroup("general", "running and selection options") @@ -44,9 +51,35 @@ def pytest_configure(config): config.option.maxfail = 1 def pytest_cmdline_main(config): - return Session(config).main() + """ default command line protocol for initialization, collection, + running tests and reporting. """ + session = Session(config) + session.exitstatus = EXIT_OK + try: + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=session) + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + except pytest.UsageError: + raise + except KeyboardInterrupt: + excinfo = py.code.ExceptionInfo() + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = EXIT_INTERRUPTED + except: + excinfo = py.code.ExceptionInfo() + config.pluginmanager.notify_exception(excinfo) + session.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + if not session.exitstatus and session._testsfailed: + session.exitstatus = EXIT_TESTSFAILED + config.hook.pytest_sessionfinish(session=session, + exitstatus=session.exitstatus) + config.pluginmanager.do_unconfigure(config) + return session.exitstatus -def pytest_collection_perform(session): +def pytest_collection(session): collection = session.collection assert not hasattr(collection, 'items') hook = session.config.hook @@ -55,7 +88,7 @@ def pytest_collection_perform(session): hook.pytest_collection_finish(collection=collection) return True -def pytest_runtest_mainloop(session): +def pytest_runtestloop(session): if session.config.option.collectonly: return True for item in session.collection.items: @@ -86,16 +119,7 @@ def pytest_collect_directory(path, paren def pytest_report_iteminfo(item): return item.reportinfo() - -# exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 -EXIT_NOHOSTS = 4 - class Session(object): - nodeid = "" class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ __module__ = 'builtins' # for py3 @@ -120,37 +144,6 @@ class Session(object): self._testsfailed) pytest_collectreport = pytest_runtest_logreport - def main(self): - """ main loop for running tests. """ - self.shouldstop = False - self.exitstatus = EXIT_OK - config = self.config - try: - config.pluginmanager.do_configure(config) - config.hook.pytest_sessionstart(session=self) - config.hook.pytest_collection_perform(session=self) - config.hook.pytest_runtest_mainloop(session=self) - except pytest.UsageError: - raise - except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() - self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - self.exitstatus = EXIT_INTERRUPTED - except: - excinfo = py.code.ExceptionInfo() - self.config.pluginmanager.notify_exception(excinfo) - self.exitstatus = EXIT_INTERNALERROR - if excinfo.errisinstance(SystemExit): - sys.stderr.write("mainloop: caught Spurious SystemExit!\n") - - if not self.exitstatus and self._testsfailed: - self.exitstatus = EXIT_TESTSFAILED - self.config.hook.pytest_sessionfinish( - session=self, exitstatus=self.exitstatus, - ) - config.pluginmanager.do_unconfigure(config) - return self.exitstatus - class Collection: def __init__(self, config): self.config = config From commits-noreply at bitbucket.org Mon Nov 1 00:26:48 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 31 Oct 2010 18:26:48 -0500 (CDT) Subject: [py-svn] pytest-xdist commit ba07fdbea9d9: adapt to new pytest changes, use new addini/getini methods for rsyncdirs / rsyncignore options Message-ID: <20101031232648.C1DE76C129F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1288567663 -3600 # Node ID ba07fdbea9d953cf4f55db67e2e242527948019f # Parent 407d2815cad2f31af54070ddbc9367c8cba31ee6 adapt to new pytest changes, use new addini/getini methods for rsyncdirs / rsyncignore options --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -126,17 +126,18 @@ put options values in a ``conftest.py`` Any commandline ``--tx`` specifictions will add to the list of available execution environments. -Specifying "rsync" dirs in a conftest.py -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +Specifying "rsync" dirs in a setup.cfg|tox.ini ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -In your ``mypkg/conftest.py`` you may specify directories to synchronise -or to exclude:: +In a ``tox.ini`` or ``setup.cfg`` file in your root project directory +you may specify directories to include or to exclude in synchronisation:: - rsyncdirs = ['.', '../plugins'] - rsyncignore = ['_cache'] + [pytest] + rsyncdirs = . mypkg helperpkg + rsyncignore = .hg These directory specifications are relative to the directory -where the ``conftest.py`` is found. +where the configuration file was found. """ @@ -173,6 +174,11 @@ def pytest_addoption(parser): group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", help="add directory for rsyncing to remote tx nodes.") + parser.addini('rsyncdirs', 'list of (relative) paths to be rsynced for' + ' remote distributed testing.', type="pathlist") + parser.addini('rsyncignore', 'list of (relative) paths to be ignored ' + 'for rsyncing.', type="pathlist") + # ------------------------------------------------------------------------- # distributed testing hooks # ------------------------------------------------------------------------- --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -168,12 +168,12 @@ class DSession: self.terminal = None def report_line(self, line): - if self.terminal: + if self.terminal and self.config.option.verbose >= 0: self.terminal.write_line(line) def pytest_sessionstart(self, session, __multicall__): #print "remaining multicall methods", __multicall__.methods - if not self.config.getvalue("verbose"): + if self.config.option.verbose > 0: self.report_line("instantiating gateways (use -v for details): %s" % ",".join(self.config.option.tx)) self.nodemanager = NodeManager(self.config) @@ -183,11 +183,11 @@ class DSession: """ teardown any resources after a test run. """ self.nodemanager.teardown_nodes() - def pytest_perform_collection(self, __multicall__): + def pytest_collection(self, __multicall__): # prohibit collection of test items in master process __multicall__.methods[:] = [] - def pytest_runtest_mainloop(self): + def pytest_runtestloop(self): numnodes = len(self.nodemanager.gwmanager.specs) dist = self.config.getvalue("dist") if dist == "load": @@ -316,13 +316,13 @@ class TerminalDistReporter: def pytest_gwmanage_newgateway(self, gateway): rinfo = gateway._rinfo() - if self.config.getvalue("verbose"): + if self.config.option.verbose >= 0: version = "%s.%s.%s" %rinfo.version_info[:3] self.write_line("[%s] %s Python %s cwd: %s" % ( gateway.id, rinfo.platform, version, rinfo.cwd)) def pytest_testnodeready(self, node): - if self.config.getvalue("verbose"): + if self.config.option.verbose >= 0: d = node.slaveinfo infoline = "[%s] Python %s" %( d['id'], --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -173,8 +173,9 @@ class TestNodeManager: source.ensure("dir1", "somefile", dir=1) dir2.ensure("hello") source.ensure("bogusdir", "file") - source.join("conftest.py").write(py.code.Source(""" - rsyncdirs = ['dir1/dir2'] + source.join("tox.ini").write(py.std.textwrap.dedent(""" + [pytest] + rsyncdirs=dir1/dir2 """)) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) @@ -189,9 +190,10 @@ class TestNodeManager: dir5 = source.ensure("dir5", "dir6", "bogus") dirf = source.ensure("dir5", "file") dir2.ensure("hello") - source.join("conftest.py").write(py.code.Source(""" - rsyncdirs = ['dir1', 'dir5'] - rsyncignore = ['dir1/dir2', 'dir5/dir6'] + source.join("tox.ini").write(py.std.textwrap.dedent(""" + [pytest] + rsyncdirs = dir1 dir5 + rsyncignore = dir1/dir2 dir5/dir6 """)) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ 1.5a1 ------------------------- - +- adapt to pytest-2.0 changes, rsyncdirs and rsyncignore can now + only be specified in [pytest] sections of ini files, see "py.test -h" + for details. - major internal refactoring to match the py-1.4 event refactoring - perform test collection always at slave side instead of at the master - make python2/python3 bridging work, remove usage of pickling --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -1,4 +1,4 @@ -import py +import py, pytest import sys, os import execnet import xdist.remote @@ -18,7 +18,7 @@ class NodeManager(object): self.config.hook.pytest_trace(category="nodemanage", msg=msg) def config_getignores(self): - return self.config.getconftest_pathlist("rsyncignore") + return self.config.getini("rsyncignore") def rsync_roots(self): """ make sure that all remote gateways @@ -78,7 +78,7 @@ class NodeManager(object): else: xspeclist.extend([xspec[i+1:]] * num) if not xspeclist: - raise config.Error( + raise pytest.UsageError( "MISSING test execution (tx) nodes: please specify --tx") return [execnet.XSpec(x) for x in xspeclist] @@ -86,14 +86,14 @@ class NodeManager(object): config = self.config candidates = [py._pydir] candidates += config.option.rsyncdir - conftestroots = config.getconftest_pathlist("rsyncdirs") + conftestroots = config.getini("rsyncdirs") if conftestroots: candidates.extend(conftestroots) roots = [] for root in candidates: root = py.path.local(root).realpath() if not root.check(): - raise config.Error("rsyncdir doesn't exist: %r" %(root,)) + raise pytest.UsageError("rsyncdir doesn't exist: %r" %(root,)) if root not in roots: roots.append(root) return roots --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -145,7 +145,7 @@ class SlaveFailSession: if self.config.option.debug: print(" ".join(map(str, args))) - def pytest_perform_collection(self, session): + def pytest_collection(self, session): self.session = session self.collection = session.collection self.topdir, self.trails = self.current_command --- a/xdist/remote.py +++ b/xdist/remote.py @@ -42,10 +42,10 @@ class SlaveInteractor: self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput) return res - def pytest_perform_collection(self, session): + def pytest_collection(self, session): self.sendevent("collectionstart") - def pytest_runtest_mainloop(self, session): + def pytest_runtestloop(self, session): self.log("entering main loop") while 1: name, kwargs = self.channel.receive() @@ -62,7 +62,7 @@ class SlaveInteractor: break return True - def pytest_log_finishcollection(self, collection): + def pytest_collection_finish(self, collection): ids = [collection.getid(item) for item in collection.items] self.sendevent("collectionfinish", topdir=str(collection.topdir), From commits-noreply at bitbucket.org Mon Nov 1 00:37:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 31 Oct 2010 18:37:50 -0500 (CDT) Subject: [py-svn] pytest commit b9dba72b749a: fix tests by using less likely existing import names Message-ID: <20101031233750.5A1271E103D@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288568324 -3600 # Node ID b9dba72b749a3bba95083da8bef0b5a562ca6e9f # Parent ad3773acb10a58745b9919cc37d4d720ef2ed971 fix tests by using less likely existing import names --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -85,8 +85,8 @@ class TestBootstrapping: def test_import_plugin_importname(self, testdir): pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("x.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_x.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') reset = testdir.syspathinsert() pluginname = "pytest_hello" @@ -103,8 +103,8 @@ class TestBootstrapping: def test_import_plugin_dotted_name(self, testdir): pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("x.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_x.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') reset = testdir.syspathinsert() testdir.mkpydir("pkg").join("plug.py").write("x=3") --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -295,8 +295,6 @@ class HookProxy: def __init__(self, node): self.node = node def __getattr__(self, name): - if name[0] == "_": - raise AttributeError(name) hookmethod = getattr(self.node.config.hook, name) def call_matching_hooks(**kwargs): plugins = self.node.config._getmatchingplugins(self.node.fspath) From commits-noreply at bitbucket.org Mon Nov 1 08:15:17 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 02:15:17 -0500 (CDT) Subject: [py-svn] pytest commit ff5f9cf9ff57: some test fixes and refinements Message-ID: <20101101071517.3E58F6C130B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288595770 -3600 # Node ID ff5f9cf9ff576554f02c8ecd1e4717a22dbe0728 # Parent 55b2bb6f2902b67d249b46ec6b35170acc6af7b6 some test fixes and refinements --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -2,16 +2,6 @@ import py, sys from pytest.plugin import python as funcargs class TestModule: - def test_module_file_not_found(self, testdir): - tmpdir = testdir.tmpdir - fn = tmpdir.join('nada','no') - config=testdir.Config() - config.args = ["hello"] - col = py.test.collect.Module(fn, config=config, - collection=testdir.Collection(config)) - col.config = testdir.parseconfig(tmpdir) - py.test.raises(py.error.ENOENT, col.collect) - def test_failing_import(self, testdir): modcol = testdir.getmodulecol("import alksdjalskdjalkjals") py.test.raises(ImportError, modcol.collect) --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -297,7 +297,7 @@ class Config(object): if self.inicfg: newargs = self.inicfg.get("addargs", None) if newargs: - args[:] = args + py.std.shlex.split(newargs) + args[:] = py.std.shlex.split(newargs) + args self._checkversion() self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() @@ -414,6 +414,7 @@ class Config(object): def getcfg(args, inibasenames): + args = [x for x in args if str(x)[0] != "-"] if not args: args = [py.path.local()] for inibasename in inibasenames: --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev14' +__version__ = '2.0.0.dev15' __all__ = ['config', 'cmdline'] --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -13,6 +13,11 @@ class TestGeneralUsage: '*ERROR: hello' ]) + def test_file_not_found(self, testdir): + result = testdir.runpytest("asd") + assert result.ret != 0 + result.stderr.fnmatch_lines(["ERROR: file not found*asd"]) + def test_config_preparse_plugin_option(self, testdir): testdir.makepyfile(pytest_xyz=""" def pytest_addoption(parser): --- a/testing/test_config.py +++ b/testing/test_config.py @@ -66,6 +66,9 @@ class TestConfigTmpdir: assert not config2.getbasetemp().relto(config3.getbasetemp()) assert not config3.getbasetemp().relto(config2.getbasetemp()) + def test_reparse_filename_too_long(self, testdir): + config = testdir.reparseconfig(["--basetemp=%s" % ("123"*300)]) + class TestConfigAPI: def test_config_getvalue_honours_conftest(self, testdir): --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev14', + version='2.0.0.dev15', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/tox.ini +++ b/tox.ini @@ -52,4 +52,4 @@ commands= [pytest] minversion=2.0 plugins=pytester -#addargs=-q -x +#addargs=-rf From commits-noreply at bitbucket.org Mon Nov 1 08:54:45 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 02:54:45 -0500 (CDT) Subject: [py-svn] pytest commit 37e6a9570c34: rename addargs to addopts, make adding of opts configurable Message-ID: <20101101075445.629051E1167@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288598114 -3600 # Node ID 37e6a9570c348624983020109d83287e0b37ddf7 # Parent ff5f9cf9ff576554f02c8ecd1e4717a22dbe0728 rename addargs to addopts, make adding of opts configurable --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -11,7 +11,7 @@ def pytest_cmdline_parse(pluginmanager, return config def pytest_addoption(parser): - parser.addini('addargs', 'default command line arguments') + parser.addini('addopts', 'default command line arguments') parser.addini('minversion', 'minimally required pytest version') class Parser: @@ -292,10 +292,11 @@ class Config(object): sys.stderr.write(err) raise - def _preparse(self, args): + def _preparse(self, args, addopts=True): + self.inicfg = {} self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) - if self.inicfg: - newargs = self.inicfg.get("addargs", None) + if self.inicfg and addopts: + newargs = self.inicfg.get("addopts", None) if newargs: args[:] = py.std.shlex.split(newargs) + args self._checkversion() --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev15' +__version__ = '2.0.0.dev16' __all__ = ['config', 'cmdline'] --- a/doc/customize.txt +++ b/doc/customize.txt @@ -45,13 +45,13 @@ builtin configuration file options minversion = 2.1 # will fail if we run with pytest-2.0 -.. confval:: addargs = OPTS +.. confval:: addopts = OPTS add the specified ``OPTS`` to the set of command line arguments as if they had been specified by the user. Example: if you have this ini file content:: [pytest] - addargs = --maxfail=2 -rf # exit after 2 failures, report fail info + addopts = --maxfail=2 -rf # exit after 2 failures, report fail info issuing ``py.test test_hello.py`` actually means:: --- a/testing/test_config.py +++ b/testing/test_config.py @@ -19,11 +19,15 @@ class TestParseIni: def test_append_parse_args(self, tmpdir): tmpdir.join("setup.cfg").write(py.code.Source(""" [pytest] - addargs = --verbose + addopts = --verbose """)) config = Config() config.parse([tmpdir]) assert config.option.verbose + config = Config() + args = [tmpdir,] + config._preparse(args, addopts=False) + assert len(args) == 1 def test_tox_ini_wrong_version(self, testdir): p = testdir.makefile('.ini', tox=""" --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -475,8 +475,8 @@ def test_getreportopt(): config.option.reportchars = "sfx" assert getreportopt(config) == "sfx" -def test_terminalreporter_reportopt_addargs(testdir): - testdir.makeini("[pytest]\naddargs=-rs") +def test_terminalreporter_reportopt_addopts(testdir): + testdir.makeini("[pytest]\naddopts=-rs") p = testdir.makepyfile(""" def pytest_funcarg__tr(request): tr = request.config.pluginmanager.getplugin("terminalreporter") --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev15', + version='2.0.0.dev16', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Mon Nov 1 08:55:51 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 02:55:51 -0500 (CDT) Subject: [py-svn] pytest-xdist commit 76db8f1c237c: don't add opts in sub processes, add -rf by default Message-ID: <20101101075551.A7BC26C1322@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1288598207 -3600 # Node ID 76db8f1c237c0e804b05bc974815854f9f18064d # Parent ba07fdbea9d953cf4f55db67e2e242527948019f don't add opts in sub processes, add -rf by default --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -82,7 +82,7 @@ class TestDistribution: "*1 passed*", ]) - def test_dist_conftest_specified(self, testdir): + def test_dist_ini_specified(self, testdir): p1 = testdir.makepyfile(""" import py def test_fail0(): @@ -95,8 +95,9 @@ class TestDistribution: py.test.skip("hello") """, ) - testdir.makeconftest(""" - option_tx = 'popen popen popen'.split() + testdir.makeini(""" + [pytest] + addopts = --tx=3*popen """) result = testdir.runpytest(p1, '-d', "-v") result.stdout.fnmatch_lines([ --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -49,14 +49,15 @@ class TestDistOptions: p = py.path.local() for bn in 'x y z'.split(): p.mkdir(bn) - testdir.makeconftest(""" - rsyncdirs= 'x', + testdir.makeini(""" + [pytest] + rsyncdirs= x """) config = testdir.parseconfigure( testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z') nm = NodeManager(config, specs=[execnet.XSpec("popen")]) roots = nm._getrsyncdirs() - assert len(roots) == 3 + 1 # pylib + #assert len(roots) == 3 + 1 # pylib assert py.path.local('y') in roots assert py.path.local('z') in roots assert testdir.tmpdir.join('x') in roots --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a2' +__version__ = '1.5a4' --- a/tox.ini +++ b/tox.ini @@ -15,3 +15,6 @@ commands= py.test -rsfxX \ deps= pytest pypi pexpect + +[pytest] +addopts = -rf --- a/xdist/remote.py +++ b/xdist/remote.py @@ -104,7 +104,7 @@ def getinfodict(): def remote_initconfig(option_dict, args): from pytest.plugin.config import Config config = Config() - config._preparse(args) + config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) config.option.looponfail = False config.option.usepdb = False --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a2', + version='1.5a4', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', From commits-noreply at bitbucket.org Mon Nov 1 09:20:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 03:20:02 -0500 (CDT) Subject: [py-svn] pytest commit 7d2db9cfe921: allow unregistration by name Message-ID: <20101101082002.6AE7A6C12A7@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288599658 -3600 # Node ID 7d2db9cfe9216159f9b41347e659fffca6095564 # Parent 37e6a9570c348624983020109d83287e0b37ddf7 allow unregistration by name --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev16', + version='2.0.0.dev17', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -164,7 +164,7 @@ class TestBootstrapping: assert pp.getplugin('hello') == a2 pp.unregister(a1) assert not pp.isregistered(a1) - pp.unregister(a2) + pp.unregister(name="hello") assert not pp.isregistered(a2) def test_pm_ordering(self): --- a/pytest/_core.py +++ b/pytest/_core.py @@ -47,9 +47,11 @@ class PluginManager(object): self._plugins.insert(0, plugin) return True - def unregister(self, plugin): + def unregister(self, plugin=None, name=None): + if plugin is None: + plugin = self.getplugin(name=name) + self._plugins.remove(plugin) self.hook.pytest_plugin_unregistered(plugin=plugin) - self._plugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev16' +__version__ = '2.0.0.dev17' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Mon Nov 1 09:22:19 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 03:22:19 -0500 (CDT) Subject: [py-svn] pytest-xdist commit 724fe27731c7: unregister terminal plugin in subprocesses before it gets configured Message-ID: <20101101082219.84D9F6C12A7@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1288599769 -3600 # Node ID 724fe27731c76cf8a91472cf8cdacfb336365765 # Parent 76db8f1c237c0e804b05bc974815854f9f18064d unregister terminal plugin in subprocesses before it gets configured --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -62,6 +62,7 @@ def test_remoteinitconfig(testdir): config1 = testdir.parseconfig() config2 = remote_initconfig(config1.option.__dict__, config1.args) assert config2.option.__dict__ == config1.option.__dict__ + py.test.raises(KeyError, 'config2.pluginmanager.getplugin("terminal")') class TestReportSerialization: def test_itemreport_outcomes(self, testdir): --- a/xdist/remote.py +++ b/xdist/remote.py @@ -104,6 +104,7 @@ def getinfodict(): def remote_initconfig(option_dict, args): from pytest.plugin.config import Config config = Config() + config.pluginmanager.unregister(name="terminal") config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) config.option.looponfail = False From commits-noreply at bitbucket.org Mon Nov 1 23:08:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 17:08:47 -0500 (CDT) Subject: [py-svn] pytest commit ac86d3a188b1: majorly changing the unittest compatibility code, calling TestCase(name)(result) Message-ID: <20101101220847.0E58C6C140C@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288649296 -3600 # Node ID ac86d3a188b1da11c3df0fe6054805e7cb625a83 # Parent 7d2db9cfe9216159f9b41347e659fffca6095564 majorly changing the unittest compatibility code, calling TestCase(name)(result) --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -383,7 +383,8 @@ class Function(FunctionMixin, pytest.col config=config, collection=collection) self._args = args if self._isyieldedfunction(): - assert not callspec, "yielded functions (deprecated) cannot have funcargs" + assert not callspec, ( + "yielded functions (deprecated) cannot have funcargs") else: if callspec is not None: self.funcargs = callspec.funcargs or {} --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev17' +__version__ = '2.0.0.dev18' __all__ = ['config', 'cmdline'] --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ Changes between 1.3.4 and 2.0.0dev0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 +- try harder to run unittest test suites in a more compatible manner + by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, by default setup.cfg and tox.ini files are searched. The old ways (certain environment variables, dynamic conftest.py reading --- a/testing/plugin/test_unittest.py +++ b/testing/plugin/test_unittest.py @@ -81,3 +81,25 @@ def test_module_level_pytestmark(testdir """) reprec = testdir.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) + +def test_class_setup(testdir): + testpath = testdir.makepyfile(""" + import unittest + import py + class MyTestCase(unittest.TestCase): + x = 0 + @classmethod + def setUpClass(cls): + cls.x += 1 + def test_func1(self): + assert self.x == 1 + def test_func2(self): + assert self.x == 1 + @classmethod + def tearDownClass(cls): + cls.x -= 1 + def test_teareddown(): + assert MyTestCase.x == 0 + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=3) --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -19,51 +19,31 @@ def pytest_pycollect_makeitem(collector, class UnitTestCase(py.test.collect.Class): def collect(self): - return [UnitTestCaseInstance("()", self)] + loader = py.std.unittest.TestLoader() + for name in loader.getTestCaseNames(self.obj): + yield TestCaseFunction(name, parent=self) def setup(self): - pass + meth = getattr(self.obj, 'setUpClass', None) + if meth is not None: + meth() def teardown(self): + meth = getattr(self.obj, 'tearDownClass', None) + if meth is not None: + meth() + +class TestCaseFunction(py.test.collect.Function): + def startTest(self, testcase): pass - -_dummy = object() -class UnitTestCaseInstance(py.test.collect.Instance): - def collect(self): - loader = py.std.unittest.TestLoader() - names = loader.getTestCaseNames(self.obj.__class__) - l = [] - for name in names: - callobj = getattr(self.obj, name) - if py.builtin.callable(callobj): - l.append(UnitTestFunction(name, parent=self)) - return l - - def _getobj(self): - x = self.parent.obj - return self.parent.obj(methodName='run') - -class UnitTestFunction(py.test.collect.Function): - def __init__(self, name, parent, args=(), obj=_dummy, sort_value=None): - super(UnitTestFunction, self).__init__(name, parent) - self._args = args - if obj is not _dummy: - self._obj = obj - self._sort_value = sort_value - if hasattr(self.parent, 'newinstance'): - self.parent.newinstance() - self.obj = self._getobj() - + def addError(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addFailure(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addSuccess(self, testcase): + pass + def stopTest(self, testcase): + pass def runtest(self): - target = self.obj - args = self._args - target(*args) - - def setup(self): - instance = py.builtin._getimself(self.obj) - instance.setUp() - - def teardown(self): - instance = py.builtin._getimself(self.obj) - instance.tearDown() - + testcase = self.parent.obj(self.name) + testcase(result=self) --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev17', + version='2.0.0.dev18', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Tue Nov 2 00:53:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 18:53:02 -0500 (CDT) Subject: [py-svn] pytest commit 9a0938131cfa: massive documentation refinements Message-ID: <20101101235302.6BFDA241235@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288655633 -3600 # Node ID 9a0938131cfa4fb581bfbe872bff1625dfbad4d2 # Parent ac86d3a188b1da11c3df0fe6054805e7cb625a83 massive documentation refinements --- a/doc/index.txt +++ b/doc/index.txt @@ -1,5 +1,8 @@ py.test: no-boilerplate testing with Python ============================================== + +.. todolist:: + Welcome to ``py.test`` documentation: @@ -8,7 +11,7 @@ Welcome to ``py.test`` documentation: overview apiref - customize + plugins examples talks develop --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -32,7 +32,7 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_simplefactory.py test_simplefactory.py F @@ -133,7 +133,7 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_example.py test_example.py .........F @@ -154,7 +154,7 @@ Note that the ``pytest_generate_tests(me the test collection phase. You can have a look at it with this:: $ py.test --collectonly test_example.py - + @@ -171,14 +171,31 @@ If you want to select only the run with $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python test path 1: test_example.py + test_example.py:6: test_func[0] PASSED + test_example.py:6: test_func[1] PASSED + test_example.py:6: test_func[2] PASSED + test_example.py:6: test_func[3] PASSED + test_example.py:6: test_func[4] PASSED + test_example.py:6: test_func[5] PASSED + test_example.py:6: test_func[6] PASSED test_example.py:6: test_func[7] PASSED + test_example.py:6: test_func[8] PASSED + test_example.py:6: test_func[9] FAILED - ======================== 9 tests deselected by '7' ========================= - ================== 1 passed, 9 deselected in 0.01 seconds ================== - + ================================= FAILURES ================================= + _______________________________ test_func[9] _______________________________ + + numiter = 9 + + def test_func(numiter): + > assert numiter < 9 + E assert 9 < 9 + + test_example.py:7: AssertionError + ==================== 1 failed, 9 passed in 0.04 seconds ==================== .. _`metafunc object`: --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,8 +88,8 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-11 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-171 test_mark.py .. test_mark_classlevel.py .. @@ -100,8 +100,8 @@ And you can also run all tests except th $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-11 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-171 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-11 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-171 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -1,11 +1,11 @@ -unittest.py style testing integration +unittest.TestCase support ===================================================================== py.test has limited support for running Python `unittest.py style`_ tests. It will automatically collect ``unittest.TestCase`` subclasses and their ``test`` methods in test files. It will invoke ``setUp/tearDown`` methods but also perform py.test's standard ways -of treating tests like IO capturing:: +of treating tests like e.g. IO capturing:: # content of test_unittest.py @@ -21,7 +21,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_unittest.py test_unittest.py F @@ -55,7 +55,5 @@ Running it yields:: hello ========================= 1 failed in 0.02 seconds ========================= -This plugin is enabled by default. - .. _`unittest.py style`: http://docs.python.org/library/unittest.html --- a/doc/cmdline.txt +++ b/doc/cmdline.txt @@ -11,9 +11,7 @@ Getting help on version, option names, e py.test --version # shows where pytest was imported from py.test --funcargs # show available builtin function arguments - py.test --help-config # show configuration values - - py.test -h | --help # show help + py.test -h | --help # show help on command line and config file options Stopping after the first (or N) failures --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -26,7 +26,7 @@ Running this would result in a passed te $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_tmpdir.py test_tmpdir.py F @@ -34,7 +34,7 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-427/test_create_file0') + tmpdir = local('/tmp/pytest-1248/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -45,7 +45,7 @@ Running this would result in a passed te E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.03 seconds ========================= + ========================= 1 failed in 0.04 seconds ========================= .. _`base temporary directory`: --- a/doc/Makefile +++ b/doc/Makefile @@ -14,6 +14,9 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctree .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest +regen: + COLUMNS=76 regendoc --update *.txt */*.txt + help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -247,7 +247,8 @@ class Config(object): basetemp = None def __init__(self, pluginmanager=None): - #: command line option values + #: command line option values, usually added via parser.addoption(...) + #: or parser.getgroup(...).addoption(...) calls self.option = CmdOptions() self._parser = Parser( usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", @@ -404,7 +405,7 @@ class Config(object): return self._getconftest(name, path, check=False) def getvalueorskip(self, name, path=None): - """ return getvalue(name) or call py.test.skip if no value exists. """ + """ (deprecated) return getvalue(name) or call py.test.skip if no value exists. """ try: val = self.getvalue(name, path) if val is None: --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -2,51 +2,91 @@ .. highlightlang:: python .. _`good practises`: -Good Practises +Good Integration Practises ================================================= -Recommendation: install tool and dependencies virtually +work with virtual environments ----------------------------------------------------------- -We recommend to work with virtual environments -(e.g. virtualenv_ or buildout_ based) and use easy_install_ -(or pip_) for installing py.test/pylib and any dependencies -you need to run your tests. Local virtual Python environments -(as opposed to system-wide "global" environments) make for a more -reproducible and reliable test environment. +We recommend to work with virtualenv_ environments and use easy_install_ +(or pip_) for installing your application dependencies as well as +the ``pytest`` package itself. This way you get a much more reproducible +environment. A good tool to help you automate test runs against multiple +dependency configurations or Python interpreters is `tox`_, +independently created by the main py.test author. The latter +is also useful for integration with the continous integration +server Hudson_. .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip + +Choosing a test layout / import rules +------------------------------------------ + +py.test supports common test layouts: + +* inlining test directories into your application package, useful if you want to + keep (unit) tests and actually tested code close together:: + + mypkg/ + __init__.py + appmodule.py + ... + test/ + test_app.py + ... + +* putting tests into an extra directory outside your actual application + code, useful if you have many functional tests or want to keep + tests separate from actual application code:: + + mypkg/ + __init__.py + appmodule.py + tests/ + test_app.py + ... + +You can always run your tests by pointing to it:: + + py.test tests/test_app.py # for external test dirs + py.test mypkg/test/test_app.py # for inlined test dirs + py.test mypkg # run tests in all below test directories + py.test # run all tests below current dir + ... + +.. note:: + + Test modules are imported under their fully qualified name as follows: + + * ``basedir`` = first upward directory not containing an ``__init__.py`` + + * perform ``sys.path.insert(0, basedir)``. + + * ``import path.to.test_module`` + .. _standalone: - - -Choosing a test layout ----------------------------- - -py.test supports common test layouts. - -XXX - .. _`genscript method`: Generating a py.test standalone Script ------------------------------------------- -If you are a maintainer or application developer and want users -to run tests you can use a facility to generate a standalone -"py.test" script that you can tell users to run:: +If you are a maintainer or application developer and want others +to easily run tests you can generate a completely standalone "py.test" +script:: py.test --genscript=runtests.py -will generate a ``mytest`` script that is, in fact, a ``py.test`` under -disguise. You can tell people to download and then e.g. run it like this:: +generates a ``runtests.py`` script which is a fully functional basic +``py.test`` script, running unchanged under Python2 and Python3. +You can tell people to download and then e.g. run it like this to +produce a Paste URL:: python runtests.py --pastebin=all -and ask them to send you the resulting URL. The resulting script has -all core features and runs unchanged under Python2 and Python3 interpreters. +and ask them to send you the resulting URL. .. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions .. _`distribute installation`: http://pypi.python.org/pypi/distribute --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,11 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-288 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-197 - conftest.py . - example.rst . - mymodule.py . - - ========================= 3 passed in 0.01 seconds ========================= + ============================= in 0.00 seconds ============================= --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,12 +122,12 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-9/conftest.py:22: 'specify ssh host with --ssh' + SKIP [1] /tmp/doc-exec-198/conftest.py:22: 'specify ssh host with --ssh' ======================== 1 skipped in 0.02 seconds ========================= --- /dev/null +++ b/doc/plugins.txt @@ -0,0 +1,282 @@ +Writing, managing and understanding plugins +============================================= + +.. _`local plugin`: + +py.test 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 locations types:: + +* builtin plugins: loaded from py.test's own `pytest/plugin`_ directory. +* `external plugins`_: modules discovered through `setuptools entry points`_ +* `conftest.py plugins`_: modules auto-discovered in test directories + +.. _`pytest/plugin`: http://bitbucket.org/hpk42/pytest/src/tip/pytest/plugin/ +.. _`conftest.py plugins`: + +conftest.py: local per-directory plugins +-------------------------------------------------------------- + +local ``conftest.py`` plugins contain directory-specific hook +implementations. Collection and test running activities will +invoke all hooks defined in "higher up" ``conftest.py`` files. +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_in_subdir.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" + +A note on ordering: ``py.test`` loads all ``conftest.py`` files upwards +from the command line file arguments. It usually performs look up +right-to-left, i.e. the hooks in "closer" conftest files will be called +earlier than further away ones. + +.. 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 ambigous 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. + +.. _`installing plugins`: +.. _`external plugins`: + +Installing External Plugins +------------------------------------------------------ + +Installing a plugin happens through any usual Python installation +tool, for example:: + + pip install pytest-NAME + pip uninstall pytest-NAME + +If a plugin is installed, py.test automatically finds and integrates it, +there is no need to activate it. If you don't need a plugin anymore simply +de-install it. You can find a list of valid plugins through a +`pytest- pypi.python.org search`_. + +.. _`available installable plugins`: +.. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search + +.. _`setuptools entry points`: + +Writing an installable plugin +------------------------------------------------------ + +.. _`Distribute`: http://pypi.python.org/pypi/distribute +.. _`setuptools`: http://pypi.python.org/pypi/setuptools + +If you want to write a plugin, there are many real-life examples +you can copy from: + +* around 20 `builtin plugins`_ which comprise py.test's own functionality +* around 10 `external plugins`_ providing additional features + +If you want to make your plugin externally available, you +may define a so called entry point for your distribution so +that ``py.test`` finds your plugin module. Entry points are +a feature that is provided by `setuptools`_ or `Distribute`_. +The concrete entry point is ``pytest11``. To make your plugin +available you can insert the following lines in your +setuptools/distribute-based setup-invocation: + +.. sourcecode:: python + + # sample ./setup.py file + from setuptools import setup + + setup( + name="myproject", + packages = ['myproject'] + + # the following makes a plugin available to py.test + entry_points = { + 'pytest11': [ + 'name_of_plugin = myproject.pluginmodule', + ] + }, + ) + +If a package is installed this way, py.test will load +``myproject.pluginmodule`` and accordingly call functions +if they match the `well specified hooks`_. + +Plugin discovery order at tool startup +-------------------------------------------- + +py.test 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 (test files and all of its *parent* directories). + Note that ``conftest.py`` files from *sub* directories are by default + not loaded at tool startup. + +* 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", + +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 py.test plugin. + +.. _`setuptools entry points`: +.. _registered: + + +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. + +.. _`well specified hooks`: + +py.test hook reference +==================================== + +hook specification and validation +----------------------------------------- + +py.test calls hook functions to implement initialization, running, +test execution and reporting. When py.test loads a plugin it validates +that all hook functions conform to their respective hook specification. +Each hook function name and its argument names need to match a hook +specification exactly but it is allowed for a hook function to accept +*less* parameters than specified. If you mistype argument names or the +hook name itself you get useful errors. + +initialisation, command line and configuration hooks +-------------------------------------------------------------------- + +.. currentmodule:: pytest.hookspec + +.. autofunction:: pytest_cmdline_parse +.. autofunction:: pytest_namespace +.. autofunction:: pytest_addoption +.. autofunction:: pytest_cmdline_main +.. autofunction:: pytest_configure +.. autofunction:: pytest_unconfigure + +generic "runtest" hooks +------------------------------ + +All all runtest related hooks receive a :py:class:`pytest.collect.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.plugin.runner` and maybe also +in :py:mod:`pytest.plugin.pdb` which intercepts creation +of reports in order to drop to interactive debugging. + +The :py:mod:`pytest.plugin.terminal` reported specifically uses +the reporting hook to print information about a test run. + +collection hooks +------------------------------ + +py.test 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 + + +reporting hooks +------------------------------ + +Collection 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 + + +Reference of important objects involved in hooks +=========================================================== + +.. autoclass:: pytest.plugin.config.Config + :members: + +.. autoclass:: pytest.plugin.config.Parser + :members: + +.. autoclass:: pytest.plugin.session.Node(name, parent) + :members: + +.. + .. autoclass:: pytest.plugin.session.File(fspath, parent) + :members: + + .. autoclass:: pytest.plugin.session.Item(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Module(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Class(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Function(name, parent) + :members: + +.. autoclass:: pytest.plugin.runner.CallInfo + :members: + +.. autoclass:: pytest.plugin.runner.TestReport + :members: + + --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-296 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-172 ============================= in 0.00 seconds ============================= --- a/doc/faq.txt +++ b/doc/faq.txt @@ -1,87 +1,56 @@ -Frequent Issues and Questions +Some Issues and Questions ================================== -.. _`installation issues`: +.. note:: -Installation issues ------------------------------- - -easy_install or pip not found? -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Consult distribute_ to install the ``easy_install`` tool on your machine. -You may also use the original but somewhat older `setuptools`_ project -although we generally recommend to use ``distribute`` because it contains -more bug fixes and also works for Python3. - -For Python2 you can also consult pip_ for the popular ``pip`` tool. - -However, If you want to install on Python3 you need to use Distribute_ which -provides the ``easy_install`` utility. - - -py.test not found on Windows despite installation? -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html - - -- **Windows**: If "easy_install" or "py.test" are not found - please see here for preparing your environment for running - command line tools: `Python for Windows`_. You may alternatively - use an `ActivePython install`_ which makes command line tools - automatically available under Windows. - -.. _`ActivePython install`: http://www.activestate.com/activepython/downloads - -.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 - -- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ - so ``py.test`` will not work correctly. You may install py.test on - CPython and type ``py.test --genscript=mytest`` and then use - ``jython mytest`` to run py.test for your tests to run in Jython. + If you don't find an answer here, checkout the :ref:`contact channels` + to get help. On naming, nosetests, licensing and magic XXX ------------------------------------------------ -Why the ``py.test`` naming, why not ``pytest``? +Why a ``py.test`` instead of a ``pytest`` command? ++++++++++++++++++++++++++++++++++++++++++++++++++ -XXX - -because of TAB-completion under Bash/Shells. If you hit -``py.`` you'll get a list of available development -tools that all share the ``py.`` prefix. Another motivation -was to unify the package ("py.test") and tool filename. - +Some historic, some practical reasons: ``py.test`` used to be part of +the ``py`` package which provided several developer utitilities, +all starting with ``py.``, providing nice TAB-completion. If +you install ``pip install pycmd`` you get these tools from a separate +package. These days the command line tool could be ``pytest`` +but then many people have gotten used to the old name and there +also is another tool with this same which would lead to some clashes. What's py.test's relation to ``nosetests``? +++++++++++++++++++++++++++++++++++++++++++++++++ py.test and nose_ share basic philosophy when it comes -to running Python tests. In fact, -with py.test-1.1.0 it is ever easier to run many test suites -that currently work with ``nosetests``. nose_ was created +to running Python tests. In fact, you can run many tests +written for unittest or nose with py.test. nose_ was originally created as a clone of ``py.test`` when py.test was in the ``0.8`` release -cycle so some of the newer features_ introduced with py.test-1.0 -and py.test-1.1 have no counterpart in nose_. +cycle. .. _features: test/features.html -.. _apipkg: http://pypi.python.org/pypi/apipkg What's this "magic" with py.test? ++++++++++++++++++++++++++++++++++++++++++ -Around 2007 it was claimed that py.test was magic implementation -wise XXX. It has been refactored. +Around 2007 (version ``0.8``) some several people claimed that py.test +was using too much "magic". It has been refactored a lot. It is today +probably one of the smallest, most universally runnable and most +customizable testing frameworks for Python. It remains true +that ``py.test`` uses metaprogramming techniques, i.e. it views +test code similar to how compilers view programs, using a +somewhat abstract internal model. -* when an ``assert`` statement fails, py.test re-interprets the expression - to show intermediate values if a test fails. If your expression - has side effects the intermediate values may not be the same, obfuscating - the initial error (this is also explained at the command line if it happens). - ``py.test --no-assert`` turns off assert re-intepretation. - Sidenote: it is good practise to avoid asserts with side effects. +It's also true that the no-boilerplate testing is implemented by making +use of the Python assert statement through "re-interpretation": +When an ``assert`` statement fails, py.test re-interprets the expression +to show intermediate values if a test fails. If your expression +has side effects the intermediate values may not be the same, obfuscating +the initial error (this is also explained at the command line if it happens). +``py.test --no-assert`` turns off assert re-intepretation. +Sidenote: it is good practise to avoid asserts with side effects. .. _`py namespaces`: index.html .. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py @@ -92,10 +61,9 @@ function arguments, parametrized tests a .. _funcargs: test/funcargs.html -Is using funcarg- versus xUnit-based setup a style question? +Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -XXX For simple applications and for people experienced with nose_ or unittest-style test setup using `xUnit style setup`_ feels natural. For larger test suites, parametrized testing --- a/distribute_setup.py +++ b/distribute_setup.py @@ -46,7 +46,7 @@ except ImportError: args = [quote(arg) for arg in args] return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 -DEFAULT_VERSION = "0.6.13" +DEFAULT_VERSION = "0.6.14" DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" SETUPTOOLS_FAKED_VERSION = "0.6c11" --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -1,4 +1,4 @@ -Getting Started +Installation and Getting Started =================================== .. _`easy_install`: @@ -20,7 +20,7 @@ To check your installation has installed If you get an error, checkout :ref:`installation issues`. -Writing a simple test function with an assertion +Our first test run ---------------------------------------------------------- Let's create a small file with a test function testing a function @@ -32,17 +32,17 @@ computes a certain value:: def test_answer(): assert func(3) == 5 -Now you can execute the test function:: +You can execute the test function:: $ py.test test_sample.py - ========================= test session starts ========================== - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev4 + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_sample.py test_sample.py F - =============================== FAILURES =============================== - _____________________________ test_answer ______________________________ + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ def test_answer(): > assert func(3) == 5 @@ -50,23 +50,27 @@ Now you can execute the test function:: E + where 4 = func(3) test_sample.py:4: AssertionError - ======================= 1 failed in 0.02 seconds ======================= + ========================= 1 failed in 0.02 seconds ========================= -We got a failure because our little ``func(3)`` call did not return ``5``. -A few notes on this little test invocation: +We told py.test to run the ``test_sample.py`` file and it :ref:`discovered` the +``test_answer`` function because of the ``test_`` prefix. We got a +failure because our little ``func(3)`` call did not return ``5``. -* ``test_answer`` was identified as a test function because of the - ``test_`` prefix, +.. note:: -* we conveniently used the standard `assert statement`_ and the failure - report shows us the intermediate values. + You can simply use the `assert statement`_ for coding expectations because + intermediate values will be presented to you. Or to put it bluntly, + there is no need to learn all `the JUnit legacy methods`_ for expressing + assertions. + +.. _`the JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases .. _`assert statement`: http://docs.python.org/reference/simple_stmts.html#the-assert-statement -Asserting that a certain exception is raised +Asserting a certain exception is raised -------------------------------------------------------------- -If you want to assert a test raises a certain exception you can +If you want to assert some code raises an exception you can use the ``raises`` helper:: # content of test_sysexit.py @@ -78,18 +82,49 @@ use the ``raises`` helper:: with py.test.raises(SystemExit): f() -Running it with:: +Running it with, this time in "quiet" reporting mode:: - $ py.test test_sysexit.py - ========================= test session starts ========================== - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev4 - test path 1: test_sysexit.py + $ py.test -q test_sysexit.py + . + 1 passed in 0.01 seconds + +.. todo:: For further ways to assert exceptions see the :pyfunc:`raises` + +Grouping multiple tests in a class +-------------------------------------------------------------- + +If you start to have more than a few tests it often makes sense +to group tests logically, in classes and modules. Let's put two +tests in a class like this:: + + # content of test_class.py + class TestClass: + def test_one(self): + x = "this" + assert 'h' in x + + def test_two(self): + x = "hello" + assert hasattr(x, 'check') + +The two tests will be discovered because of the default `automatic test +discovery`_. There is no need to subclass anything. If we now run +the module we'll see one passed and one failed test:: + + $ py.test -q test_class.py + .F + ================================= FAILURES ================================= + ____________________________ TestClass.test_two ____________________________ - test_sysexit.py . + self = - ======================= 1 passed in 0.01 seconds ======================= - -.. For further ways to assert exceptions see the :pyfunc:`raises` + def test_two(self): + x = "hello" + > assert hasattr(x, 'check') + E assert hasattr('hello', 'check') + + test_class.py:8: AssertionError + 1 failed, 1 passed in 0.02 seconds where to go from here ------------------------------------- @@ -99,6 +134,47 @@ Here are a few suggestions where to go n * :ref:`cmdline` for command line invocation examples * :ref:`good practises` for virtualenv, test layout, genscript support * :ref:`apiref` for documentation and examples on writing Python tests -* :ref:`examples` for more complex examples + +.. _`installation issues`: + +Installation issues +------------------------------ + +easy_install or pip not found? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Consult distribute_ to install the ``easy_install`` tool on your machine. +You may also use the original but somewhat older `setuptools`_ project +although we generally recommend to use ``distribute`` because it contains +more bug fixes and also works for Python3. + +For Python2 you can also consult pip_ for the popular ``pip`` tool. + +However, If you want to install on Python3 you need to use Distribute_ which +provides the ``easy_install`` utility. + + +py.test not found on Windows despite installation? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html + + +- **Windows**: If "easy_install" or "py.test" are not found + please see here for preparing your environment for running + command line tools: `Python for Windows`_. You may alternatively + use an `ActivePython install`_ which makes command line tools + automatically available under Windows. + +.. _`ActivePython install`: http://www.activestate.com/activepython/downloads + +.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 + +- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ + so ``py.test`` will not work correctly. You may install py.test on + CPython and type ``py.test --genscript=mytest`` and then use + ``jython mytest`` to run py.test for your tests to run in Jython. + + :ref:`examples` for more complex examples .. include:: links.inc --- a/doc/customize.txt +++ b/doc/customize.txt @@ -1,7 +1,3 @@ -================================================ -Customizing and Extending py.test -================================================ - basic test configuration =================================== @@ -58,309 +54,4 @@ builtin configuration file options py.test --maxfail=2 -rf test_hello.py .. _`function arguments`: funcargs.html -.. _`extensions`: -Plugin basics and project configuration -============================================= - -.. _`local plugin`: - -py.test implements all aspects of its functionality by calling `well specified -hooks`_. Hook functions are discovered in :file:`conftest.py` files or in -`named plugins`_. :file:`conftest.py` files are useful for keeping test -extensions and customizations close to test code. - -local conftest.py plugins --------------------------------------------------------------- - -local ``conftest.py`` plugins contain directory-specific hook implemenations. Its contained runtest- and collection- related hooks are called when collecting or running tests in files or directories next to or below the ``conftest.py`` -file. Example: Assume the following layout and content of files:: - - a/conftest.py: - def pytest_runtest_setup(item): - print ("setting up", item) - - a/test_in_subdir.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" - -``py.test`` loads all ``conftest.py`` files upwards from the command -line file arguments. It usually performs look up right-to-left, i.e. -the hooks in "closer" conftest files will be called earlier than further -away ones. This means you can even have a ``conftest.py`` file in your home -directory to customize test functionality globally for all of your projects. - -.. 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 ambigous because there might be other - ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. - It is good practise for projects to put ``conftest.py`` within a package - scope or to never import anything from the conftest.py file. - -.. _`named plugins`: plugin/index.html - - -Plugin discovery at tool startup --------------------------------------------- - -py.test loads plugin modules at tool startup in the following way: - -* 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 (test files and all of its *parent* directories). - Note that ``conftest.py`` files from *sub* directories are by default - not loaded at tool startup. - -* 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", - -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 py.test plugin. - -.. _`setuptools entry points`: -.. _registered: - -Writing setuptools-registered plugins ------------------------------------------------------- - -.. _`Distribute`: http://pypi.python.org/pypi/distribute -.. _`setuptools`: http://pypi.python.org/pypi/setuptools - -If you want to make your plugin publically available, you -can use `setuptools`_ or `Distribute`_ which both allow -to register an entry point. ``py.test`` will register -all objects with the ``pytest11`` entry point. -To make your plugin available you may insert the following -lines in your setuptools/distribute-based setup-invocation: - -.. sourcecode:: python - - # sample ./setup.py file - from setuptools import setup - - setup( - name="myproject", - packages = ['myproject'] - - # the following makes a plugin available to py.test - entry_points = { - 'pytest11': [ - 'name_of_plugin = myproject.pluginmodule', - ] - }, - ) - -If a package is installed with this setup, py.test will load -``myproject.pluginmodule`` under the ``name_of_plugin`` name -and use it as a 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. - -.. _`well specified hooks`: - -py.test hook reference -==================================== - -hook specification and validation ------------------------------------------ - -py.test calls hook functions to implement initialization, running, -test execution and reporting. When py.test loads a plugin it validates -that all hook functions conform to their respective hook specification. -Each hook function name and its argument names need to match a hook -specification exactly but it is allowed for a hook function to accept -*less* parameters than specified. If you mistype argument names or the -hook name itself you get useful errors. - -initialisation, command line and configuration hooks --------------------------------------------------------------------- - -.. currentmodule:: pytest.hookspec - -.. autofunction:: pytest_cmdline_parse -.. autofunction:: pytest_namespace -.. autofunction:: pytest_addoption -.. autofunction:: pytest_cmdline_main -.. autofunction:: pytest_configure -.. autofunction:: pytest_unconfigure - -generic "runtest" hooks ------------------------------- - -All all runtest related hooks receive a :py:class:`pytest.collect.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.plugin.runner` and maybe also -in :py:mod:`pytest.plugin.pdb` which intercepts creation -of reports in order to drop to interactive debugging. - -The :py:mod:`pytest.plugin.terminal` reported specifically uses -the reporting hook to print information about a test run. - -collection hooks ------------------------------- - -py.test 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 - - -reporting hooks ------------------------------- - -Collection 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 - -The test collection tree -====================================================== - - -Default filesystem test discovery ------------------------------------------------ - -Test collection starts from specified paths or from the current -directory. All tests are collected ahead of running the first test. -(This used to be different in earlier versions of ``py.test`` where -collection and running was interweaved which made test randomization -and distributed testing harder). - -Collection nodes which have children are called "Collectors" and otherwise -they are called "Items" or "test items". Here is an example of such a -tree:: - - example $ py.test --collectonly test_collectonly.py - - - - - - - - -By default all directories not starting with a dot are traversed, -looking for ``test_*.py`` and ``*_test.py`` files. Those Python -files are imported under their `package name`_. - -The Module collector looks for test functions -and test classes and methods. Test functions and methods -are prefixed ``test`` by default. Test classes must -start with a capitalized ``Test`` prefix. - -Customizing error messages -------------------------------------------------- - -On test and collection nodes ``py.test`` will invoke -the ``node.repr_failure(excinfo)`` function which -you may override and make it return an error -representation string of your choice. It -will be reported as a (red) string. - -.. _`package name`: - -constructing the package name for test modules -------------------------------------------------- - -Test modules are imported under their fully qualified -name. Given a filesystem ``fspath`` it is constructed as follows: - -* walk the directories up to the last one that contains - an ``__init__.py`` file. - -* perform ``sys.path.insert(0, basedir)``. - -* import the root package as ``root`` - -Reference of important objects involved in hooks -=========================================================== - -.. autoclass:: pytest.plugin.config.Config - :members: - -.. autoclass:: pytest.plugin.config.Parser - :members: - -.. autoclass:: pytest.plugin.session.Node(name, parent) - :members: - -.. - .. autoclass:: pytest.plugin.session.File(fspath, parent) - :members: - - .. autoclass:: pytest.plugin.session.Item(name, parent) - :members: - - .. autoclass:: pytest.plugin.python.Module(name, parent) - :members: - - .. autoclass:: pytest.plugin.python.Class(name, parent) - :members: - - .. autoclass:: pytest.plugin.python.Function(name, parent) - :members: - -.. autoclass:: pytest.plugin.runner.CallInfo - :members: - -.. autoclass:: pytest.plugin.runner.TestReport - :members: - - --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -7,6 +7,7 @@ py.test reference documentation .. toctree:: :maxdepth: 2 + customize.txt assert.txt funcargs.txt xunit_setup.txt @@ -15,7 +16,7 @@ py.test reference documentation tmpdir.txt skipping.txt mark.txt - recwarn.txt + recwarn.txt + unittest.txt doctest.txt - unittest.txt --- a/doc/contact.txt +++ b/doc/contact.txt @@ -1,23 +1,25 @@ -Contact and Communication points + +.. _`contact channels`: + +Contact channels =================================== -- `py-dev developers list`_ announcements and discussions. +- `new issue tracker`_ to report bugs or suggest features. + See also the `old issue tracker`_ but don't submit bugs there. + +- `Testing In Python`_: a mailing list for Python testing tools and discussion. + +- `py-dev developers list`_ pytest specific announcements and discussions. - #pylib on irc.freenode.net IRC channel for random questions. - - `tetamap`_: Holger Krekel's blog, often about testing and py.test related news. - -- `Testing In Python`_: a mailing list for testing tools and discussion. - -- `commit mailing list`_ or `@pylibcommit`_ to follow development commits, - -- `bitbucket issue tracker`_ use this bitbucket issue tracker to report - bugs or request features. +- `commit mailing list`_ - `merlinux.eu`_ offers on-site teaching and consulting services. -.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/py-trunk/issues/ +.. _`new issue tracker`: http://bitbucket.org/hpk42/pytest/issues/ +.. _`old issue tracker`: http://bitbucket.org/hpk42/py-trunk/issues/ .. _`merlinux.eu`: http://merlinux.eu @@ -28,20 +30,8 @@ Contact and Communication points .. _`@pylibcommit`: http://twitter.com/pylibcommit -.. - get an account on codespeak - --------------------------- - - codespeak_ is where the subversion repository is hosted. If you know - someone who is active on codespeak already or you are otherwise known in - the community (see also: FOAF_) you will get access. But even if - you are new to the python developer community please come to the IRC - or the mailing list and ask questions, get involved. - .. _`Testing in Python`: http://lists.idyll.org/listinfo/testing-in-python .. _FOAF: http://en.wikipedia.org/wiki/FOAF -.. _us: http://codespeak.net/mailman/listinfo/py-dev -.. _codespeak: http://codespeak.net/ .. _`py-dev`: .. _`development mailing list`: .. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev --- a/doc/xunit_setup.txt +++ b/doc/xunit_setup.txt @@ -54,13 +54,13 @@ Similarly, the following methods are cal def setup_method(self, method): """ setup up any state tied to the execution of the given - method in a class. setup_method is invoked for every - test method of a class. + method in a class. setup_method is invoked for every + test method of a class. """ def teardown_method(self, method): """ teardown any state that was previously setup - with a setup_method call. + with a setup_method call. """ If you rather define test functions directly at module level @@ -68,12 +68,13 @@ you can also use the following functions def setup_function(function): """ setup up any state tied to the execution of the given - function. Invoked for every test function in the module. + function. Invoked for every test function in the module. """ def teardown_method(function): """ teardown any state that was previously setup - with a setup_function call. + with a setup_function call. + """ Note that it possible that setup/teardown pairs are invoked multiple times per testing process. --- a/doc/links.inc +++ b/doc/links.inc @@ -16,3 +16,4 @@ .. _`pip`: http://pypi.python.org/pypi/pip .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _hudson: http://hudson-ci.org/ +.. _tox: http://codespeak.net/tox --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,8 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', + 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -36,12 +36,12 @@ and when running it will see a skipped " $ py.test test_module.py -rs # "-rs" means report on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-12/conftest.py:9: 'need --runslow option to run' + SKIP [1] /tmp/doc-exec-195/conftest.py:9: 'need --runslow option to run' =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_module.py test_module.py .. --- a/doc/features.txt +++ b/doc/features.txt @@ -4,9 +4,11 @@ py.test Features no-boilerplate testing with Python ---------------------------------- -- automatic customizable Python test discovery +- automatic, fully customizable Python test discovery +- :pep:`8` consistent testing style +- allows simple test functions +- ``assert`` statement for your assertions - powerful parametrization of test functions -- use the ``assert`` statement for your assertions - rely on powerful traceback and assertion reporting - use ``print`` or ``pdb`` debugging on failures --- a/doc/assert.txt +++ b/doc/assert.txt @@ -1,5 +1,5 @@ -Writing easy assertions in tests +Writing and reporting of assertions in tests ============================================ assert with the ``assert`` statement @@ -21,7 +21,7 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_assert1.py test_assert1.py F @@ -101,7 +101,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_assert2.py test_assert2.py F --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -25,8 +25,8 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev10 - test path 1: /home/hpk/p/pytest/doc/example/nonpython + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: test_simple.yml test_simple.yml .F @@ -35,7 +35,7 @@ now execute the test specification:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.37 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -45,7 +45,7 @@ reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev10 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython test_simple.yml:1: usecase: ok PASSED @@ -56,7 +56,7 @@ reporting in ``verbose`` mode:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.07 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: --- /dev/null +++ b/doc/discovery.txt @@ -0,0 +1,45 @@ + +Test collection and discovery +====================================================== + +.. _`discovered`: + +Default filesystem test discovery +----------------------------------------------- + +Test collection starts from paths specified at the command line or from +the current directory. Tests are collected ahead of running the first test. +(This used to be different in earlier versions of ``py.test`` where +collection and running was interweaved which made test randomization +and distributed testing harder). + +Collection nodes which have children are called "Collectors" and otherwise +they are called "Items" or "test items". Here is an example of such a +tree:: + + example $ py.test --collectonly test_collectonly.py + + + + + + + + +By default all directories not starting with a dot are traversed, +looking for ``test_*.py`` and ``*_test.py`` files. Those Python +files are imported under their `package name`_. + +The Module collector looks for test functions +and test classes and methods. Test functions and methods +are prefixed ``test`` by default. Test classes must +start with a capitalized ``Test`` prefix. + +Customizing error messages +------------------------------------------------- + +On test and collection nodes ``py.test`` will invoke +the ``node.repr_failure(excinfo)`` function which +you may override and make it return an error +representation string of your choice. It +will be reported as a (red) string. From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit 4380c721021d: adapt test_module_alias_import. Message-ID: <20101104130831.3EEDA6C1424@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288766672 -3600 # Node ID 4380c721021dc1dc436fb40d5e681697b66b7b95 # Parent afac3bb76444fff702c7ee2ba06085c09f6749dd adapt test_module_alias_import. --- a/test_apipkg.py +++ b/test_apipkg.py @@ -134,7 +134,8 @@ class TestScenarios: """)) monkeypatch.syspath_prepend(tmpdir) import aliasimport - assert aliasimport.some is py.std.os.path + for k, v in py.std.os.path.__dict__.items(): + assert getattr(aliasimport.some, k) == v def test_from_module_alias_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("fromaliasimport") From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit afac3bb76444: make importing from an alias module work. Message-ID: <20101104130831.337596C141C@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288766668 -3600 # Node ID afac3bb76444fff702c7ee2ba06085c09f6749dd # Parent 1ccd1b2e2232569d72695e44295410a29c8e89bd make importing from an alias module work. --- a/apipkg.py +++ b/apipkg.py @@ -65,10 +65,17 @@ class ApiModule(ModuleType): attrname = parts and parts[0] or "" if modpath[0] == '.': modpath = implprefix + modpath - if name == '__doc__': - self.__doc__ = importobj(modpath, attrname) + + if not attrname: + subname = '%s.%s'%(self.__name__, name) + apimod = AliasModule(subname, modpath) + sys.modules[subname] = apimod + setattr(self, name, apimod) else: - self.__map__[name] = (modpath, attrname) + if name == '__doc__': + self.__doc__ = importobj(modpath, attrname) + else: + self.__map__[name] = (modpath, attrname) def __repr__(self): l = [] @@ -118,3 +125,35 @@ class ApiModule(ModuleType): pass return dict __dict__ = property(__dict__) + +class AliasModule(ModuleType): + def __init__(self, name, modpath): + self.__name__ = name + self.__modpath = modpath + + def __repr__(self): + l = [] + if hasattr(self, '__version__'): + l.append("version=" + repr(self.__version__)) + if hasattr(self, '__file__'): + l.append('from ' + repr(self.__file__)) + if l: + return '' % (self.__name__, " ".join(l)) + return '' % (self.__name__,) + + def __getattr__(self, name): + mod = importobj(self.__modpath, None) + result = getattr(mod, name) + setattr(self, name, result) + for k, v in mod.__dict__.items(): + setattr(self, k, v) + return result + + def __dict__(self): + # force all the content of the module to be loaded when __dict__ is read + dictdescr = ModuleType.__dict__['__dict__'] + dict = dictdescr.__get__(self) + if dict is not None: + hasattr(self, 'some') + return dict + __dict__ = property(__dict__) From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit ad8da71fea6d: bump version, add changelog Message-ID: <20101104130831.1CE0A6C111B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User holger krekel # Date 1288876187 -3600 # Node ID ad8da71fea6dedc0757be6d7fedf7aa2e59362ce # Parent 4380c721021dc1dc436fb40d5e681697b66b7b95 bump version, add changelog --- a/apipkg.py +++ b/apipkg.py @@ -9,7 +9,7 @@ import os import sys from types import ModuleType -__version__ = "1.1" +__version__ = "1.2.dev1" def initpkg(pkgname, exportdefs, attr=dict()): """ initialize given package from the export definitions. """ --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +1.2 +---------------------------------------- + +- Allow to import from Aliasmodules (thanks Ralf Schmitt) + 1.1 ---------------------------------------- From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit 1ccd1b2e2232: test that importing from an alias module works. Message-ID: <20101104130831.289476C1414@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288766662 -3600 # Node ID 1ccd1b2e2232569d72695e44295410a29c8e89bd # Parent 5769180a95146f3b4a41d53331f88a5955b60862 test that importing from an alias module works. --- a/test_apipkg.py +++ b/test_apipkg.py @@ -136,6 +136,18 @@ class TestScenarios: import aliasimport assert aliasimport.some is py.std.os.path + def test_from_module_alias_import(self, monkeypatch, tmpdir): + pkgdir = tmpdir.mkdir("fromaliasimport") + pkgdir.join('__init__.py').write(py.code.Source(""" + import apipkg + apipkg.initpkg(__name__, exportdefs={ + 'some': 'os.path', + }) + """)) + monkeypatch.syspath_prepend(tmpdir) + from fromaliasimport.some import join + assert join is py.std.os.path.join + def xtest_nested_absolute_imports(): import email api_email = apipkg.ApiModule('email',{ From commits-noreply at bitbucket.org Thu Nov 4 16:02:15 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 10:02:15 -0500 (CDT) Subject: [py-svn] pylib commit 45b898021117: introduce a dirname attribute to path objects Message-ID: <20101104150215.6D1851E0FBF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1288728276 -3600 # Node ID 45b89802111778fd14cfb57646dd17a54da282e4 # Parent 95f405382cc8501c1099c339345c5c680e96c22e introduce a dirname attribute to path objects --- a/py/_path/common.py +++ b/py/_path/common.py @@ -91,6 +91,11 @@ class PathBase(object): return self._getbyspec('basename')[0] basename = property(basename, None, None, basename.__doc__) + def dirname(self): + """ dirname part of path. """ + return self._getbyspec('dirname')[0] + dirname = property(dirname, None, None, dirname.__doc__) + def purebasename(self): """ pure base name of the path.""" return self._getbyspec('purebasename')[0] --- a/testing/path/common.py +++ b/testing/path/common.py @@ -68,6 +68,10 @@ class CommonFSTests(object): assert newpath.check(basename='sampledir') assert newpath.basename, 'sampledir' + def test_dirname(self, path1): + newpath = path1.join('sampledir') + assert newpath.dirname == str(path1) + def test_dirpath(self, path1): newpath = path1.join('sampledir') assert newpath.dirpath() == path1 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ Changes between 1.3.4 and 2.0.0dev0 - use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() +- path objects have a .dirname attribute now Changes between 1.3.3 and 1.3.4 ================================================== From commits-noreply at bitbucket.org Thu Nov 4 16:02:15 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 10:02:15 -0500 (CDT) Subject: [py-svn] pylib commit e8cc8ddb7505: add breadthfirst and sorted options to visit(), refactor to a Visit class Message-ID: <20101104150215.7EC821E0FC0@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1288882994 -3600 # Node ID e8cc8ddb7505e5b4a2928c09990d9974a4508c77 # Parent 45b89802111778fd14cfb57646dd17a54da282e4 add breadthfirst and sorted options to visit(), refactor to a Visit class to avoid passing around tons of arguments. --- a/py/_path/common.py +++ b/py/_path/common.py @@ -268,7 +268,7 @@ newline will be removed from the end of except AttributeError: return str(self) < str(other) - def visit(self, fil=None, rec=None, ignore=NeverRaised): + def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): """ yields all paths below the current one fil is a filter (glob pattern or callable), if not matching the @@ -280,26 +280,14 @@ newline will be removed from the end of ignore is an Exception class that is ignoredwhen calling dirlist() on any of the paths (by default, all exceptions are reported) + + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False + + sort if True will sort entries within each directory level. """ - if isinstance(fil, str): - fil = FNMatcher(fil) - if rec: - if isinstance(rec, str): - rec = fnmatch(fil) - elif not hasattr(rec, '__call__'): - rec = None - try: - entries = self.listdir() - except ignore: - return - dirs = [p for p in entries - if p.check(dir=1) and (rec is None or rec(p))] - for subdir in dirs: - for p in subdir.visit(fil=fil, rec=rec, ignore=ignore): - yield p - for p in entries: - if fil is None or fil(p): - yield p + for x in Visitor(fil, rec, ignore, bf, sort).gen(self): + yield x def _sortlist(self, res, sort): if sort: @@ -312,6 +300,41 @@ newline will be removed from the end of """ return True if other refers to the same stat object as self. """ return self.strpath == str(other) +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, str): + fil = FNMatcher(fil) + if rec: + if isinstance(rec, str): + rec = fnmatch(fil) + else: + assert hasattr(rec, '__call__') + self.fil = fil + self.rec = rec + self.ignore = ignore + self.breadthfirst = bf + self.optsort = sort and sorted or (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort([p for p in entries + if p.check(dir=1) and (rec is None or rec(p))]) + if not self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + class FNMatcher: def __init__(self, pattern): self.pattern = pattern --- a/testing/path/common.py +++ b/testing/path/common.py @@ -287,6 +287,12 @@ class CommonFSTests(object): def test_relto_wrong_type(self, path1): py.test.raises(TypeError, "path1.relto(42)") + def test_load(self, path1): + p = path1.join('samplepickle') + obj = p.load() + assert type(obj) is dict + assert obj.get('answer',None) == 42 + def test_visit_filesonly(self, path1): l = [] for i in path1.visit(lambda x: x.check(file=1)): @@ -294,12 +300,6 @@ class CommonFSTests(object): assert not "sampledir" in l assert path1.sep.join(["sampledir", "otherfile"]) in l - def test_load(self, path1): - p = path1.join('samplepickle') - obj = p.load() - assert type(obj) is dict - assert obj.get('answer',None) == 42 - def test_visit_nodotfiles(self, path1): l = [] for i in path1.visit(lambda x: x.check(dotfile=0)): @@ -308,6 +308,28 @@ class CommonFSTests(object): assert path1.sep.join(["sampledir", "otherfile"]) in l assert not ".dotfile" in l + def test_visit_breadthfirst(self, path1): + l = [] + for i in path1.visit(bf=True): + l.append(i.relto(path1)) + for i, p in enumerate(l): + if path1.sep in p: + for j in range(i, len(l)): + assert path1.sep in l[j] + break + else: + py.test.fail("huh") + + def test_visit_sort(self, path1): + l = [] + for i in path1.visit(bf=True, sort=True): + l.append(i.relto(path1)) + for i, p in enumerate(l): + if path1.sep in p: + break + assert l[:i] == sorted(l[:i]) + assert l[i:] == sorted(l[i:]) + def test_endswith(self, path1): def chk(p): return p.check(endswith="pickle") From commits-noreply at bitbucket.org Thu Nov 4 16:03:37 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 10:03:37 -0500 (CDT) Subject: [py-svn] pylib commit 577b5991dbce: bump version, add changelog Message-ID: <20101104150337.A86E3240F52@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1288883096 -3600 # Node ID 577b5991dbceffe0887001c8597fd9a2f4d50023 # Parent e8cc8ddb7505e5b4a2928c09990d9974a4508c77 bump version, add changelog --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def main(): long_description = long_description, install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev4', + version='2.0.0.dev5', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev4' +__version__ = '2.0.0.dev5' from py import _apipkg --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ Changes between 1.3.4 and 2.0.0dev0 - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() - path objects have a .dirname attribute now +- path.visit() accepts breadthfirst (bf) and sort options Changes between 1.3.3 and 1.3.4 ================================================== From commits-noreply at bitbucket.org Thu Nov 4 22:35:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 16:35:01 -0500 (CDT) Subject: [py-svn] apipkg commit fd590ef45812: make initpkg work without an "old module" in sys.modules Message-ID: <20101104213501.B792F1E0FC0@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288906297 -3600 # Node ID fd590ef45812b3b031ec3412e99b6f03324e9bf9 # Parent 41f548cd98ae6bc5afa5da737730dda749646d76 make initpkg work without an "old module" in sys.modules --- a/test_apipkg.py +++ b/test_apipkg.py @@ -400,3 +400,9 @@ def test_aliasmodule_repr(): assert "" == r am.version assert repr(am) == r + +def test_initpkg_without_old_module(): + apipkg.initpkg("initpkg_without_old_module", + dict(modules="sys:modules")) + from initpkg_without_old_module import modules + assert modules is sys.modules --- a/apipkg.py +++ b/apipkg.py @@ -13,7 +13,7 @@ __version__ = "1.2.dev1" def initpkg(pkgname, exportdefs, attr=dict()): """ initialize given package from the export definitions. """ - oldmod = sys.modules[pkgname] + oldmod = sys.modules.get(pkgname) d = {} f = getattr(oldmod, '__file__', None) if f: @@ -28,7 +28,8 @@ def initpkg(pkgname, exportdefs, attr=di if hasattr(oldmod, '__doc__'): d['__doc__'] = oldmod.__doc__ d.update(attr) - oldmod.__dict__.update(d) + if hasattr(oldmod, "__dict__"): + oldmod.__dict__.update(d) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod From commits-noreply at bitbucket.org Thu Nov 4 22:35:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 16:35:01 -0500 (CDT) Subject: [py-svn] apipkg commit 41f548cd98ae: simplify AliasModule.__repr__. Message-ID: <20101104213501.AAB0D1E0FBF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288905387 -3600 # Node ID 41f548cd98ae6bc5afa5da737730dda749646d76 # Parent ad8da71fea6dedc0757be6d7fedf7aa2e59362ce simplify AliasModule.__repr__. also do not use __name__, rather load it from the original module. --- a/test_apipkg.py +++ b/test_apipkg.py @@ -393,3 +393,10 @@ def test_extra_attributes(tmpdir, monkey monkeypatch.syspath_prepend(tmpdir) import extra_attributes assert extra_attributes.foo == 'bar' + +def test_aliasmodule_repr(): + am = apipkg.AliasModule("mymod", "sys") + r = repr(am) + assert "" == r + am.version + assert repr(am) == r --- a/apipkg.py +++ b/apipkg.py @@ -128,18 +128,11 @@ class ApiModule(ModuleType): class AliasModule(ModuleType): def __init__(self, name, modpath): - self.__name__ = name + self.__name = name self.__modpath = modpath def __repr__(self): - l = [] - if hasattr(self, '__version__'): - l.append("version=" + repr(self.__version__)) - if hasattr(self, '__file__'): - l.append('from ' + repr(self.__file__)) - if l: - return '' % (self.__name__, " ".join(l)) - return '' % (self.__name__,) + return '' % (self.__name, self.__modpath) def __getattr__(self, name): mod = importobj(self.__modpath, None) From commits-noreply at bitbucket.org Thu Nov 4 22:50:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 16:50:05 -0500 (CDT) Subject: [py-svn] apipkg commit f7ad3ae4e30f: dont load __doc__ early, making ApiModules now fully lazy Message-ID: <20101104215005.3CEFE1E0FC0@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User holger krekel # Date 1288907454 -3600 # Node ID f7ad3ae4e30f937c2e789331e80ae22315b17c2c # Parent fd590ef45812b3b031ec3412e99b6f03324e9bf9 dont load __doc__ early, making ApiModules now fully lazy also bump and normalize __version__ setting --- a/setup.py +++ b/setup.py @@ -13,15 +13,13 @@ try: except ImportError: from distutils.core import setup -from apipkg import __version__ - def main(): setup( name='apipkg', description= 'apipkg: namespace control and lazy-import mechanism', long_description = open('README.txt').read(), - version= __version__, + version='1.2.dev4', url='http://bitbucket.org/hpk42/apipkg', license='MIT License', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/apipkg.py +++ b/apipkg.py @@ -25,7 +25,7 @@ def initpkg(pkgname, exportdefs, attr=di d['__loader__'] = oldmod.__loader__ if hasattr(oldmod, '__path__'): d['__path__'] = [os.path.abspath(p) for p in oldmod.__path__] - if hasattr(oldmod, '__doc__'): + if '__doc__' not in exportdefs and getattr(oldmod, '__doc__', None): d['__doc__'] = oldmod.__doc__ d.update(attr) if hasattr(oldmod, "__dict__"): @@ -45,6 +45,16 @@ def importobj(modpath, attrname): return retval class ApiModule(ModuleType): + def __docget(self): + try: + return self.__doc + except AttributeError: + if '__doc__' in self.__map__: + return self.__makeattr('__doc__') + def __docset(self, value): + self.__doc = value + __doc__ = property(__docget, __docset) + def __init__(self, name, importspec, implprefix=None, attr=None): self.__name__ = name self.__all__ = [x for x in importspec if x != '__onfirstaccess__'] @@ -73,10 +83,7 @@ class ApiModule(ModuleType): sys.modules[subname] = apimod setattr(self, name, apimod) else: - if name == '__doc__': - self.__doc__ = importobj(modpath, attrname) - else: - self.__map__[name] = (modpath, attrname) + self.__map__[name] = (modpath, attrname) def __repr__(self): l = [] --- a/test_apipkg.py +++ b/test_apipkg.py @@ -115,12 +115,12 @@ class TestScenarios: }) """)) pkgdir.join('submod.py').write(py.code.Source(""" - import recmodule + import recmodule class someclass: pass print (recmodule.__dict__) """)) monkeypatch.syspath_prepend(tmpdir) - import recmodule + import recmodule assert isinstance(recmodule, apipkg.ApiModule) assert recmodule.some.__name__ == "someclass" @@ -224,14 +224,22 @@ def test_initpkg_transfers_attrs(monkeyp assert newmod.__loader__ == mod.__loader__ assert newmod.__doc__ == mod.__doc__ -def test_initpkg_not_overwrite_exportdefs(monkeypatch): +def test_initpkg_nodoc(monkeypatch): mod = type(sys)('hello') - mod.__doc__ = "this is the documentation" + mod.__file__ = "hello.py" monkeypatch.setitem(sys.modules, 'hello', mod) + apipkg.initpkg('hello', {}) + newmod = sys.modules['hello'] + assert not newmod.__doc__ + +def test_initpkg_overwrite_doc(monkeypatch): + hello = type(sys)('hello') + hello.__doc__ = "this is the documentation" + monkeypatch.setitem(sys.modules, 'hello', hello) apipkg.initpkg('hello', {"__doc__": "sys:__doc__"}) - newmod = sys.modules['hello'] - assert newmod != mod - assert newmod.__doc__ == sys.__doc__ + newhello = sys.modules['hello'] + assert newhello != hello + assert newhello.__doc__ == sys.__doc__ def test_initpkg_not_transfers_not_existing_attrs(monkeypatch): mod = type(sys)('hello') @@ -289,7 +297,7 @@ def test_onfirstaccess(tmpdir, monkeypat """)) pkgdir.join('submod.py').write(py.code.Source(""" l = [] - def init(): + def init(): l.append(1) """)) monkeypatch.syspath_prepend(tmpdir) @@ -311,9 +319,9 @@ def test_onfirstaccess_setsnewattr(tmpdi ) """)) pkgdir.join('submod.py').write(py.code.Source(""" - def init(): + def init(): import %s as pkg - pkg.newattr = 42 + pkg.newattr = 42 """ % pkgname)) monkeypatch.syspath_prepend(tmpdir) mod = __import__(pkgname) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ -1.2 +1.2.dev ---------------------------------------- - Allow to import from Aliasmodules (thanks Ralf Schmitt) +- avoid loading __doc__ early, so ApiModule is now fully lazy 1.1 ---------------------------------------- From commits-noreply at bitbucket.org Sat Nov 6 00:46:34 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 5 Nov 2010 18:46:34 -0500 (CDT) Subject: [py-svn] pylib commit 5367bea2f477: update iniconfig Message-ID: <20101105234634.0F61A6C140E@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User Ronny Pfannschmidt # Date 1288999101 -3600 # Node ID 5367bea2f47738bdefa9eb55752e5a2a4b0e841b # Parent 577b5991dbceffe0887001c8597fd9a2f4d50023 update iniconfig --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -92,7 +92,7 @@ def parse(input): # only for testing purposes - _parse() does not use state except path ini = object.__new__(IniConfig) ini.path = "sample" - return ini._parse(input) + return ini._parse(input.splitlines(True)) def parse_a_error(input): return py.test.raises(ParseError, parse, input) --- a/py/_iniconfig.py +++ b/py/_iniconfig.py @@ -46,10 +46,13 @@ class IniConfig(object): self.path = str(path) # convenience if data is None: f = open(self.path) - data = f.read() - f.close() - tokens = self._parse(data) - + try: + tokens = self._parse(iter(f)) + finally: + f.close() + else: + tokens = self._parse(data.splitlines(True)) + self._sources = {} self.sections = {} @@ -69,10 +72,10 @@ class IniConfig(object): def _raise(self, lineno, msg): raise ParseError(self.path, lineno, msg) - def _parse(self, data): + def _parse(self, line_iter): result = [] section = None - for lineno, line in enumerate(data.splitlines(True)): + for lineno, line in enumerate(line_iter): name, data = self._parseline(line, lineno) # new value if name is not None and data is not None: @@ -116,7 +119,7 @@ class IniConfig(object): try: name, value = line.split(": ", 1) except ValueError: - self._raise(lineno, 'unexpected line: %s') + self._raise(lineno, 'unexpected line: %r' % line) return name.strip(), value.strip() # continuation else: From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 2dfb0db0864d: remove pytest_report_iteminfo hook, i strongly guess nobody needs or uses it. Message-ID: <20101106085700.96733241420@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288909283 -3600 # Node ID 2dfb0db0864d075300712366a86cdf642c7c68d9 # Parent 3253d770b03c29b46f2d5c96f46b00b01f882551 remove pytest_report_iteminfo hook, i strongly guess nobody needs or uses it. --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -1061,18 +1061,6 @@ class TestReportInfo: nodeinfo = runner.getitemnodeinfo(item) assert nodeinfo.location == ("ABCDE", 42, "custom") - def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") - tup = "FGHJ", 42, "custom" - class Plugin: - def pytest_report_iteminfo(self, item): - return tup - item.config.pluginmanager.register(Plugin()) - runner = runner = item.config.pluginmanager.getplugin("runner") - nodeinfo = runner.getitemnodeinfo(item) - location = nodeinfo.location - assert location == tup - def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") fspath, lineno, modpath = item.reportinfo() --- a/pytest/plugin/nose.py +++ b/pytest/plugin/nose.py @@ -49,19 +49,6 @@ def pytest_runtest_makereport(__multical call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) call.excinfo = call2.excinfo -def pytest_report_iteminfo(item): - # nose 0.11.1 uses decorators for "raises" and other helpers. - # for reporting progress by filename we fish for the filename - if isinstance(item, py.test.collect.Function): - obj = item.obj - if hasattr(obj, 'compat_co_firstlineno'): - fn = sys.modules[obj.__module__].__file__ - if fn.endswith(".pyc"): - fn = fn[:-1] - #assert 0 - #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) - lineno = obj.compat_co_firstlineno - return py.path.local(fn), lineno, obj.__module__ def pytest_runtest_setup(item): if isinstance(item, (py.test.collect.Function)): --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -123,8 +123,19 @@ class PyobjMixin(object): return self._fslineno def reportinfo(self): - fspath, lineno = self._getfslineno() - modpath = self.getmodpath() + obj = self.obj + if hasattr(obj, 'compat_co_firstlineno'): + # nose compatibility + fspath = sys.modules[obj.__module__].__file__ + if fspath.endswith(".pyc"): + fspath = fspath[:-1] + #assert 0 + #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + lineno = obj.compat_co_firstlineno + modpath = obj.__module__ + else: + fspath, lineno = self._getfslineno() + modpath = self.getmodpath() return fspath, lineno, modpath class PyCollectorMixin(PyobjMixin, pytest.collect.Collector): @@ -501,16 +512,16 @@ class Metafunc: :arg funcargs: argument keyword dictionary used when invoking the test function. - :arg id: used for reporting and identification purposes. If you + :arg id: used for reporting and identification purposes. If you don't supply an `id` the length of the currently list of calls to the test function will be used. :arg param: will be exposed to a later funcarg factory invocation through the ``request.param`` attribute. Setting it (instead of directly providing a ``funcargs`` ditionary) is called - *indirect parametrization*. Indirect parametrization is - preferable if test values are expensive to setup or can - only be created after certain fixtures or test-run related + *indirect parametrization*. Indirect parametrization is + preferable if test values are expensive to setup or can + only be created after certain fixtures or test-run related initialization code has been run. """ assert funcargs is None or isinstance(funcargs, dict) @@ -593,7 +604,7 @@ class FuncargRequest: def applymarker(self, marker): """ apply a marker to a single test function invocation. This method is useful if you don't want to have a keyword/marker - on all function invocations. + on all function invocations. :arg marker: a :py:class:`pytest.plugin.mark.MarkDecorator` object created by a call to ``py.test.mark.NAME(...)``. --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -20,7 +20,7 @@ def pytest_cmdline_parse(pluginmanager, pytest_cmdline_parse.firstresult = True def pytest_addoption(parser): - """add optparse-style options and ini-style config values via calls + """add optparse-style options and ini-style config values via calls to ``parser.addoption`` and ``parser.addini(...)``. """ @@ -194,14 +194,6 @@ pytest_report_teststatus.firstresult = T def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ -def pytest_report_iteminfo(item): - """ return (fspath, lineno, domainpath) location info for the item. - the information is used for result display and to sort tests. - fspath,lineno: file and linenumber of source of item definition. - domainpath: custom id - e.g. for python: dotted import address - """ -pytest_report_iteminfo.firstresult = True - # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -116,9 +116,6 @@ def pytest_collect_directory(path, paren return return Directory(path, parent=parent) -def pytest_report_iteminfo(item): - return item.reportinfo() - class Session(object): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -96,7 +96,7 @@ class TestTerminal: tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) nodeid = item.collection.getid(item) - location = item.ihook.pytest_report_iteminfo(item=item) + location = item.reportinfo() tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location, fspath=str(item.fspath)) linecomp.assert_contains_lines([ --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -41,7 +41,7 @@ def getitemnodeinfo(item): try: return item._nodeinfo except AttributeError: - location = item.ihook.pytest_report_iteminfo(item=item) + location = item.reportinfo() location = (str(location[0]), location[1], str(location[2])) nodenames = tuple(item.listnames()) nodeid = item.collection.getid(item) --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -1,5 +1,4 @@ import py -from pytest.plugin.session import pytest_report_iteminfo class SessionTests: def test_basic_testitem_events(self, testdir): @@ -225,12 +224,3 @@ def test_exclude(testdir): result = testdir.runpytest("--ignore=hello", "--ignore=hello2") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - -def test_pytest_report_iteminfo(): - class FakeItem(object): - - def reportinfo(self): - return "-reportinfo-" - - res = pytest_report_iteminfo(FakeItem()) - assert res == "-reportinfo-" From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 125c0136ba20: add "linelist" type for ini-files Message-ID: <20101106085701.19801243F49@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 125c0136ba20fa7c651de76a4c679b1c59440487 # Parent e4e9f0f7c4dd2e8b39991b38db44f676802f1b99 add "linelist" type for ini-files --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -70,7 +70,7 @@ class Parser: def addini(self, name, help, type=None, default=None): """ add an ini-file option with the given name and description. """ - assert type in (None, "pathlist", "args") + assert type in (None, "pathlist", "args", "linelist") self._inidict[name] = (help, type, default) class OptionGroup: @@ -365,7 +365,9 @@ class Config(object): except KeyError: if default is not None: return default - return {'pathlist': [], 'args': [], None: ''}.get(type) + if type is None: + return '' + return [] if type == "pathlist": dp = py.path.local(self.inicfg.config.path).dirpath() l = [] @@ -374,6 +376,8 @@ class Config(object): return l elif type == "args": return py.std.shlex.split(value) + elif type == "linelist": + return filter(None, map(lambda x: x.strip(), value.split("\n"))) else: assert type is None return value --- a/testing/test_config.py +++ b/testing/test_config.py @@ -169,6 +169,24 @@ class TestConfigAPI: l = config.getini("a2") assert l == list("123") + def test_addini_linelist(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + parser.addini("xy", "", type="linelist") + parser.addini("a2", "", "linelist") + """) + p = testdir.makeini(""" + [pytest] + xy= 123 345 + second line + """) + config = testdir.parseconfig() + l = config.getini("xy") + assert len(l) == 2 + assert l == ["123 345", "second line"] + l = config.getini("a2") + assert l == [] + def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): reprec = testdir.inline_run(*opts) From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 0aaee1071bb4: update issues Message-ID: <20101106085701.34796243F53@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 0aaee1071bb49468aa9dc35c3a18305262de9998 # Parent 4bdabf614c7658b422eba78d787f8bce41b81a2d update issues --- a/ISSUES.txt +++ b/ISSUES.txt @@ -3,11 +3,18 @@ checks / deprecations for next release tags: bug 2.4 core xdist * check oejskit plugin compatibility -* some simple profiling * move pytest_nose out of pylib because it implicitely extends the protocol now - setup/teardown is called at module level. consider making calling of setup/teardown configurable +profiling / hook call optimization +------------------------------------- +tags: enhancement 2.1 + +bench/bench.py reveals that for very quick running +unit tests the hook architecture is a bit slow. +Profile and improve hook calls. + do early-teardown of test modules ----------------------------------------- tags: feature 2.1 @@ -108,13 +115,12 @@ tags: feature 2.1 allow to name conftest.py files (in sub directories) that should be imported early, as to include command line options. -a central py.test ini/yml file +improve central py.test ini file ---------------------------------- tags: feature 2.1 -introduce a declarative configuration file that allows: -- default options -- to-be-collected test directories +introduce more declarative configuration options: +- (to-be-collected test directories) - required plugins - test func/class/file matching patterns - skip/xfail (non-intrusive) @@ -125,14 +131,11 @@ new documentation tags: feature 2.1 - logo py.test -- reference / customization -- writing a (local or global) plugin - examples for unittest or functional testing - resource management for functional testing - patterns: page object - parametrized testing - better / more integrated plugin docs - i.e. not per-plugin but per-feature referencing a plugin generalize parametrized testing to generate combinations ------------------------------------------------------------- From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 4bdabf614c76: reverse options ordering in Parser class instead of on PluginManager Message-ID: <20101106085701.27F72243F4D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 4bdabf614c7658b422eba78d787f8bce41b81a2d # Parent 125c0136ba20fa7c651de76a4c679b1c59440487 reverse options ordering in Parser class instead of on PluginManager --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -53,12 +53,12 @@ class Parser: def parse(self, args): self.optparser = optparser = MyOptionParser(self) - groups = self._groups + [self._anonymous] + groups = list(reversed(self._groups)) + [self._anonymous] for group in groups: if group.options: desc = group.description or group.name optgroup = py.std.optparse.OptionGroup(optparser, desc) - optgroup.add_options(group.options) + optgroup.add_options(reversed(group.options)) optparser.add_option_group(optgroup) return self.optparser.parse_args([str(x) for x in args]) @@ -304,7 +304,7 @@ class Config(object): self.pluginmanager.consider_env() self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) - self.pluginmanager.do_addoption(self._parser) + self.hook.pytest_addoption(parser=self._parser) def _checkversion(self): minver = self.inicfg.get('minversion', None) --- a/pytest/_core.py +++ b/pytest/_core.py @@ -180,11 +180,6 @@ class PluginManager(object): for hint in self._hints: tw.line("hint: %s" % hint) - def do_addoption(self, parser): - mname = "pytest_addoption" - methods = reversed(self.listattr(mname)) - MultiCall(methods, {'parser': parser}).execute() - def do_configure(self, config): assert not hasattr(self, '_config') self._config = config From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 22ce60ec51fe: introduce a minimal tag-based tracer, to be extended if needed, strike pytest_trace hook. Message-ID: <20101106085701.4D178243F54@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 22ce60ec51feb6e4464ba3142c5c008322d74557 # Parent 0aaee1071bb49468aa9dc35c3a18305262de9998 introduce a minimal tag-based tracer, to be extended if needed, strike pytest_trace hook. --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -8,6 +8,8 @@ import pytest def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) config.parse(args) + if config.option.debug: + config.trace.root.setwriter(sys.stderr.write) return config class Parser: @@ -253,6 +255,7 @@ class Config(object): ) #: a pluginmanager instance self.pluginmanager = pluginmanager or PluginManager(load=True) + self.trace = self.pluginmanager.trace.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook @@ -272,10 +275,6 @@ class Config(object): plugins += self._conftest.getconftestmodules(fspath) return plugins - def trace(self, msg): - if getattr(self.option, 'traceconfig', None): - self.hook.pytest_trace(category="config", msg=msg) - def _setinitialconftest(self, args): # capture output during conftest init (#issue93) name = hasattr(os, 'dup') and 'StdCaptureFD' or 'StdCapture' --- a/testing/test_config.py +++ b/testing/test_config.py @@ -75,6 +75,14 @@ class TestConfigTmpdir: class TestConfigAPI: + def test_config_trace(self, testdir): + config = testdir.Config() + l = [] + config.trace.root.setwriter(l.append) + config.trace("hello") + assert len(l) == 1 + assert l[0] == "[pytest:config] hello\n" + def test_config_getvalue_honours_conftest(self, testdir): testdir.makepyfile(conftest="x=1") testdir.mkdir("sub").join("conftest.py").write("x=2 ; y = 3") --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -511,20 +511,28 @@ def test_tbstyle_short(testdir): assert 'x = 0' in s assert 'assert x' in s -def test_trace_reporting(testdir): +def test_traceconfig(testdir, monkeypatch): result = testdir.runpytest("--traceconfig") result.stdout.fnmatch_lines([ "*active plugins*" ]) assert result.ret == 0 -def test_trace_reporting(testdir): - result = testdir.runpytest("--traceconfig") - result.stdout.fnmatch_lines([ - "*active plugins*" +def test_debug(testdir, monkeypatch): + result = testdir.runpytest("--debug") + result.stderr.fnmatch_lines([ + "*registered*session*", ]) assert result.ret == 0 +def test_PYTEST_DEBUG(testdir, monkeypatch): + monkeypatch.setenv("PYTEST_DEBUG", "1") + result = testdir.runpytest() + assert result.ret == 0 + result.stderr.fnmatch_lines([ + "*registered*PluginManager*" + ]) + class TestGenericReporting: """ this test class can be subclassed with a different option --- a/pytest/_core.py +++ b/pytest/_core.py @@ -13,11 +13,53 @@ default_plugins = ( IMPORTPREFIX = "pytest_" +class TagTracer: + def __init__(self): + self._tag2proc = {} + self.writer = None + + def get(self, name): + return TagTracerSub(self, (name,)) + + def processmessage(self, tags, args): + if self.writer is not None: + prefix = ":".join(tags) + content = " ".join(map(str, args)) + self.writer("[%s] %s\n" %(prefix, content)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + 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,)) + class PluginManager(object): def __init__(self, load=False): self._name2plugin = {} self._plugins = [] self._hints = [] + self.trace = TagTracer().get("pytest") + if os.environ.get('PYTEST_DEBUG'): + self.trace.root.setwriter(sys.stderr.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) if load: @@ -41,6 +83,7 @@ class PluginManager(object): self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + self.trace("registered", plugin) if not prepend: self._plugins.append(plugin) else: --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -36,7 +36,7 @@ def pytest_configure(config): if config.option.collectonly: reporter = CollectonlyReporter(config) else: - # we try hard to make printing resilient against + # we try hard to make printing resilient against # later changes on FD level. stdout = py.std.sys.stdout if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): @@ -50,6 +50,11 @@ def pytest_configure(config): config._toclose = stdout reporter = TerminalReporter(config, stdout) config.pluginmanager.register(reporter, 'terminalreporter') + if config.option.debug or config.option.traceconfig: + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + config.trace.root.setprocessor("pytest:config", mywriter) def pytest_unconfigure(config): if hasattr(config, '_toclose'): @@ -152,11 +157,6 @@ class TerminalReporter: # which garbles our output if we use self.write_line self.write_line(msg) - def pytest_trace(self, category, msg): - if self.config.option.debug or \ - self.config.option.traceconfig and category.find("config") != -1: - self.write_line("[%s] %s" %(category, msg)) - def pytest_deselected(self, items): self.stats.setdefault('deselected', []).extend(items) --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -266,6 +266,19 @@ class TestBootstrapping: l = list(plugins.listattr('x')) assert l == [41, 42, 43] + def test_register_trace(self): + pm = PluginManager() + class api1: + x = 41 + l = [] + pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) + p = api1() + pm.register(p) + assert len(l) == 1 + kw, args = l[0] + assert args[0] == "registered" + assert args[1] == p + class TestPytestPluginInteractions: def test_addhooks_conftestplugin(self, testdir): @@ -470,7 +483,7 @@ class TestMultiCall: reslist = MultiCall([f], dict(x=23, z=2)).execute() assert reslist == [25] - def test_keywords_call_error(self): + def test_tags_call_error(self): multicall = MultiCall([lambda x: x], {}) py.test.raises(TypeError, "multicall.execute()") @@ -537,3 +550,55 @@ class TestHookRelay: res = mcm.hello(arg=3) assert res == 4 +class TestTracer: + def test_simple(self): + from pytest._core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "[pytest] world\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "[pytest:collection] hello\n" + + def test_setprocessor(self): + from pytest._core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + + def test_setmyprocessor(self): + from pytest._core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit e90c2abaefeb: add indent facility to tracing Message-ID: <20101106085701.AC695243F56@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289030717 -3600 # Node ID e90c2abaefeb33d96b672654b2698281420acc54 # Parent 09ceba1e4812bd91aecb71f2e31d78d663957f60 add indent facility to tracing --- a/testing/test_main.py +++ b/testing/test_main.py @@ -553,17 +553,39 @@ class TestHookRelay: class TestTracer: def test_simple(self): from pytest.main import TagTracer - rootlogger = TagTracer() + rootlogger = TagTracer("[my] ") log = rootlogger.get("pytest") log("hello") l = [] rootlogger.setwriter(l.append) log("world") assert len(l) == 1 - assert l[0] == "[pytest] world\n" + assert l[0] == "[my] world\n" sublog = log.get("collection") sublog("hello") - assert l[1] == "[pytest:collection] hello\n" + assert l[1] == "[my] hello\n" + + def test_indent(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + l = [] + log.root.setwriter(lambda arg: l.append(arg)) + log("hello") + log.root.indent += 1 + log("line1") + log("line2") + log.root.indent += 1 + log("line3") + log("line4") + log.root.indent -= 1 + log("line5") + log.root.indent -= 1 + log("last") + assert len(l) == 7 + names = [x.rstrip()[len(rootlogger.prefix):] for x in l] + assert names == ['hello', ' line1', ' line2', + ' line3', ' line4', ' line5', 'last'] def test_setprocessor(self): from pytest.main import TagTracer --- a/pytest/main.py +++ b/pytest/main.py @@ -19,18 +19,21 @@ default_plugins = ( IMPORTPREFIX = "pytest_" class TagTracer: - def __init__(self): + def __init__(self, prefix="[pytest] "): self._tag2proc = {} self.writer = None + self.indent = 0 + self.prefix = prefix def get(self, name): return TagTracerSub(self, (name,)) def processmessage(self, tags, args): if self.writer is not None: - prefix = ":".join(tags) - content = " ".join(map(str, args)) - self.writer("[%s] %s\n" %(prefix, content)) + if args: + indent = " " * self.indent + content = " ".join(map(str, args)) + self.writer("%s%s%s\n" %(self.prefix, indent, content)) try: self._tag2proc[tags](tags, args) except KeyError: @@ -62,7 +65,7 @@ class PluginManager(object): self._name2plugin = {} self._plugins = [] self._hints = [] - self.trace = TagTracer().get("pytest") + self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): self.trace.root.setwriter(sys.stderr.write) self.hook = HookRelay([hookspec], pm=self) @@ -340,6 +343,7 @@ class HookRelay: hookspecs = [hookspecs] self._hookspecs = [] self._pm = pm + self.trace = pm.trace.root.get("hook") for hookspec in hookspecs: self._addhooks(hookspec, prefix) @@ -376,6 +380,7 @@ class HookCaller: return mc.execute() def pcall(self, plugins, **kwargs): + self.hookrelay.trace(self.name, kwargs) methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) return mc.execute() From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 09ceba1e4812: implement and document new invocation mechanisms, see doc/usage.txt Message-ID: <20101106085701.988DB243F55@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 09ceba1e4812bd91aecb71f2e31d78d663957f60 # Parent 22ce60ec51feb6e4464ba3142c5c008322d74557 implement and document new invocation mechanisms, see doc/usage.txt also rename pytest._core to pytest.main for convenience. --- /dev/null +++ b/pytest/__main__.py @@ -0,0 +1,4 @@ +import pytest + +if __name__ == '__main__': + raise SystemExit(pytest.main()) --- a/doc/test/plugin/cov.txt +++ b/doc/test/plugin/cov.txt @@ -35,7 +35,7 @@ However easy_install does not provide an .. IMPORTANT:: - Ensure that you manually delete the init_cov_core.pth file in your site-packages directory. + Ensure that you manually delete the init_covmain.pth file in your site-packages directory. This file starts coverage collection of subprocesses if appropriate during site initialisation at python startup. --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -9,8 +9,6 @@ __version__ = '2.0.0.dev18' __all__ = ['config', 'cmdline'] -from pytest import _core as cmdline +from pytest import main as cmdline UsageError = cmdline.UsageError - -def __main__(): - raise SystemExit(cmdline.main()) +main = cmdline.main --- a/doc/index.txt +++ b/doc/index.txt @@ -3,10 +3,14 @@ py.test: no-boilerplate testing with Pyt .. todolist:: - +.. note:: + version 2.0 introduces ``pytest`` as the main Python import name + but for historic reasons ``py.test`` remains fully valid and + represents the same package. + Welcome to ``py.test`` documentation: -.. toctree:: +.. toctree:: :maxdepth: 2 overview @@ -27,4 +31,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - --- a/doc/cmdline.txt +++ /dev/null @@ -1,111 +0,0 @@ - -.. _cmdline: - -Using the interactive command line -=============================================== - -Getting help on version, option names, environment vars ------------------------------------------------------------ - -:: - - py.test --version # shows where pytest was imported from - py.test --funcargs # show available builtin function arguments - py.test -h | --help # show help on command line and config file options - - -Stopping after the first (or N) failures ---------------------------------------------------- - -To stop the testing process after the first (N) failures:: - - py.test -x # stop after first failure - py.test -maxfail=2 # stop after two failures - -Modifying Python traceback printing ----------------------------------------------- - -Examples for modifying traceback printing:: - - py.test --showlocals # show local variables in tracebacks - py.test -l # show local variables (shortcut) - - py.test --tb=long # the default informative traceback formatting - py.test --tb=native # the Python standard library formatting - py.test --tb=short # a shorter traceback format - py.test --tb=line # only one line per failure - -Dropping to PDB (Python Debugger) on failures ----------------------------------------------- - -.. _PDB: http://docs.python.org/library/pdb.html - -Python comes with a builtin Python debugger called PDB_. ``py.test`` -allows to drop into the PDB prompt via a command line option:: - - py.test --pdb - -This will invoke the Python debugger on every failure. Often you might -only want to do this for the first failing test to understand a certain -failure situation:: - - py.test -x --pdb # drop to PDB on first failure, then end test session - py.test --pdb --maxfail=3 # drop to PDB for the first three failures - - -Setting a breakpoint / aka ``set_trace()`` ----------------------------------------------------- - -If you want to set a breakpoint and enter the ``pdb.set_trace()`` you -can use a helper:: - - def test_function(): - ... - py.test.set_trace() # invoke PDB debugger and tracing - -.. versionadded: 2.0.0 - -In previous versions you could only enter PDB tracing if -you :ref:`disable capturing`. - -creating JUnitXML format files ----------------------------------------------------- - -To create result files which can be read by Hudson_ or other Continous -integration servers, use this invocation:: - - py.test --junitxml=path - -to create an XML file at ``path``. - -creating resultlog format files ----------------------------------------------------- - -To create plain-text machine-readable result files you can issue:: - - py.test --resultlog=path - -and look at the content at the ``path`` location. Such files are used e.g. -by the `PyPy-test`_ web page to show test results over several revisions. - -.. _`PyPy-test`: http://codespeak.net:8099/summary - - -send test report to pocoo pastebin service ------------------------------------------------------ - -**Creating a URL for each test failure**:: - - py.test --pastebin=failed - -This will submit test run information to a remote Paste service and -provide a URL for each failure. You may select tests as usual or add -for example ``-x`` if you only want to send one particular failure. - -**Creating a URL for a whole test session log**:: - - py.test --pastebin=all - -Currently only pasting to the http://paste.pocoo.org service is implemented. - -.. include:: links.inc --- a/pytest/_core.py +++ /dev/null @@ -1,397 +0,0 @@ -import sys, os -import inspect -import py -from pytest import hookspec - -assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) - -default_plugins = ( - "config session terminal runner python pdb capture unittest mark skipping " - "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml doctest").split() - -IMPORTPREFIX = "pytest_" - -class TagTracer: - def __init__(self): - self._tag2proc = {} - self.writer = None - - def get(self, name): - return TagTracerSub(self, (name,)) - - def processmessage(self, tags, args): - if self.writer is not None: - prefix = ":".join(tags) - content = " ".join(map(str, args)) - self.writer("[%s] %s\n" %(prefix, content)) - try: - self._tag2proc[tags](tags, args) - except KeyError: - pass - - def setwriter(self, writer): - self.writer = writer - - def setprocessor(self, tags, processor): - if isinstance(tags, str): - tags = tuple(tags.split(":")) - else: - 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,)) - -class PluginManager(object): - def __init__(self, load=False): - self._name2plugin = {} - self._plugins = [] - self._hints = [] - self.trace = TagTracer().get("pytest") - if os.environ.get('PYTEST_DEBUG'): - self.trace.root.setwriter(sys.stderr.write) - self.hook = HookRelay([hookspec], pm=self) - self.register(self) - if load: - for spec in default_plugins: - self.import_plugin(spec) - - def _getpluginname(self, plugin, name): - if name is None: - if hasattr(plugin, '__name__'): - name = plugin.__name__.split(".")[-1] - else: - name = id(plugin) - return name - - def register(self, plugin, name=None, prepend=False): - assert not self.isregistered(plugin), plugin - assert not self.isregistered(plugin), plugin - name = self._getpluginname(plugin, name) - if name in self._name2plugin: - return False - self._name2plugin[name] = plugin - self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) - self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.trace("registered", plugin) - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - return True - - def unregister(self, plugin=None, name=None): - if plugin is None: - plugin = self.getplugin(name=name) - self._plugins.remove(plugin) - self.hook.pytest_plugin_unregistered(plugin=plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - - def isregistered(self, plugin, name=None): - if self._getpluginname(plugin, name) in self._name2plugin: - return True - for val in self._name2plugin.values(): - if plugin == val: - return True - - def addhooks(self, spec): - self.hook._addhooks(spec, prefix="pytest_") - - def getplugins(self): - return list(self._plugins) - - def skipifmissing(self, name): - if not self.hasplugin(name): - py.test.skip("plugin %r is missing" % name) - - def hasplugin(self, name): - try: - self.getplugin(name) - return True - except KeyError: - return False - - def getplugin(self, name): - try: - return self._name2plugin[name] - except KeyError: - impname = canonical_importname(name) - return self._name2plugin[impname] - - # API for bootstrapping - # - def _envlist(self, varname): - val = py.std.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 - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = canonical_importname(ep.name) - if name in self._name2plugin: - continue - plugin = ep.load() - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.import_plugin(opt2) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__): - 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, spec): - assert isinstance(spec, str) - modname = canonical_importname(spec) - if modname in self._name2plugin: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except: - e = py.std.sys.exc_info()[1] - if not hasattr(py.test, 'skip'): - raise - elif not isinstance(e, py.test.skip.Exception): - raise - self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) - - def pytest_plugin_registered(self, plugin): - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(py.test, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) - - def _setns(self, obj, dic): - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - mod = py.std.types.ModuleType(name) - sys.modules['pytest.%s' % name] = mod - sys.modules['py.test.%s' % name] = mod - mod.__all__ = [] - setattr(obj, name, mod) - self._setns(mod, value) - else: - #print "setting", name, value, "on", obj - setattr(obj, name, value) - obj.__all__.append(name) - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - def do_configure(self, config): - assert not hasattr(self, '_config') - self._config = config - config.hook.pytest_configure(config=self._config) - - def do_unconfigure(self, config): - config = self._config - del self._config - config.hook.pytest_unconfigure(config=config) - config.pluginmanager.unregister(self) - - def notify_exception(self, excinfo): - excrepr = excinfo.getrepr(funcargs=True, showlocals=True) - res = self.hook.pytest_internalerror(excrepr=excrepr) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - for plugin in plugins: - try: - l.append(getattr(plugin, attrname)) - except AttributeError: - continue - return l - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() - -def canonical_importname(name): - if '.' in name: - return name - name = name.lower() - if not name.startswith(IMPORTPREFIX): - name = IMPORTPREFIX + name - return name - -def importplugin(importspec): - try: - return __import__(importspec, None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(importspec) == -1: - raise - name = importspec - try: - if name.startswith("pytest_"): - name = importspec[7:] - return __import__("pytest.plugin.%s" %(name), None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(name) == -1: - raise - # show the original exception, not the failing internal one - return __import__(importspec, None, None, '__doc__') - - -class MultiCall: - """ execute a call into multiple python functions/methods. """ - def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) - self.kwargs = kwargs - 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): - while self.methods: - method = self.methods.pop() - kwargs = self.getkwargs(method) - res = method(**kwargs) - if res is not None: - self.results.append(res) - if self.firstresult: - return res - if not self.firstresult: - return self.results - - def getkwargs(self, method): - kwargs = {} - for argname in varnames(method): - try: - kwargs[argname] = self.kwargs[argname] - except KeyError: - if argname == "__multicall__": - kwargs[argname] = self - return kwargs - -def varnames(func): - if not inspect.isfunction(func) and not inspect.ismethod(func): - func = getattr(func, '__call__', func) - ismethod = inspect.ismethod(func) - rawcode = py.code.getrawcode(func) - try: - return rawcode.co_varnames[ismethod:rawcode.co_argcount] - except AttributeError: - return () - -class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] - self._hookspecs = [] - self._pm = pm - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspecs, prefix): - self._hookspecs.append(hookspecs) - added = False - for name, method in vars(hookspecs).items(): - if name.startswith(prefix): - if not method.__doc__: - raise ValueError("docstring required for hook %r, in %r" - % (method, hookspecs)) - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult) - 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, hookspecs,)) - - -class HookCaller: - def __init__(self, hookrelay, name, firstresult): - self.hookrelay = hookrelay - self.name = name - self.firstresult = firstresult - - def __repr__(self): - return "" %(self.name,) - - def __call__(self, **kwargs): - methods = self.hookrelay._pm.listattr(self.name) - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() - - def pcall(self, plugins, **kwargs): - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() - -pluginmanager = PluginManager(load=True) # will trigger default plugin importing - -def main(args=None): - global pluginmanager - if args is None: - args = sys.argv[1:] - hook = pluginmanager.hook - try: - config = hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - exitstatus = hook.pytest_cmdline_main(config=config) - except UsageError: - e = sys.exc_info()[1] - sys.stderr.write("ERROR: %s\n" %(e.args[0],)) - exitstatus = 3 - pluginmanager = PluginManager(load=True) - return exitstatus - -class UsageError(Exception): - """ error in py.test usage or invocation""" --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -1,7 +1,7 @@ import py import sys, os -from pytest._core import PluginManager +from pytest.main import PluginManager import pytest --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -76,16 +76,21 @@ You can always run your tests by pointin ... .. _`package name`: - -.. note:: + +.. note:: Test modules are imported under their fully qualified name as follows: - * ``basedir`` = first upward directory not containing an ``__init__.py`` + * find ``basedir`` -- this is the first "upward" directory not + containing an ``__init__.py`` - * perform ``sys.path.insert(0, basedir)``. + * perform ``sys.path.insert(0, basedir)`` to make the fully + qualified test module path importable. - * ``import path.to.test_module`` + * ``import path.to.test_module`` where the path is determined + by converting path separators into "." files. This means + you must follow the convention of having directory and file + names map to the import names. .. _standalone: .. _`genscript method`: @@ -94,7 +99,7 @@ Generating a py.test standalone Script ------------------------------------------- If you are a maintainer or application developer and want others -to easily run tests you can generate a completely standalone "py.test" +to easily run tests you can generate a completely standalone "py.test" script:: py.test --genscript=runtests.py --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def main(): ) def cmdline_entrypoints(versioninfo, platform, basename): - target = 'pytest:__main__' + target = 'pytest:main' if platform.startswith('java'): points = {'py.test-jython': target} else: --- a/testing/test_pluginmanager.py +++ /dev/null @@ -1,604 +0,0 @@ -import py, os -from pytest._core import PluginManager, canonical_importname -from pytest._core import MultiCall, HookRelay, varnames - - -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - py.test.raises(ImportError, "pluginmanager.consider_env()") - - def test_preparse_args(self): - pluginmanager = PluginManager() - py.test.raises(ImportError, """ - pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) - """) - - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(pytest_skipping1=""" - import py - py.test.skip("hello") - """) - p.copy(p.dirpath("pytest_skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*hint*skipping2*hello*", - "*hint*skipping1*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_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('pytest_xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = py.test.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "mytestplugin" - 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 - plugin2 = pluginmanager.getplugin("pytest_mytestplugin") - assert plugin2 == plugin - - 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): - x500 = testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import py - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('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() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - reset = testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("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("hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - reset = 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_plug1="#") - testdir.makepyfile(pytest_plug2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" - assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" - - 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 = PluginManager() - reprec = testdir.getreportrecorder(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() - py.test.raises(ImportError, "pp.consider_conftest(mod)") - - def test_pm(self): - pp = PluginManager() - 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() - assert a1 in l - assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) - pp.unregister(name="hello") - assert not pp.isregistered(a2) - - 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 - py.test.raises(AssertionError, "pp.register(mod)") - mod2 = py.std.types.ModuleType("pytest_hello") - #pp.register(mod2) # double pm - py.test.raises(AssertionError, "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('xyz') - assert pp.getplugin('xyz') == mod - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) - - def test_register_mismatch_method(self): - pp = PluginManager(load=True) - class hello: - def pytest_gurgel(self): - pass - py.test.raises(Exception, "pp.register(hello())") - - def test_register_mismatch_arg(self): - pp = PluginManager(load=True) - class hello: - def pytest_configure(self, asd): - pass - excinfo = py.test.raises(Exception, "pp.register(hello())") - - def test_canonical_importname(self): - for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': - impname = canonical_importname(name) - - def test_notify_exception(self, capfd): - pp = PluginManager() - excinfo = py.test.raises(ValueError, "raise ValueError(1)") - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert "ValueError" in err - class A: - def pytest_internalerror(self, excrepr): - return True - pp.register(A()) - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert not err - - def test_register(self): - pm = PluginManager(load=False) - class MyPlugin: - pass - my = MyPlugin() - pm.register(my) - assert pm.getplugins() - my2 = MyPlugin() - pm.register(my2) - assert pm.getplugins()[1:] == [my, my2] - - assert pm.isregistered(my) - assert pm.isregistered(my2) - pm.unregister(my) - assert not pm.isregistered(my) - assert pm.getplugins()[1:] == [my2] - - def test_listattr(self): - plugins = PluginManager() - 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_trace(self): - pm = PluginManager() - class api1: - x = 41 - l = [] - pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) - p = api1() - pm.register(p) - assert len(l) == 1 - kw, args = l[0] - assert args[0] == "registered" - assert args[1] == p - -class TestPytestPluginInteractions: - - def test_addhooks_conftestplugin(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - def pytest_myhook(xyz): - "new hook" - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - def pytest_myhook(xyz): - return xyz + 1 - """) - config = testdir.Config() - config._conftest.importconftest(conf) - print(config.pluginmanager.getplugins()) - res = config.hook.pytest_myhook(xyz=10) - assert res == [11] - - def test_addhooks_docstring_error(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - class A: # no pytest_ prefix - pass - def pytest_myhook(xyz): - pass - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*docstring*pytest_myhook*newhooks*" - ]) - - def test_addhooks_nohooks(self, testdir): - conf = testdir.makeconftest(""" - import sys - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(sys) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*did not find*sys*" - ]) - - def test_do_option_conftestplugin(self, testdir): - p = testdir.makepyfile(""" - def pytest_addoption(parser): - parser.addoption('--test123', action="store_true") - """) - config = testdir.Config() - config._conftest.importconftest(p) - print(config.pluginmanager.getplugins()) - config.parse([]) - assert not config.option.test123 - - def test_namespace_early_from_import(self, testdir): - p = testdir.makepyfile(""" - from py.test.collect import Item - from pytest.collect import Item as Item2 - assert Item is Item2 - """) - result = testdir.runpython(p) - assert result.ret == 0 - - def test_do_ext_namespace(self, testdir): - testdir.makeconftest(""" - def pytest_namespace(): - return {'hello': 'world'} - """) - p = testdir.makepyfile(""" - from py.test import hello - import py - def test_hello(): - assert hello == "world" - assert 'hello' in py.test.__all__ - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - def test_do_option_postinitialize(self, testdir): - config = testdir.Config() - config.parse([]) - config.pluginmanager.do_configure(config=config) - assert not hasattr(config.option, 'test123') - p = testdir.makepyfile(""" - def pytest_addoption(parser): - parser.addoption('--test123', action="store_true", - default=True) - """) - config._conftest.importconftest(p) - assert config.option.test123 - - def test_configure(self, testdir): - config = testdir.parseconfig() - l = [] - class A: - def pytest_configure(self, config): - l.append(self) - - config.pluginmanager.register(A()) - assert len(l) == 0 - config.pluginmanager.do_configure(config=config) - assert len(l) == 1 - config.pluginmanager.register(A()) # this should lead to a configured() plugin - assert len(l) == 2 - assert l[0] != l[1] - - config.pluginmanager.do_unconfigure(config=config) - config.pluginmanager.register(A()) - assert len(l) == 2 - - # lower level API - - def test_listattr(self): - pluginmanager = PluginManager() - class My2: - x = 42 - pluginmanager.register(My2()) - assert not pluginmanager.listattr("hello") - assert pluginmanager.listattr("x") == [42] - -def test_namespace_has_default_and_env_plugins(testdir): - p = testdir.makepyfile(""" - import py - py.test.mark - """) - result = testdir.runpython(p) - assert result.ret == 0 - -def test_varnames(): - def f(x): - i = 3 - class A: - def f(self, y): - pass - class B(object): - def __call__(self, z): - pass - assert varnames(f) == ("x",) - assert varnames(A().f) == ('y',) - assert varnames(B()) == ('z',) - -class TestMultiCall: - def test_uses_copy_of_methods(self): - l = [lambda: 42] - mc = MultiCall(l, {}) - repr(mc) - l[:] = [] - res = mc.execute() - return res == 42 - - def test_call_passing(self): - class P1: - def m(self, __multicall__, x): - assert len(__multicall__.results) == 1 - assert not __multicall__.methods - return 17 - - class P2: - def m(self, __multicall__, x): - assert __multicall__.results == [] - assert __multicall__.methods - return 23 - - p1 = P1() - p2 = P2() - multicall = MultiCall([p1.m, p2.m], {'x': 23}) - assert "23" in repr(multicall) - reslist = multicall.execute() - assert len(reslist) == 2 - # ensure reversed order - assert reslist == [23, 17] - - def test_keyword_args(self): - def f(x): - return x + 1 - class A: - def f(self, x, y): - return x + y - multicall = MultiCall([f, A().f], dict(x=23, y=24)) - assert "'x': 23" in repr(multicall) - assert "'y': 24" in repr(multicall) - reslist = multicall.execute() - assert reslist == [24+23, 24] - assert "2 results" in repr(multicall) - - def test_keyword_args_with_defaultargs(self): - def f(x, z=1): - return x + z - reslist = MultiCall([f], dict(x=23, y=24)).execute() - assert reslist == [24] - reslist = MultiCall([f], dict(x=23, z=2)).execute() - assert reslist == [25] - - def test_tags_call_error(self): - multicall = MultiCall([lambda x: x], {}) - py.test.raises(TypeError, "multicall.execute()") - - def test_call_subexecute(self): - def m(__multicall__): - subresult = __multicall__.execute() - return subresult + 1 - - def n(): - return 1 - - call = MultiCall([n, m], {}, firstresult=True) - res = call.execute() - assert res == 2 - - def test_call_none_is_no_result(self): - def m1(): - return 1 - def m2(): - return None - res = MultiCall([m1, m2], {}, firstresult=True).execute() - assert res == 1 - res = MultiCall([m1, m2], {}).execute() - assert res == [1] - -class TestHookRelay: - def test_happypath(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - assert hasattr(mcm, 'hello') - assert repr(mcm.hello).find("hello") != -1 - class Plugin: - def hello(self, arg): - return arg + 1 - pm.register(Plugin()) - l = mcm.hello(arg=3) - assert l == [4] - assert not hasattr(mcm, 'world') - - def test_only_kwargs(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - py.test.raises(TypeError, "mcm.hello(3)") - - def test_firstresult_definition(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - hello.firstresult = True - - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - class Plugin: - def hello(self, arg): - return arg + 1 - pm.register(Plugin()) - res = mcm.hello(arg=3) - assert res == 4 - -class TestTracer: - def test_simple(self): - from pytest._core import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("pytest") - log("hello") - l = [] - rootlogger.setwriter(l.append) - log("world") - assert len(l) == 1 - assert l[0] == "[pytest] world\n" - sublog = log.get("collection") - sublog("hello") - assert l[1] == "[pytest:collection] hello\n" - - def test_setprocessor(self): - from pytest._core import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - assert log2.tags == tuple("12") - l = [] - rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) - log("not seen") - log2("seen") - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == ("seen",) - l2 = [] - rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) - log2("seen") - tags, args = l2[0] - assert args == ("seen",) - - - def test_setmyprocessor(self): - from pytest._core import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - l = [] - log2.setmyprocessor(lambda *args: l.append(args)) - log("not seen") - assert not l - log2(42) - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == (42,) --- /dev/null +++ b/testing/test_main.py @@ -0,0 +1,604 @@ +import py, os +from pytest.main import PluginManager, canonical_importname +from pytest.main import MultiCall, HookRelay, varnames + + +class TestBootstrapping: + def test_consider_env_fails_to_import(self, monkeypatch): + pluginmanager = PluginManager() + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + py.test.raises(ImportError, "pluginmanager.consider_env()") + + def test_preparse_args(self): + pluginmanager = PluginManager() + py.test.raises(ImportError, """ + pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) + """) + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(pytest_skipping1=""" + import py + py.test.skip("hello") + """) + p.copy(p.dirpath("pytest_skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*hint*skipping2*hello*", + "*hint*skipping1*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): + pluginmanager = PluginManager() + testdir.syspathinsert() + testdir.makepyfile(pytest_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('pytest_xy123') + pluginmanager.consider_env() + l3 = len(pluginmanager.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + 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 + plugin2 = pluginmanager.getplugin("pytest_mytestplugin") + assert plugin2 == plugin + + 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): + x500 = testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import py + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('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() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') + + reset = testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pluginmanager.import_plugin("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("hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir): + pluginmanager = PluginManager() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') + + reset = 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_plug1="#") + testdir.makepyfile(pytest_plug2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + pluginmanager.consider_module(mod) + assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" + assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" + + 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 = PluginManager() + reprec = testdir.getreportrecorder(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() + py.test.raises(ImportError, "pp.consider_conftest(mod)") + + def test_pm(self): + pp = PluginManager() + 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() + assert a1 in l + assert a2 in l + assert pp.getplugin('hello') == a2 + pp.unregister(a1) + assert not pp.isregistered(a1) + pp.unregister(name="hello") + assert not pp.isregistered(a2) + + 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 + py.test.raises(AssertionError, "pp.register(mod)") + mod2 = py.std.types.ModuleType("pytest_hello") + #pp.register(mod2) # double pm + py.test.raises(AssertionError, "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('xyz') + assert pp.getplugin('xyz') == mod + assert pp.getplugin('pytest_xyz') == mod + assert pp.isregistered(mod) + + def test_register_mismatch_method(self): + pp = PluginManager(load=True) + class hello: + def pytest_gurgel(self): + pass + py.test.raises(Exception, "pp.register(hello())") + + def test_register_mismatch_arg(self): + pp = PluginManager(load=True) + class hello: + def pytest_configure(self, asd): + pass + excinfo = py.test.raises(Exception, "pp.register(hello())") + + def test_canonical_importname(self): + for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': + impname = canonical_importname(name) + + def test_notify_exception(self, capfd): + pp = PluginManager() + excinfo = py.test.raises(ValueError, "raise ValueError(1)") + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert "ValueError" in err + class A: + def pytest_internalerror(self, excrepr): + return True + pp.register(A()) + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert not err + + def test_register(self): + pm = PluginManager(load=False) + class MyPlugin: + pass + my = MyPlugin() + pm.register(my) + assert pm.getplugins() + my2 = MyPlugin() + pm.register(my2) + assert pm.getplugins()[1:] == [my, my2] + + assert pm.isregistered(my) + assert pm.isregistered(my2) + pm.unregister(my) + assert not pm.isregistered(my) + assert pm.getplugins()[1:] == [my2] + + def test_listattr(self): + plugins = PluginManager() + 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_trace(self): + pm = PluginManager() + class api1: + x = 41 + l = [] + pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) + p = api1() + pm.register(p) + assert len(l) == 1 + kw, args = l[0] + assert args[0] == "registered" + assert args[1] == p + +class TestPytestPluginInteractions: + + def test_addhooks_conftestplugin(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + def pytest_myhook(xyz): + "new hook" + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + def pytest_myhook(xyz): + return xyz + 1 + """) + config = testdir.Config() + config._conftest.importconftest(conf) + print(config.pluginmanager.getplugins()) + res = config.hook.pytest_myhook(xyz=10) + assert res == [11] + + def test_addhooks_docstring_error(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + class A: # no pytest_ prefix + pass + def pytest_myhook(xyz): + pass + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*docstring*pytest_myhook*newhooks*" + ]) + + def test_addhooks_nohooks(self, testdir): + conf = testdir.makeconftest(""" + import sys + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(sys) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*did not find*sys*" + ]) + + def test_do_option_conftestplugin(self, testdir): + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true") + """) + config = testdir.Config() + config._conftest.importconftest(p) + print(config.pluginmanager.getplugins()) + config.parse([]) + assert not config.option.test123 + + def test_namespace_early_from_import(self, testdir): + p = testdir.makepyfile(""" + from py.test.collect import Item + from pytest.collect import Item as Item2 + assert Item is Item2 + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_do_ext_namespace(self, testdir): + testdir.makeconftest(""" + def pytest_namespace(): + return {'hello': 'world'} + """) + p = testdir.makepyfile(""" + from py.test import hello + import py + def test_hello(): + assert hello == "world" + assert 'hello' in py.test.__all__ + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_do_option_postinitialize(self, testdir): + config = testdir.Config() + config.parse([]) + config.pluginmanager.do_configure(config=config) + assert not hasattr(config.option, 'test123') + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true", + default=True) + """) + config._conftest.importconftest(p) + assert config.option.test123 + + def test_configure(self, testdir): + config = testdir.parseconfig() + l = [] + class A: + def pytest_configure(self, config): + l.append(self) + + config.pluginmanager.register(A()) + assert len(l) == 0 + config.pluginmanager.do_configure(config=config) + assert len(l) == 1 + config.pluginmanager.register(A()) # this should lead to a configured() plugin + assert len(l) == 2 + assert l[0] != l[1] + + config.pluginmanager.do_unconfigure(config=config) + config.pluginmanager.register(A()) + assert len(l) == 2 + + # lower level API + + def test_listattr(self): + pluginmanager = PluginManager() + class My2: + x = 42 + pluginmanager.register(My2()) + assert not pluginmanager.listattr("hello") + assert pluginmanager.listattr("x") == [42] + +def test_namespace_has_default_and_env_plugins(testdir): + p = testdir.makepyfile(""" + import py + py.test.mark + """) + result = testdir.runpython(p) + assert result.ret == 0 + +def test_varnames(): + def f(x): + i = 3 + class A: + def f(self, y): + pass + class B(object): + def __call__(self, z): + pass + assert varnames(f) == ("x",) + assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) + +class TestMultiCall: + def test_uses_copy_of_methods(self): + l = [lambda: 42] + mc = MultiCall(l, {}) + repr(mc) + l[:] = [] + res = mc.execute() + return res == 42 + + def test_call_passing(self): + class P1: + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.methods + return 17 + + class P2: + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.methods + return 23 + + p1 = P1() + p2 = P2() + multicall = MultiCall([p1.m, p2.m], {'x': 23}) + assert "23" in repr(multicall) + reslist = multicall.execute() + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + def test_keyword_args(self): + def f(x): + return x + 1 + class A: + def f(self, x, y): + return x + y + multicall = MultiCall([f, A().f], dict(x=23, y=24)) + assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) + reslist = multicall.execute() + assert reslist == [24+23, 24] + assert "2 results" in repr(multicall) + + def test_keyword_args_with_defaultargs(self): + def f(x, z=1): + return x + z + reslist = MultiCall([f], dict(x=23, y=24)).execute() + assert reslist == [24] + reslist = MultiCall([f], dict(x=23, z=2)).execute() + assert reslist == [25] + + def test_tags_call_error(self): + multicall = MultiCall([lambda x: x], {}) + py.test.raises(TypeError, "multicall.execute()") + + def test_call_subexecute(self): + def m(__multicall__): + subresult = __multicall__.execute() + return subresult + 1 + + def n(): + return 1 + + call = MultiCall([n, m], {}, firstresult=True) + res = call.execute() + assert res == 2 + + def test_call_none_is_no_result(self): + def m1(): + return 1 + def m2(): + return None + res = MultiCall([m1, m2], {}, firstresult=True).execute() + assert res == 1 + res = MultiCall([m1, m2], {}).execute() + assert res == [1] + +class TestHookRelay: + def test_happypath(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + assert hasattr(mcm, 'hello') + assert repr(mcm.hello).find("hello") != -1 + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + l = mcm.hello(arg=3) + assert l == [4] + assert not hasattr(mcm, 'world') + + def test_only_kwargs(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + py.test.raises(TypeError, "mcm.hello(3)") + + def test_firstresult_definition(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + hello.firstresult = True + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + res = mcm.hello(arg=3) + assert res == 4 + +class TestTracer: + def test_simple(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "[pytest] world\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "[pytest:collection] hello\n" + + def test_setprocessor(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + + def test_setmyprocessor(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ Changes between 1.3.4 and 2.0.0dev0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 +- new ability: python -m pytest / python -m pytest.main ability +- new python invcation: pytest.main(args, plugins) to load + some custom plugins early. - try harder to run unittest test suites in a more compatible manner by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, --- a/doc/customize.txt +++ b/doc/customize.txt @@ -58,8 +58,8 @@ builtin configuration file options .. confval:: norecursedirs Set the directory basename patterns to avoid when recursing - for test discovery. The individual (fnmatch-style) patterns are - applied to the basename of a directory to decide if to recurse into it. + for test discovery. The individual (fnmatch-style) patterns are + applied to the basename of a directory to decide if to recurse into it. Pattern matching characters:: * matches everything @@ -68,7 +68,7 @@ builtin configuration file options [!seq] matches any char not in seq Default patterns are ``.* _* CVS {args}``. Setting a ``norecurse`` - replaces the default. Here is a customizing example for avoiding + replaces the default. Here is a customizing example for avoiding a different set of directories:: # content of setup.cfg --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -4,6 +4,7 @@ py.test reference documentation ================================================ + .. toctree:: :maxdepth: 2 --- /dev/null +++ b/doc/usage.txt @@ -0,0 +1,165 @@ + +.. _usage: + +Usage and Invocations +========================================== + + +.. _cmdline: + +Getting help on version, option names, environment vars +----------------------------------------------------------- + +:: + + py.test --version # shows where pytest was imported from + py.test --funcargs # show available builtin function arguments + py.test -h | --help # show help on command line and config file options + + +Stopping after the first (or N) failures +--------------------------------------------------- + +To stop the testing process after the first (N) failures:: + + py.test -x # stop after first failure + py.test -maxfail=2 # stop after two failures + +calling pytest from Python code +---------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke ``py.test`` from Python code directly:: + + pytest.main() + +this acts as if you would call "py.test" from the command line. +It will not raise ``SystemExit`` but return the exitcode instead. +You can pass in options and arguments:: + + pytest.main(['x', 'mytestdir']) + +or pass in a string:: + + pytest.main("-x mytestdir") + +You can specify additional plugins to ``pytest.main``:: + + # content of myinvoke.py + import pytest + class MyPlugin: + def pytest_addoption(self, parser): + raise pytest.UsageError("hi from our plugin") + + pytest.main(plugins=[MyPlugin()]) + +Running it will exit quickly:: + + $ python myinvoke.py + ERROR: hi from our plugin + +calling pytest through ``python -m pytest`` +----------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke testing through the Python interpreter from the command line:: + + python -m pytest.main [...] + +Python2.7 and Python3 introduced specifying packages to "-m" so there +you can also type:: + + python -m pytest [...] + +All of these invocations are equivalent to the ``py.test [...]`` command line invocation. + + +Modifying Python traceback printing +---------------------------------------------- + +Examples for modifying traceback printing:: + + py.test --showlocals # show local variables in tracebacks + py.test -l # show local variables (shortcut) + + py.test --tb=long # the default informative traceback formatting + py.test --tb=native # the Python standard library formatting + py.test --tb=short # a shorter traceback format + py.test --tb=line # only one line per failure + +Dropping to PDB (Python Debugger) on failures +---------------------------------------------- + +.. _PDB: http://docs.python.org/library/pdb.html + +Python comes with a builtin Python debugger called PDB_. ``py.test`` +allows to drop into the PDB prompt via a command line option:: + + py.test --pdb + +This will invoke the Python debugger on every failure. Often you might +only want to do this for the first failing test to understand a certain +failure situation:: + + py.test -x --pdb # drop to PDB on first failure, then end test session + py.test --pdb --maxfail=3 # drop to PDB for the first three failures + + +Setting a breakpoint / aka ``set_trace()`` +---------------------------------------------------- + +If you want to set a breakpoint and enter the ``pdb.set_trace()`` you +can use a helper:: + + def test_function(): + ... + py.test.set_trace() # invoke PDB debugger and tracing + +.. versionadded: 2.0.0 + +In previous versions you could only enter PDB tracing if +you :ref:`disable capturing`. + +creating JUnitXML format files +---------------------------------------------------- + +To create result files which can be read by Hudson_ or other Continous +integration servers, use this invocation:: + + py.test --junitxml=path + +to create an XML file at ``path``. + +creating resultlog format files +---------------------------------------------------- + +To create plain-text machine-readable result files you can issue:: + + py.test --resultlog=path + +and look at the content at the ``path`` location. Such files are used e.g. +by the `PyPy-test`_ web page to show test results over several revisions. + +.. _`PyPy-test`: http://codespeak.net:8099/summary + + +send test report to pocoo pastebin service +----------------------------------------------------- + +**Creating a URL for each test failure**:: + + py.test --pastebin=failed + +This will submit test run information to a remote Paste service and +provide a URL for each failure. You may select tests as usual or add +for example ``-x`` if you only want to send one particular failure. + +**Creating a URL for a whole test session log**:: + + py.test --pastebin=all + +Currently only pasting to the http://paste.pocoo.org service is implemented. + +.. include:: links.inc --- a/doc/overview.txt +++ b/doc/overview.txt @@ -7,7 +7,7 @@ Overview and Introduction features.txt getting-started.txt - cmdline.txt + usage.txt goodpractises.txt faq.txt --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -2,7 +2,7 @@ funcargs and support code for testing py.test's own functionality. """ -import py +import py, pytest import sys, os import re import inspect @@ -10,7 +10,7 @@ import time from fnmatch import fnmatch from pytest.plugin.session import Collection from py.builtin import print_ -from pytest._core import HookRelay +from pytest.main import HookRelay def pytest_addoption(parser): group = parser.getgroup("pylib") @@ -401,6 +401,10 @@ class TmpTestdir: #print "env", env return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + def pytestmain(self, *args, **kwargs): + ret = pytest.main(*args, **kwargs) + if ret == 2: + raise KeyboardInterrupt() def run(self, *cmdargs): return self._run(*cmdargs) --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,4 @@ -import sys, py +import sys, py, pytest class TestGeneralUsage: def test_config_error(self, testdir): @@ -82,36 +82,6 @@ class TestGeneralUsage: ]) - def test_earlyinit(self, testdir): - p = testdir.makepyfile(""" - import py - assert hasattr(py.test, 'mark') - """) - result = testdir.runpython(p) - assert result.ret == 0 - - def test_pydoc(self, testdir): - result = testdir.runpython_c("import py;help(py.test)") - assert result.ret == 0 - s = result.stdout.str() - assert 'MarkGenerator' in s - - def test_double_pytestcmdline(self, testdir): - p = testdir.makepyfile(run=""" - import py - py.test.cmdline.main() - py.test.cmdline.main() - """) - testdir.makepyfile(""" - def test_hello(): - pass - """) - result = testdir.runpython(p) - result.stdout.fnmatch_lines([ - "*1 passed*", - "*1 passed*", - ]) - @py.test.mark.xfail def test_early_skip(self, testdir): @@ -225,19 +195,6 @@ class TestGeneralUsage: "*1 pass*", ]) - - @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_minus_m_invocation_ok(self, testdir): - p1 = testdir.makepyfile("def test_hello(): pass") - res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) - assert res.ret == 0 - - @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_minus_m_invocation_fail(self, testdir): - p1 = testdir.makepyfile("def test_fail(): 0/0") - res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) - assert res.ret == 1 - def test_skip_on_generated_funcarg_id(self, testdir): testdir.makeconftest(""" import py @@ -253,3 +210,83 @@ class TestGeneralUsage: res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) + +class TestInvocationVariants: + def test_earlyinit(self, testdir): + p = testdir.makepyfile(""" + import py + assert hasattr(py.test, 'mark') + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_pydoc(self, testdir): + result = testdir.runpython_c("import py;help(py.test)") + assert result.ret == 0 + s = result.stdout.str() + assert 'MarkGenerator' in s + + def test_double_pytestcmdline(self, testdir): + p = testdir.makepyfile(run=""" + import py + py.test.cmdline.main() + py.test.cmdline.main() + """) + testdir.makepyfile(""" + def test_hello(): + pass + """) + result = testdir.runpython(p) + result.stdout.fnmatch_lines([ + "*1 passed*", + "*1 passed*", + ]) + + @py.test.mark.skipif("sys.version_info < (2,5)") + def test_python_minus_m_invocation_ok(self, testdir): + p1 = testdir.makepyfile("def test_hello(): pass") + res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) + assert res.ret == 0 + + @py.test.mark.skipif("sys.version_info < (2,5)") + def test_python_minus_m_invocation_fail(self, testdir): + p1 = testdir.makepyfile("def test_fail(): 0/0") + res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) + assert res.ret == 1 + + def test_python_pytest_main(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + @py.test.mark.skipif("sys.version_info < (2,7)") + def test_python_pytest_package(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_equivalence_pytest_pytest(self): + assert pytest.main == py.test.cmdline.main + + def test_invoke_with_string(self, capsys): + retcode = pytest.main("-h") + assert not retcode + out, err = capsys.readouterr() + assert "--help" in out + + def test_invoke_with_path(self, testdir, capsys): + retcode = testdir.pytestmain(testdir.tmpdir) + assert not retcode + out, err = capsys.readouterr() + + def test_invoke_plugin_api(self, capsys): + class MyPlugin: + def pytest_addoption(self, parser): + parser.addoption("--myopt") + + pytest.main(["-h"], plugins=[MyPlugin()]) + out, err = capsys.readouterr() + assert "--myopt" in out + --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -461,7 +461,7 @@ def hasinit(obj): def getfuncargnames(function): - # XXX merge with _core.py's varnames + # XXX merge with main.py's varnames argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] startindex = py.std.inspect.ismethod(function) and 1 or 0 defaults = getattr(function, 'func_defaults', --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -1,7 +1,19 @@ -pytest builtin helpers +py.test builtin helpers ================================================ +builtin py.test.* helpers +----------------------------------------------------- + +You can always use an interactive Python prompt and type:: + + import pytest + help(pytest) + +to get an overview on available globally available helpers. + +.. automodule:: pytest + :members: builtin function arguments ----------------------------------------------------- @@ -54,17 +66,3 @@ You can ask for available builtin or pro * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - -builtin py.test.* helpers ------------------------------------------------------ - -You can always use an interactive Python prompt and type:: - - import pytest - help(pytest) - -to get an overview on available globally available helpers. - -.. automodule:: pytest - :members: - --- a/testing/plugin/test_pytester.py +++ b/testing/plugin/test_pytester.py @@ -1,7 +1,7 @@ import py import os, sys from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder -from pytest._core import PluginManager +from pytest.main import PluginManager def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -97,7 +97,7 @@ def test_hookrecorder_basic_no_args_hook def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" import py - from pytest._core import HookRelay, PluginManager + from pytest.main import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): class ApiClass: --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -1,4 +1,4 @@ -""" hook specifications for pytest plugins, invoked from _core.py and builtin plugins. """ +""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ # ------------------------------------------------------------------------- # Initialization --- a/doc/features.txt +++ b/doc/features.txt @@ -5,8 +5,8 @@ no-boilerplate testing with Python ---------------------------------- - automatic, fully customizable Python test discovery -- :pep:`8` consistent testing style -- allows simple test functions +- allows fully :pep:`8` compliant coding style +- write simple test functions and freely group tests - ``assert`` statement for your assertions - powerful parametrization of test functions - rely on powerful traceback and assertion reporting @@ -25,8 +25,8 @@ extensive plugin and customization syste mature command line testing tool -------------------------------------- +- powerful :ref:`usage` possibilities - used in many projects, ranging from 10 to 10K tests -- autodiscovery of tests - simple well sorted command line options - runs on Unix, Windows from Python 2.4 up to Python 3.1 and 3.2 - is itself tested extensively on a CI server --- a/testing/plugin/conftest.py +++ b/testing/plugin/conftest.py @@ -2,7 +2,7 @@ import py import pytest.plugin plugindir = py.path.local(pytest.plugin.__file__).dirpath() -from pytest._core import default_plugins +from pytest.main import default_plugins def pytest_collect_file(path, parent): if path.basename.startswith("pytest_") and path.ext == ".py": --- /dev/null +++ b/pytest/main.py @@ -0,0 +1,412 @@ +""" +pytest PluginManager, basic initialization and tracing. +All else is in pytest/plugin. +(c) Holger Krekel 2004-2010 +""" +import sys, os +import inspect +import py +from pytest import hookspec # the extension point definitions + +assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " + "%s is too old, remove or upgrade 'py'" % (py.__version__)) + +default_plugins = ( + "config session terminal runner python pdb capture unittest mark skipping " + "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " + "junitxml doctest").split() + +IMPORTPREFIX = "pytest_" + +class TagTracer: + def __init__(self): + self._tag2proc = {} + self.writer = None + + def get(self, name): + return TagTracerSub(self, (name,)) + + def processmessage(self, tags, args): + if self.writer is not None: + prefix = ":".join(tags) + content = " ".join(map(str, args)) + self.writer("[%s] %s\n" %(prefix, content)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + 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,)) + +class PluginManager(object): + def __init__(self, load=False): + self._name2plugin = {} + self._plugins = [] + self._hints = [] + self.trace = TagTracer().get("pytest") + if os.environ.get('PYTEST_DEBUG'): + self.trace.root.setwriter(sys.stderr.write) + self.hook = HookRelay([hookspec], pm=self) + self.register(self) + if load: + for spec in default_plugins: + self.import_plugin(spec) + + def _getpluginname(self, plugin, name): + if name is None: + if hasattr(plugin, '__name__'): + name = plugin.__name__.split(".")[-1] + else: + name = id(plugin) + return name + + def register(self, plugin, name=None, prepend=False): + assert not self.isregistered(plugin), plugin + assert not self.isregistered(plugin), plugin + name = self._getpluginname(plugin, name) + if name in self._name2plugin: + return False + self._name2plugin[name] = plugin + self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) + self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + self.trace("registered", plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) + return True + + def unregister(self, plugin=None, name=None): + if plugin is None: + plugin = self.getplugin(name=name) + self._plugins.remove(plugin) + self.hook.pytest_plugin_unregistered(plugin=plugin) + for name, value in list(self._name2plugin.items()): + if value == plugin: + del self._name2plugin[name] + + def isregistered(self, plugin, name=None): + if self._getpluginname(plugin, name) in self._name2plugin: + return True + for val in self._name2plugin.values(): + if plugin == val: + return True + + def addhooks(self, spec): + self.hook._addhooks(spec, prefix="pytest_") + + def getplugins(self): + return list(self._plugins) + + def skipifmissing(self, name): + if not self.hasplugin(name): + py.test.skip("plugin %r is missing" % name) + + def hasplugin(self, name): + try: + self.getplugin(name) + return True + except KeyError: + return False + + def getplugin(self, name): + try: + return self._name2plugin[name] + except KeyError: + impname = canonical_importname(name) + return self._name2plugin[impname] + + # API for bootstrapping + # + def _envlist(self, varname): + val = py.std.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 + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = canonical_importname(ep.name) + if name in self._name2plugin: + continue + plugin = ep.load() + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.import_plugin(opt2) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__): + 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, spec): + assert isinstance(spec, str) + modname = canonical_importname(spec) + if modname in self._name2plugin: + return + try: + mod = importplugin(modname) + except KeyboardInterrupt: + raise + except: + e = py.std.sys.exc_info()[1] + if not hasattr(py.test, 'skip'): + raise + elif not isinstance(e, py.test.skip.Exception): + raise + self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + self.register(mod, modname) + self.consider_module(mod) + + def pytest_plugin_registered(self, plugin): + dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + self._setns(py.test, dic) + if hasattr(self, '_config'): + self.call_plugin(plugin, "pytest_addoption", + {'parser': self._config._parser}) + self.call_plugin(plugin, "pytest_configure", + {'config': self._config}) + + def _setns(self, obj, dic): + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + mod = py.std.types.ModuleType(name) + sys.modules['pytest.%s' % name] = mod + sys.modules['py.test.%s' % name] = mod + mod.__all__ = [] + setattr(obj, name, mod) + self._setns(mod, value) + else: + #print "setting", name, value, "on", obj + setattr(obj, name, value) + obj.__all__.append(name) + + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + if terminalreporter.config.option.traceconfig: + for hint in self._hints: + tw.line("hint: %s" % hint) + + def do_configure(self, config): + assert not hasattr(self, '_config') + self._config = config + config.hook.pytest_configure(config=self._config) + + def do_unconfigure(self, config): + config = self._config + del self._config + config.hook.pytest_unconfigure(config=config) + config.pluginmanager.unregister(self) + + def notify_exception(self, excinfo): + excrepr = excinfo.getrepr(funcargs=True, showlocals=True) + res = self.hook.pytest_internalerror(excrepr=excrepr) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + + def listattr(self, attrname, plugins=None): + if plugins is None: + plugins = self._plugins + l = [] + for plugin in plugins: + try: + l.append(getattr(plugin, attrname)) + except AttributeError: + continue + return l + + def call_plugin(self, plugin, methname, kwargs): + return MultiCall(methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() + +def canonical_importname(name): + if '.' in name: + return name + name = name.lower() + if not name.startswith(IMPORTPREFIX): + name = IMPORTPREFIX + name + return name + +def importplugin(importspec): + try: + return __import__(importspec, None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(importspec) == -1: + raise + name = importspec + try: + if name.startswith("pytest_"): + name = importspec[7:] + return __import__("pytest.plugin.%s" %(name), None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(name) == -1: + raise + # show the original exception, not the failing internal one + return __import__(importspec, None, None, '__doc__') + + +class MultiCall: + """ execute a call into multiple python functions/methods. """ + def __init__(self, methods, kwargs, firstresult=False): + self.methods = list(methods) + self.kwargs = kwargs + 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): + while self.methods: + method = self.methods.pop() + kwargs = self.getkwargs(method) + res = method(**kwargs) + if res is not None: + self.results.append(res) + if self.firstresult: + return res + if not self.firstresult: + return self.results + + def getkwargs(self, method): + kwargs = {} + for argname in varnames(method): + try: + kwargs[argname] = self.kwargs[argname] + except KeyError: + if argname == "__multicall__": + kwargs[argname] = self + return kwargs + +def varnames(func): + if not inspect.isfunction(func) and not inspect.ismethod(func): + func = getattr(func, '__call__', func) + ismethod = inspect.ismethod(func) + rawcode = py.code.getrawcode(func) + try: + return rawcode.co_varnames[ismethod:rawcode.co_argcount] + except AttributeError: + return () + +class HookRelay: + def __init__(self, hookspecs, pm, prefix="pytest_"): + if not isinstance(hookspecs, list): + hookspecs = [hookspecs] + self._hookspecs = [] + self._pm = pm + for hookspec in hookspecs: + self._addhooks(hookspec, prefix) + + def _addhooks(self, hookspecs, prefix): + self._hookspecs.append(hookspecs) + added = False + for name, method in vars(hookspecs).items(): + if name.startswith(prefix): + if not method.__doc__: + raise ValueError("docstring required for hook %r, in %r" + % (method, hookspecs)) + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self, name, firstresult=firstresult) + 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, hookspecs,)) + + +class HookCaller: + def __init__(self, hookrelay, name, firstresult): + self.hookrelay = hookrelay + self.name = name + self.firstresult = firstresult + + def __repr__(self): + return "" %(self.name,) + + def __call__(self, **kwargs): + methods = self.hookrelay._pm.listattr(self.name) + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + return mc.execute() + + def pcall(self, plugins, **kwargs): + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + return mc.execute() + +_preinit = [PluginManager(load=True)] # triggers default plugin importing + +def main(args=None, plugins=None): + if args is None: + args = sys.argv[1:] + elif not isinstance(args, (tuple, list)): + args = py.std.shlex.split(str(args)) + if _preinit: + _pluginmanager = _preinit.pop(0) + else: # subsequent calls to main will create a fresh instance + _pluginmanager = PluginManager(load=True) + hook = _pluginmanager.hook + try: + if plugins: + for plugin in plugins: + _pluginmanager.register(plugin) + config = hook.pytest_cmdline_parse( + pluginmanager=_pluginmanager, args=args) + exitstatus = hook.pytest_cmdline_main(config=config) + except UsageError: + e = sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n" %(e.args[0],)) + exitstatus = 3 + return exitstatus + +class UsageError(Exception): + """ error in py.test usage or invocation""" + +if __name__ == '__main__': + raise SystemExit(main()) From commits-noreply at bitbucket.org Sat Nov 6 09:57:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:02 -0500 (CDT) Subject: [py-svn] pytest commit 3da7c9990c7d: majorly refactor collection process Message-ID: <20101106085702.052D3243F58@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289033884 -3600 # Node ID 3da7c9990c7de697f8a395956d68a277fff5c2fb # Parent e90c2abaefeb33d96b672654b2698281420acc54 majorly refactor collection process - get rid of py.test.collect.Directory alltogether. - introduce direct node.nodeid attribute - remove now superflous attributes on collect and test reports --- a/testing/test_collect.py +++ /dev/null @@ -1,288 +0,0 @@ -import py - -class TestCollector: - def test_collect_versus_item(self): - from pytest.collect import Collector, Item - assert not issubclass(Collector, Item) - assert not issubclass(Item, Collector) - - def test_compat_attributes(self, testdir, recwarn): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - recwarn.clear() - assert modcol.Module == py.test.collect.Module - recwarn.pop(DeprecationWarning) - assert modcol.Class == py.test.collect.Class - recwarn.pop(DeprecationWarning) - assert modcol.Item == py.test.collect.Item - recwarn.pop(DeprecationWarning) - assert modcol.File == py.test.collect.File - recwarn.pop(DeprecationWarning) - assert modcol.Function == py.test.collect.Function - recwarn.pop(DeprecationWarning) - - def test_check_equality(self, testdir): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) - fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) - - assert fn1 == fn2 - assert fn1 != modcol - if py.std.sys.version_info < (3, 0): - assert cmp(fn1, fn2) == 0 - assert hash(fn1) == hash(fn2) - - fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) - assert not (fn1 == fn3) - assert fn1 != fn3 - - for fn in fn1,fn2,fn3: - assert fn != 3 - assert fn != modcol - assert fn != [1,2,3] - assert [1,2,3] != fn - assert modcol != fn - - def test_getparent(self, testdir): - modcol = testdir.getmodulecol(""" - class TestClass: - def test_foo(): - pass - """) - cls = testdir.collect_by_name(modcol, "TestClass") - fn = testdir.collect_by_name( - testdir.collect_by_name(cls, "()"), "test_foo") - - parent = fn.getparent(py.test.collect.Module) - assert parent is modcol - - parent = fn.getparent(py.test.collect.Function) - assert parent is fn - - parent = fn.getparent(py.test.collect.Class) - assert parent is cls - - - def test_getcustomfile_roundtrip(self, testdir): - hello = testdir.makefile(".xxx", hello="world") - testdir.makepyfile(conftest=""" - import py - class CustomFile(py.test.collect.File): - pass - def pytest_collect_file(path, parent): - if path.ext == ".xxx": - return CustomFile(path, parent=parent) - """) - config = testdir.parseconfig(hello) - node = testdir.getnode(config, hello) - assert isinstance(node, py.test.collect.File) - assert node.name == "hello.xxx" - id = node.collection.getid(node) - nodes = node.collection.getbyid(id) - assert len(nodes) == 1 - assert isinstance(nodes[0], py.test.collect.File) - -class TestCollectFS: - def test_ignored_certain_directories(self, testdir): - tmpdir = testdir.tmpdir - tmpdir.ensure("_darcs", 'test_notfound.py') - tmpdir.ensure("CVS", 'test_notfound.py') - tmpdir.ensure("{arch}", 'test_notfound.py') - tmpdir.ensure(".whatever", 'test_notfound.py') - tmpdir.ensure(".bzr", 'test_notfound.py') - tmpdir.ensure("normal", 'test_found.py') - - result = testdir.runpytest("--collectonly") - s = result.stdout.str() - assert "test_notfound" not in s - assert "test_found" in s - - def test_custom_norecursedirs(self, testdir): - testdir.makeini(""" - [pytest] - norecursedirs = mydir xyz* - """) - tmpdir = testdir.tmpdir - tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") - tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") - tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") - rec = testdir.inline_run() - rec.assertoutcome(passed=1) - rec = testdir.inline_run("xyz123/test_2.py") - rec.assertoutcome(failed=1) - - def test_found_certain_testfiles(self, testdir): - p1 = testdir.makepyfile(test_found = "pass", found_test="pass") - col = testdir.getnode(testdir.parseconfig(p1), p1.dirpath()) - items = col.collect() # Directory collect returns files sorted by name - assert len(items) == 2 - assert items[1].name == 'test_found.py' - assert items[0].name == 'found_test.py' - - def test_directory_file_sorting(self, testdir): - p1 = testdir.makepyfile(test_one="hello") - p1.dirpath().mkdir("x") - p1.dirpath().mkdir("dir1") - testdir.makepyfile(test_two="hello") - p1.dirpath().mkdir("dir2") - config = testdir.parseconfig() - col = testdir.getnode(config, p1.dirpath()) - names = [x.name for x in col.collect()] - assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] - -class TestCollectPluginHookRelay: - def test_pytest_collect_file(self, testdir): - tmpdir = testdir.tmpdir - wascalled = [] - class Plugin: - def pytest_collect_file(self, path, parent): - wascalled.append(path) - config = testdir.Config() - config.pluginmanager.register(Plugin()) - config.parse([tmpdir]) - col = testdir.getnode(config, tmpdir) - testdir.makefile(".abc", "xyz") - res = col.collect() - assert len(wascalled) == 1 - assert wascalled[0].ext == '.abc' - - def test_pytest_collect_directory(self, testdir): - tmpdir = testdir.tmpdir - wascalled = [] - class Plugin: - def pytest_collect_directory(self, path, parent): - wascalled.append(path.basename) - return py.test.collect.Directory(path, parent) - testdir.plugins.append(Plugin()) - testdir.mkdir("hello") - testdir.mkdir("world") - reprec = testdir.inline_run() - assert "hello" in wascalled - assert "world" in wascalled - # make sure the directories do not get double-appended - colreports = reprec.getreports("pytest_collectreport") - names = [rep.nodenames[-1] for rep in colreports] - assert names.count("hello") == 1 - -class TestPrunetraceback: - def test_collection_error(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - result = testdir.runpytest(p) - assert "__import__" not in result.stdout.str(), "too long traceback" - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*mport*not_exists*" - ]) - - def test_custom_repr_failure(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - testdir.makeconftest(""" - import py - def pytest_collect_file(path, parent): - return MyFile(path, parent) - class MyError(Exception): - pass - class MyFile(py.test.collect.File): - def collect(self): - raise MyError() - def repr_failure(self, excinfo): - if excinfo.errisinstance(MyError): - return "hello world" - return py.test.collect.File.repr_failure(self, excinfo) - """) - - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*hello world*", - ]) - - @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") - def test_collect_report_postprocessing(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - testdir.makeconftest(""" - import py - def pytest_make_collect_report(__multicall__): - rep = __multicall__.execute() - rep.headerlines += ["header1"] - return rep - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*header1*", - ]) - - -class TestCustomConftests: - def test_ignore_collect_path(self, testdir): - testdir.makeconftest(""" - def pytest_ignore_collect(path, config): - return path.basename.startswith("x") or \ - path.basename == "test_one.py" - """) - testdir.mkdir("xy123").ensure("test_hello.py").write( - "syntax error" - ) - testdir.makepyfile("def test_hello(): pass") - testdir.makepyfile(test_one="syntax error") - result = testdir.runpytest() - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed*"]) - - def test_collectignore_exclude_on_option(self, testdir): - testdir.makeconftest(""" - collect_ignore = ['hello', 'test_world.py'] - def pytest_addoption(parser): - parser.addoption("--XX", action="store_true", default=False) - def pytest_configure(config): - if config.getvalue("XX"): - collect_ignore[:] = [] - """) - testdir.mkdir("hello") - testdir.makepyfile(test_world="#") - reprec = testdir.inline_run(testdir.tmpdir) - names = [rep.nodenames[-1] - for rep in reprec.getreports("pytest_collectreport")] - assert 'hello' not in names - assert 'test_world.py' not in names - reprec = testdir.inline_run(testdir.tmpdir, "--XX") - names = [rep.nodenames[-1] - for rep in reprec.getreports("pytest_collectreport")] - assert 'hello' in names - assert 'test_world.py' in names - - def test_pytest_fs_collect_hooks_are_seen(self, testdir): - conf = testdir.makeconftest(""" - import py - class MyDirectory(py.test.collect.Directory): - pass - class MyModule(py.test.collect.Module): - pass - def pytest_collect_directory(path, parent): - return MyDirectory(path, parent) - def pytest_collect_file(path, parent): - return MyModule(path, parent) - """) - sub = testdir.mkdir("sub") - p = testdir.makepyfile("def test_x(): pass") - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*MyDirectory*", - "*MyModule*", - "*test_x*" - ]) --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev18' +__version__ = '2.0.0.dev19' __all__ = ['config', 'cmdline'] --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -3,6 +3,7 @@ """ import py +import os import time def pytest_addoption(parser): @@ -36,7 +37,9 @@ class LogXML(object): self._durations = {} def _opentestcase(self, report): - names = report.nodenames + names = report.nodeid.split("::") + names[0] = names[0].replace(os.sep, '.') + names = tuple(names) d = {'time': self._durations.pop(names, "0")} names = [x.replace(".py", "") for x in names if x != "()"] classnames = names[:-1] --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -17,7 +17,7 @@ def pytest_addoption(parser): def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": - if config.getvalue("doctestmodules"): + if config.option.doctestmodules: return DoctestModule(path, parent) elif path.check(fnmatch=config.getvalue("doctestglob")): return DoctestTextfile(path, parent) --- a/pytest/plugin/capture.py +++ b/pytest/plugin/capture.py @@ -133,7 +133,11 @@ class CaptureManager: def pytest_make_collect_report(self, __multicall__, collector): method = self._getmethod(collector.config, collector.fspath) - self.resumecapture(method) + try: + self.resumecapture(method) + except ValueError: + return # recursive collect, XXX refactor capturing + # to allow for more lightweight recursive capturing try: rep = __multicall__.execute() finally: --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -257,9 +257,9 @@ class TestCollectionReports: assert not rep.skipped assert rep.passed locinfo = rep.location - assert locinfo[0] == col.fspath + assert locinfo[0] == col.fspath.basename assert not locinfo[1] - assert locinfo[2] == col.fspath + assert locinfo[2] == col.fspath.basename res = rep.result assert len(res) == 2 assert res[0].name == "test_func1" --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -255,7 +255,7 @@ class Config(object): ) #: a pluginmanager instance self.pluginmanager = pluginmanager or PluginManager(load=True) - self.trace = self.pluginmanager.trace.get("config") + self.trace = self.pluginmanager.trace.root.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -1,14 +1,9 @@ """ terminal reporting of the full testing process. """ -import py +import pytest,py import sys -# =============================================================================== -# plugin tests -# -# =============================================================================== - from pytest.plugin.terminal import TerminalReporter, \ CollectonlyReporter, repr_pythonversion, getreportopt from pytest.plugin import runner @@ -95,9 +90,8 @@ class TestTerminal: item = testdir.getitem("def test_func(): pass") tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) - nodeid = item.collection.getid(item) location = item.reportinfo() - tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, + tr.config.hook.pytest_runtest_logstart(nodeid=item.nodeid, location=location, fspath=str(item.fspath)) linecomp.assert_contains_lines([ "*test_show_runtest_logstart.py*" @@ -424,6 +418,7 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 + pytest.xfail("repair xdist") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -9,11 +9,13 @@ def runandparse(testdir, *args): return result, xmldoc def assert_attr(node, **kwargs): + __tracebackhide__ = True for name, expected in kwargs.items(): anode = node.getAttributeNode(name) assert anode, "node %r has no attribute %r" %(node, name) val = anode.value - assert val == str(expected) + if val != str(expected): + py.test.fail("%r != %r" %(str(val), str(expected))) class TestPython: def test_summing_simple(self, testdir): @@ -50,7 +52,7 @@ class TestPython: assert_attr(node, errors=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_setup_error.test_setup_error", + classname="test_setup_error", name="test_function") fnode = tnode.getElementsByTagName("error")[0] assert_attr(fnode, message="test setup failure") @@ -68,9 +70,21 @@ class TestPython: assert_attr(node, failures=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_classname_instance.test_classname_instance.TestClass", + classname="test_classname_instance.TestClass", name="test_method") + def test_classname_nested_dir(self, testdir): + p = testdir.tmpdir.ensure("sub", "test_hello.py") + p.write("def test_func(): 0/0") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="sub.test_hello", + name="test_func") + def test_internal_error(self, testdir): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makepyfile("def test_function(): pass") @@ -92,7 +106,7 @@ class TestPython: assert_attr(node, failures=1, tests=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_failure_function.test_failure_function", + classname="test_failure_function", name="test_fail") fnode = tnode.getElementsByTagName("failure")[0] assert_attr(fnode, message="test failure") @@ -112,11 +126,11 @@ class TestPython: assert_attr(node, failures=2, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_failure_escape.test_failure_escape", + classname="test_failure_escape", name="test_func[<]") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, - classname="test_failure_escape.test_failure_escape", + classname="test_failure_escape", name="test_func[&]") def test_junit_prefixing(self, testdir): @@ -133,11 +147,11 @@ class TestPython: assert_attr(node, failures=1, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="xyz.test_junit_prefixing.test_junit_prefixing", + classname="xyz.test_junit_prefixing", name="test_func") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, - classname="xyz.test_junit_prefixing.test_junit_prefixing." + classname="xyz.test_junit_prefixing." "TestHello", name="test_hello") @@ -153,7 +167,7 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_xfailure_function.test_xfailure_function", + classname="test_xfailure_function", name="test_xfail") fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="expected test failure") @@ -172,7 +186,7 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_xfailure_xpass.test_xfailure_xpass", + classname="test_xfailure_xpass", name="test_xpass") fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="xfail-marked test passes unexpectedly") --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev18', + version='2.0.0.dev19', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/CHANGELOG +++ b/CHANGELOG @@ -6,10 +6,10 @@ Changes between 1.3.4 and 2.0.0dev0 - new python invcation: pytest.main(args, plugins) to load some custom plugins early. - try harder to run unittest test suites in a more compatible manner - by deferring setup/teardown semantics to the unittest package. + by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, by default setup.cfg and tox.ini files are searched. The old - ways (certain environment variables, dynamic conftest.py reading + ways (certain environment variables, dynamic conftest.py reading is removed). - add a new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. @@ -26,7 +26,8 @@ Changes between 1.3.4 and 2.0.0dev0 output on assertion failures for comparisons and other cases (Floris Bruynooghe) - nose-plugin: pass through type-signature failures in setup/teardown functions instead of not calling them (Ed Singleton) -- major refactoring of internal collection handling +- remove py.test.collect.Directory (follows from a major refactoring + and simplification of the collection process) - majorly reduce py.test core code, shift function/python testing to own plugin - fix issue88 (finding custom test nodes from command line arg) - refine 'tmpdir' creation, will now create basenames better associated --- a/testing/test_config.py +++ b/testing/test_config.py @@ -81,7 +81,7 @@ class TestConfigAPI: config.trace.root.setwriter(l.append) config.trace("hello") assert len(l) == 1 - assert l[0] == "[pytest:config] hello\n" + assert l[0] == "[pytest] hello\n" def test_config_getvalue_honours_conftest(self, testdir): testdir.makepyfile(conftest="x=1") --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -14,11 +14,15 @@ EXIT_OK = 0 EXIT_TESTSFAILED = 1 EXIT_INTERRUPTED = 2 EXIT_INTERNALERROR = 3 -EXIT_NOHOSTS = 4 def pytest_addoption(parser): parser.addini("norecursedirs", "directory patterns to avoid for recursion", type="args", default=('.*', 'CVS', '_darcs', '{arch}')) + #parser.addini("dirpatterns", + # "patterns specifying possible locations of test files", + # type="linelist", default=["**/test_*.txt", + # "**/test_*.py", "**/*_test.py"] + #) group = parser.getgroup("general", "running and selection options") group._addoption('-x', '--exitfirst', action="store_true", default=False, dest="exitfirst", @@ -44,12 +48,11 @@ def pytest_addoption(parser): def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, - File=File, Directory=Directory)) + return dict(collect=dict(Item=Item, Collector=Collector, File=File)) def pytest_configure(config): py.test.config = config # compatibiltiy - if config.getvalue("exitfirst"): + if config.option.exitfirst: config.option.maxfail = 1 def pytest_cmdline_main(config): @@ -84,8 +87,10 @@ def pytest_cmdline_main(config): def pytest_collection(session): collection = session.collection assert not hasattr(collection, 'items') + + collection.perform_collect() hook = session.config.hook - collection.items = items = collection.perform_collect() + items = collection.items hook.pytest_collection_modifyitems(config=session.config, items=items) hook.pytest_collection_finish(collection=collection) return True @@ -108,18 +113,6 @@ def pytest_ignore_collect(path, config): ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -def pytest_collect_directory(path, parent): - # check if cmdline specified this dir or a subdir directly - for arg in parent.collection._argfspaths: - if path == arg or arg.relto(path): - break - else: - patterns = parent.config.getini("norecursedirs") - for pat in patterns or []: - if path.check(fnmatch=pat): - return - return Directory(path, parent=parent) - class Session(object): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ @@ -145,160 +138,17 @@ class Session(object): self._testsfailed) pytest_collectreport = pytest_runtest_logreport -class Collection: - def __init__(self, config): - self.config = config - self.topdir = gettopdir(self.config.args) - self._argfspaths = [py.path.local(decodearg(x)[0]) - for x in self.config.args] - x = pytest.collect.Directory(fspath=self.topdir, - config=config, collection=self) - self._topcollector = x.consider_dir(self.topdir) - self._topcollector.parent = None - - def _normalizearg(self, arg): - return "::".join(self._parsearg(arg)) - - def _parsearg(self, arg, base=None): - """ return normalized name list for a command line specified id - which might be of the form x/y/z::name1::name2 - and should result into the form x::y::z::name1::name2 - """ - if base is None: - base = py.path.local() - parts = str(arg).split("::") - path = base.join(parts[0], abs=True) - if not path.check(): - raise pytest.UsageError("file not found: %s" %(path,)) - topdir = self.topdir - if path != topdir and not path.relto(topdir): - raise pytest.UsageError("path %r is not relative to %r" % - (str(path), str(topdir))) - topparts = path.relto(topdir).split(path.sep) - return topparts + parts[1:] - - def getid(self, node): - """ return id for node, relative to topdir. """ - path = node.fspath - chain = [x for x in node.listchain() if x.fspath == path] - chain = chain[1:] - names = [x.name for x in chain if x.name != "()"] - relpath = path.relto(self.topdir) - if not relpath: - assert path == self.topdir - path = '' - else: - path = relpath - if os.sep != "/": - path = str(path).replace(os.sep, "/") - names.insert(0, path) - return "::".join(names) - - def getbyid(self, id): - """ return one or more nodes matching the id. """ - names = [x for x in id.split("::") if x] - if names and '/' in names[0]: - names[:1] = names[0].split("/") - return list(self.matchnodes([self._topcollector], names)) - - def perform_collect(self): - items = [] - for arg in self.config.args: - names = self._parsearg(arg) - try: - for node in self.matchnodes([self._topcollector], names): - items.extend(self.genitems(node)) - except NoMatch: - raise pytest.UsageError("can't collect: %s" % (arg,)) - return items - - def matchnodes(self, matching, names): - if not matching: - return - if not names: - for x in matching: - yield x - return - name = names[0] - names = names[1:] - for node in matching: - if isinstance(node, pytest.collect.Item): - if not name: - yield node - continue - assert isinstance(node, pytest.collect.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - #print "matching", rep.result, "against name", name - if rep.passed: - if not name: - for x in rep.result: - yield x - else: - matched = False - for x in rep.result: - try: - if x.name == name or x.fspath.basename == name: - for x in self.matchnodes([x], names): - yield x - matched = True - elif x.name == "()": # XXX special Instance() case - for x in self.matchnodes([x], [name] + names): - yield x - matched = True - except NoMatch: - pass - if not matched: - node.ihook.pytest_collectreport(report=rep) - raise NoMatch(name) - node.ihook.pytest_collectreport(report=rep) - - def genitems(self, node): - if isinstance(node, pytest.collect.Item): - node.ihook.pytest_itemcollected(item=node) - yield node - else: - assert isinstance(node, pytest.collect.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for subnode in rep.result: - for x in self.genitems(subnode): - yield x - node.ihook.pytest_collectreport(report=rep) - class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ -def gettopdir(args): - """ return the top directory for the given paths. - if the common base dir resides in a python package - parent directory of the root package is returned. - """ - fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] - p = fsargs and fsargs[0] or None - for x in fsargs[1:]: - p = p.common(x) - assert p, "cannot determine common basedir of %s" %(fsargs,) - pkgdir = p.pypkgpath() - if pkgdir is None: - if p.check(file=1): - p = p.dirpath() - return p - else: - return pkgdir.dirpath() - -def decodearg(arg): - arg = str(arg) - return arg.split("::") - class HookProxy: - def __init__(self, node): - self.node = node + def __init__(self, fspath, config): + self.fspath = fspath + self.config = config def __getattr__(self, name): - hookmethod = getattr(self.node.config.hook, name) + hookmethod = getattr(self.config.hook, name) def call_matching_hooks(**kwargs): - plugins = self.node.config._getmatchingplugins(self.node.fspath) + plugins = self.config._getmatchingplugins(self.fspath) return hookmethod.pcall(plugins, **kwargs) return call_matching_hooks @@ -329,7 +179,7 @@ class Node(object): #: the file where this item is contained/collected from. self.fspath = getattr(parent, 'fspath', None) - self.ihook = HookProxy(self) + self.ihook = self.collection.gethookproxy(self.fspath) self.keywords = {self.name: True} Module = compatproperty("Module") @@ -339,14 +189,19 @@ class Node(object): Item = compatproperty("Item") def __repr__(self): - if getattr(self.config.option, 'debug', False): - return "<%s %r %0x>" %(self.__class__.__name__, - getattr(self, 'name', None), id(self)) - else: - return "<%s %r>" %(self.__class__.__name__, - getattr(self, 'name', None)) + return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) # methods for ordering nodes + @property + def nodeid(self): + try: + return self._nodeid + except AttributeError: + self._nodeid = x = self._makeid() + return x + + def _makeid(self): + return self.parent.nodeid + "::" + self.name def __eq__(self, other): if not isinstance(other, Node): @@ -447,7 +302,7 @@ class Collector(Node): def _memocollect(self): """ internal helper method to cache results of calling collect(). """ - return self._memoizedcall('_collected', self.collect) + return self._memoizedcall('_collected', lambda: list(self.collect())) def _prunetraceback(self, excinfo): if hasattr(self, 'fspath'): @@ -460,55 +315,177 @@ class Collector(Node): class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, collection=None): - fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, - parent, config, collection) + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + name = parent and fspath.relto(parent.fspath) or fspath.basename + super(FSCollector, self).__init__(name, parent, config, collection) self.fspath = fspath + def _makeid(self): + if self == self.collection: + return "." + relpath = self.collection.fspath.bestrelpath(self.fspath) + if os.sep != "/": + relpath = str(path).replace(os.sep, "/") + return relpath + class File(FSCollector): """ base class for collecting tests from a file. """ -class Directory(FSCollector): - def collect(self): - l = [] - for path in self.fspath.listdir(sort=True): - res = self.consider(path) - if res is not None: - if isinstance(res, (list, tuple)): - l.extend(res) - else: - l.append(res) - return l - - def consider(self, path): - if self.ihook.pytest_ignore_collect(path=path, config=self.config): - return - if path.check(file=1): - res = self.consider_file(path) - elif path.check(dir=1): - res = self.consider_dir(path) - else: - res = None - if isinstance(res, list): - # throw out identical results - l = [] - for x in res: - if x not in l: - assert x.parent == self, (x.parent, self) - assert x.fspath == path, (x.fspath, path) - l.append(x) - res = l - return res - - def consider_file(self, path): - return self.ihook.pytest_collect_file(path=path, parent=self) - - def consider_dir(self, path): - return self.ihook.pytest_collect_directory(path=path, parent=self) - class Item(Node): """ a basic test invocation item. Note that for a single function there might be multiple test invocation items. """ def reportinfo(self): return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + location = (str(location[0]), location[1], str(location[2])) + self._location = location + return location + +class Collection(FSCollector): + def __init__(self, config): + super(Collection, self).__init__(py.path.local(), parent=None, + config=config, collection=self) + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") + + def isinitpath(self, path): + return path in self._initialpaths + + def gethookproxy(self, fspath): + return HookProxy(fspath, self.config) + + def perform_collect(self, args=None, genitems=True): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + self._initialpaths = set() + self._initialargs = args + for arg in args: + parts = self._parsearg(arg) + self._initialpaths.add(parts[0]) + self.ihook.pytest_collectstart(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + for arg, exc in self._notfound: + line = "no name %r in any of %r" % (exc.args[1], exc.args[0]) + raise pytest.UsageError("not found: %s\n%s" %(arg, line)) + if not genitems: + return rep.result + else: + self.items = items = [] + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for arg in self._initialargs: + self.trace("processing arg", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + self.trace.root.indent -= 1 + break + self.trace.root.indent -= 1 + + def _collect(self, arg): + names = self._parsearg(arg) + path = names.pop(0) + if path.check(dir=1): + assert not names, "invalid arg %r" %(arg,) + for path in path.visit(rec=self._recurse, bf=True, sort=True): + for x in self._collectfile(path): + yield x + else: + assert path.check(file=1) + for x in self.matchnodes(self._collectfile(path), names): + yield x + + def _collectfile(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return + for pat in self._norecursepatterns: + if path.check(fnmatch=pat): + return False + ihook.pytest_collect_directory(path=path, parent=self) + return True + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + parts = str(arg).split("::") + path = self.fspath.join(parts[0], abs=True) + if not path.check(): + raise pytest.UsageError("file not found: %s" %(path,)) + parts[0] = path + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, pytest.collect.Item): + resultnodes.append(node) + continue + assert isinstance(node, pytest.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for x in rep.result: + if x.name == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + self.trace("genitems", node) + if isinstance(node, pytest.collect.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, pytest.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for subnode in rep.result: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) + --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -105,6 +105,7 @@ class HookRecorder: return l def contains(self, entries): + __tracebackhide__ = True from py.builtin import print_ i = 0 entries = list(entries) @@ -123,8 +124,7 @@ class HookRecorder: break print_("NONAMEMATCH", name, "with", call) else: - raise AssertionError("could not find %r in %r" %( - name, self.calls[i:])) + py.test.fail("could not find %r check %r" % (name, check)) def popcall(self, name): for i, call in enumerate(self.calls): @@ -278,7 +278,16 @@ class TmpTestdir: Collection = Collection def getnode(self, config, arg): collection = Collection(config) - return collection.getbyid(collection._normalizearg(arg))[0] + assert '::' not in str(arg) + p = py.path.local(arg) + x = collection.fspath.bestrelpath(p) + return collection.perform_collect([x], genitems=False)[0] + + def getpathnode(self, path): + config = self.parseconfig(path) + collection = Collection(config) + x = collection.fspath.bestrelpath(path) + return collection.perform_collect([x], genitems=False)[0] def genitems(self, colitems): collection = colitems[0].collection @@ -291,8 +300,9 @@ class TmpTestdir: #config = self.parseconfig(*args) config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - items = Collection(config).perform_collect() - return items, rec + collection = Collection(config) + collection.perform_collect() + return collection.items, rec def runitem(self, source): # used from runner functional tests @@ -469,11 +479,12 @@ class TmpTestdir: p = py.path.local.make_numbered_dir(prefix="runpytest-", keep=None, rootdir=self.tmpdir) args = ('--basetemp=%s' % p, ) + args - for x in args: - if '--confcutdir' in str(x): - break - else: - args = ('--confcutdir=.',) + args + #for x in args: + # if '--confcutdir' in str(x): + # break + #else: + # pass + # args = ('--confcutdir=.',) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args @@ -530,7 +541,7 @@ class ReportRecorder(object): """ return a testreport whose dotted import path matches """ l = [] for rep in self.getreports(names=names): - if not inamepart or inamepart in rep.nodenames: + if not inamepart or inamepart in rep.nodeid.split("::"): l.append(rep) if not l: raise ValueError("could not find test report matching %r: no test reports at all!" % @@ -616,6 +627,8 @@ class LineMatcher: raise ValueError("line %r not found in output" % line) def fnmatch_lines(self, lines2): + def show(arg1, arg2): + py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None @@ -626,17 +639,17 @@ class LineMatcher: while lines1: nextline = lines1.pop(0) if line == nextline: - print_("exact match:", repr(line)) + show("exact match:", repr(line)) break elif fnmatch(nextline, line): - print_("fnmatch:", repr(line)) - print_(" with:", repr(nextline)) + show("fnmatch:", repr(line)) + show(" with:", repr(nextline)) break else: if not nomatchprinted: - print_("nomatch:", repr(line)) + show("nomatch:", repr(line)) nomatchprinted = True - print_(" and:", repr(nextline)) + show(" and:", repr(nextline)) extralines.append(nextline) else: - assert line == nextline + py.test.fail("remains unmatched: %r, see stderr" % (line,)) --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -115,10 +115,10 @@ class TerminalReporter: def write_fspath_result(self, fspath, res): if fspath != self.currentfspath: self.currentfspath = fspath - fspath = self.curdir.bestrelpath(fspath) + #fspath = self.curdir.bestrelpath(fspath) self._tw.line() - relpath = self.curdir.bestrelpath(fspath) - self._tw.write(relpath + " ") + #relpath = self.curdir.bestrelpath(fspath) + self._tw.write(fspath + " ") self._tw.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): @@ -163,14 +163,15 @@ class TerminalReporter: def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) - def pytest_runtest_logstart(self, nodeid, location, fspath): + def pytest_runtest_logstart(self, nodeid, location): # ensure that the path is printed before the # 1st test of a module starts running + fspath = nodeid.split("::")[0] if self.showlongtestinfo: line = self._locationline(fspath, *location) self.write_ensure_prefix(line, "") elif self.showfspath: - self.write_fspath_result(py.path.local(fspath), "") + self.write_fspath_result(fspath, "") def pytest_runtest_logreport(self, report): rep = report --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -29,30 +29,12 @@ def pytest_sessionfinish(session, exitst session.exitstatus = 1 class NodeInfo: - def __init__(self, nodeid, nodenames, fspath, location): - self.nodeid = nodeid - self.nodenames = nodenames - self.fspath = fspath + def __init__(self, location): self.location = location -def getitemnodeinfo(item): - try: - return item._nodeinfo - except AttributeError: - location = item.reportinfo() - location = (str(location[0]), location[1], str(location[2])) - nodenames = tuple(item.listnames()) - nodeid = item.collection.getid(item) - fspath = item.fspath - item._nodeinfo = n = NodeInfo(nodeid, nodenames, fspath, location) - return n - def pytest_runtest_protocol(item): - nodeinfo = getitemnodeinfo(item) item.ihook.pytest_runtest_logstart( - nodeid=nodeinfo.nodeid, - location=nodeinfo.location, - fspath=str(item.fspath), + nodeid=item.nodeid, location=item.location, ) runtestprotocol(item) return True @@ -142,16 +124,18 @@ class BaseReport(object): failed = property(lambda x: x.outcome == "failed") skipped = property(lambda x: x.outcome == "skipped") + @property + def fspath(self): + return self.nodeid.split("::")[0] def pytest_runtest_makereport(item, call): - nodeinfo = getitemnodeinfo(item) when = call.when keywords = dict([(x,1) for x in item.keywords]) - excinfo = call.excinfo if not call.excinfo: outcome = "passed" longrepr = None else: + excinfo = call.excinfo if not isinstance(excinfo, py.code.ExceptionInfo): outcome = "failed" longrepr = excinfo @@ -164,25 +148,18 @@ def pytest_runtest_makereport(item, call longrepr = item.repr_failure(excinfo) else: # exception in setup or teardown longrepr = item._repr_failure_py(excinfo) - return TestReport(nodeinfo.nodeid, nodeinfo.nodenames, - nodeinfo.fspath, nodeinfo.location, + return TestReport(item.nodeid, item.location, keywords, outcome, longrepr, when) class TestReport(BaseReport): """ Basic test report object (also used for setup and teardown calls if they fail). """ - def __init__(self, nodeid, nodenames, fspath, location, + def __init__(self, nodeid, location, keywords, outcome, longrepr, when): #: normalized collection node id self.nodeid = nodeid - #: list of names indicating position in collection tree. - self.nodenames = nodenames - - #: the collected path of the file containing the test. - self.fspath = fspath # where the test was collected - #: a (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. @@ -212,39 +189,27 @@ class TeardownErrorReport(BaseReport): self.longrepr = longrepr def pytest_make_collect_report(collector): - result = excinfo = None - try: - result = collector._memocollect() - except KeyboardInterrupt: - raise - except: - excinfo = py.code.ExceptionInfo() - nodenames = tuple(collector.listnames()) - nodeid = collector.collection.getid(collector) - fspath = str(collector.fspath) + call = CallInfo(collector._memocollect, "memocollect") reason = longrepr = None - if not excinfo: + if not call.excinfo: outcome = "passed" else: - if excinfo.errisinstance(py.test.skip.Exception): + if call.excinfo.errisinstance(py.test.skip.Exception): outcome = "skipped" - reason = str(excinfo.value) - longrepr = collector._repr_failure_py(excinfo, "line") + reason = str(call.excinfo.value) + longrepr = collector._repr_failure_py(call.excinfo, "line") else: outcome = "failed" - errorinfo = collector.repr_failure(excinfo) + errorinfo = collector.repr_failure(call.excinfo) if not hasattr(errorinfo, "toterminal"): errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo - return CollectReport(nodenames, nodeid, fspath, - outcome, longrepr, result, reason) + return CollectReport(collector.nodeid, outcome, longrepr, + getattr(call, 'result', None), reason) class CollectReport(BaseReport): - def __init__(self, nodenames, nodeid, fspath, outcome, - longrepr, result, reason): - self.nodenames = nodenames + def __init__(self, nodeid, outcome, longrepr, result, reason): self.nodeid = nodeid - self.fspath = fspath self.outcome = outcome self.longrepr = longrepr self.result = result or [] @@ -255,7 +220,8 @@ class CollectReport(BaseReport): return (self.fspath, None, self.fspath) def __repr__(self): - return "" % (self.nodeid, self.outcome) + return "" % ( + self.nodeid, len(self.result), self.outcome) class CollectErrorRepr(TerminalRepr): def __init__(self, msg): --- a/tox.ini +++ b/tox.ini @@ -52,4 +52,5 @@ commands= [pytest] minversion=2.0 plugins=pytester -#addargs=-rf +addargs=-rfx +rsyncdirs=pytest testing --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -235,7 +235,7 @@ class TestFunction: param = 1 funcargs = {} id = "world" - collection = object() + collection = testdir.Collection(config) f5 = py.test.collect.Function(name="name", config=config, callspec=callspec1, callobj=isinstance, collection=collection) f5b = py.test.collect.Function(name="name", config=config, @@ -395,8 +395,8 @@ def test_generate_tests_only_done_in_sub def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) - trail = modcol.collection.getid(modcol) - newcol = modcol.collection.getbyid(trail)[0] + trail = modcol.nodeid + newcol = modcol.collection.perform_collect([trail], genitems=0)[0] assert modcol.name == newcol.name @@ -1058,8 +1058,7 @@ class TestReportInfo: """) item = testdir.getitem("def test_func(): pass") runner = item.config.pluginmanager.getplugin("runner") - nodeinfo = runner.getitemnodeinfo(item) - assert nodeinfo.location == ("ABCDE", 42, "custom") + assert item.location == ("ABCDE", 42, "custom") def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -76,14 +76,13 @@ class TestGeneralUsage: p1 = testdir.makepyfile("") p2 = testdir.makefile(".pyc", "123") result = testdir.runpytest(p1, p2) - assert result.ret != 0 + assert result.ret result.stderr.fnmatch_lines([ - "*ERROR: can't collect:*%s" %(p2.basename,) + "*ERROR: not found:*%s" %(p2.basename,) ]) - @py.test.mark.xfail def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest(""" @@ -97,7 +96,6 @@ class TestGeneralUsage: "*1 skip*" ]) - def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" import py @@ -145,7 +143,7 @@ class TestGeneralUsage: print (py.__file__) print (py.__path__) os.chdir(os.path.dirname(os.getcwd())) - print (py.log.Producer) + print (py.log) """)) result = testdir.runpython(p, prepend=False) assert not result.ret @@ -210,6 +208,27 @@ class TestGeneralUsage: res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) + + def test_direct_addressing_selects(self, testdir): + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall({'i': 1}, id="1") + metafunc.addcall({'i': 2}, id="2") + def test_func(i): + pass + """) + res = testdir.runpytest(p.basename + "::" + "test_func[1]") + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_direct_addressing_notfound(self, testdir): + p = testdir.makepyfile(""" + def test_func(): + pass + """) + res = testdir.runpytest(p.basename + "::" + "test_notfound") + assert res.ret + res.stderr.fnmatch_lines(["*ERROR*not found*"]) class TestInvocationVariants: def test_earlyinit(self, testdir): --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -17,13 +17,14 @@ class SessionTests: assert len(skipped) == 0 assert len(passed) == 1 assert len(failed) == 3 - assert failed[0].nodenames[-1] == "test_one_one" - assert failed[1].nodenames[-1] == "test_other" - assert failed[2].nodenames[-1] == "test_two" + end = lambda x: x.nodeid.split("::")[-1] + assert end(failed[0]) == "test_one_one" + assert end(failed[1]) == "test_other" + assert end(failed[2]) == "test_two" itemstarted = reprec.getcalls("pytest_itemcollected") assert len(itemstarted) == 4 colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 + 1 # XXX ExtraTopCollector + assert len(colstarted) == 1 + 1 col = colstarted[1].collector assert isinstance(col, py.test.collect.Module) @@ -186,7 +187,7 @@ class TestNewSession(SessionTests): started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 8 + 1 # XXX extra TopCollector + assert len(started) == 8 # XXX extra TopCollector colfail = [x for x in finished if x.failed] colskipped = [x for x in finished if x.skipped] assert len(colfail) == 1 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -48,11 +48,10 @@ def pytest_pyfunc_call(__multicall__, py def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename - if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.collection._argfspaths: - if ext == ".py": - return parent.ihook.pytest_pycollect_makemodule( - path=path, parent=parent) + if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or + parent.collection.isinitpath(path)): + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) def pytest_pycollect_makemodule(path, parent): return Module(path, parent) @@ -713,11 +712,13 @@ class FuncargRequest: def showfuncargs(config): from pytest.plugin.session import Collection collection = Collection(config) - firstid = collection._normalizearg(config.args[0]) - colitem = collection.getbyid(firstid)[0] + collection.perform_collect() + if collection.items: + plugins = getplugins(collection.items[0]) + else: + plugins = getplugins(collection) curdir = py.path.local() tw = py.io.TerminalWriter() - plugins = getplugins(colitem, withpy=True) verbose = config.getvalue("verbose") for plugin in plugins: available = [] --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -126,7 +126,7 @@ def pytest_runtest_protocol(item): """ pytest_runtest_protocol.firstresult = True -def pytest_runtest_logstart(nodeid, location, fspath): +def pytest_runtest_logstart(nodeid, location): """ signal the start of a test run. """ def pytest_runtest_setup(item): --- a/pytest/main.py +++ b/pytest/main.py @@ -67,7 +67,10 @@ class PluginManager(object): self._hints = [] self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): - self.trace.root.setwriter(sys.stderr.write) + err = sys.stderr + if hasattr(os, 'dup'): + err = py.io.dupfile(err) + self.trace.root.setwriter(err.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) if load: @@ -370,6 +373,7 @@ class HookCaller: self.hookrelay = hookrelay self.name = name self.firstresult = firstresult + self.trace = self.hookrelay.trace def __repr__(self): return "" %(self.name,) @@ -380,10 +384,15 @@ class HookCaller: return mc.execute() def pcall(self, plugins, **kwargs): - self.hookrelay.trace(self.name, kwargs) + self.trace(self.name, kwargs) + self.trace.root.indent += 1 methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() + res = mc.execute() + if res: + self.trace(res) + self.trace.root.indent -= 1 + return res _preinit = [PluginManager(load=True)] # triggers default plugin importing --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,283 @@ import py -from pytest.plugin.session import Collection, gettopdir +from pytest.plugin.session import Collection + +class TestCollector: + def test_collect_versus_item(self): + from pytest.collect import Collector, Item + assert not issubclass(Collector, Item) + assert not issubclass(Item, Collector) + + def test_compat_attributes(self, testdir, recwarn): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + recwarn.clear() + assert modcol.Module == py.test.collect.Module + recwarn.pop(DeprecationWarning) + assert modcol.Class == py.test.collect.Class + recwarn.pop(DeprecationWarning) + assert modcol.Item == py.test.collect.Item + recwarn.pop(DeprecationWarning) + assert modcol.File == py.test.collect.File + recwarn.pop(DeprecationWarning) + assert modcol.Function == py.test.collect.Function + recwarn.pop(DeprecationWarning) + + def test_check_equality(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn1, py.test.collect.Function) + fn2 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn2, py.test.collect.Function) + + assert fn1 == fn2 + assert fn1 != modcol + if py.std.sys.version_info < (3, 0): + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = testdir.collect_by_name(modcol, "test_fail") + assert isinstance(fn3, py.test.collect.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + + for fn in fn1,fn2,fn3: + assert fn != 3 + assert fn != modcol + assert fn != [1,2,3] + assert [1,2,3] != fn + assert modcol != fn + + def test_getparent(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass: + def test_foo(): + pass + """) + cls = testdir.collect_by_name(modcol, "TestClass") + fn = testdir.collect_by_name( + testdir.collect_by_name(cls, "()"), "test_foo") + + parent = fn.getparent(py.test.collect.Module) + assert parent is modcol + + parent = fn.getparent(py.test.collect.Function) + assert parent is fn + + parent = fn.getparent(py.test.collect.Class) + assert parent is cls + + + def test_getcustomfile_roundtrip(self, testdir): + hello = testdir.makefile(".xxx", hello="world") + testdir.makepyfile(conftest=""" + import py + class CustomFile(py.test.collect.File): + pass + def pytest_collect_file(path, parent): + if path.ext == ".xxx": + return CustomFile(path, parent=parent) + """) + node = testdir.getpathnode(hello) + assert isinstance(node, py.test.collect.File) + assert node.name == "hello.xxx" + nodes = node.collection.perform_collect([node.nodeid], genitems=False) + assert len(nodes) == 1 + assert isinstance(nodes[0], py.test.collect.File) + +class TestCollectFS: + def test_ignored_certain_directories(self, testdir): + tmpdir = testdir.tmpdir + tmpdir.ensure("_darcs", 'test_notfound.py') + tmpdir.ensure("CVS", 'test_notfound.py') + tmpdir.ensure("{arch}", 'test_notfound.py') + tmpdir.ensure(".whatever", 'test_notfound.py') + tmpdir.ensure(".bzr", 'test_notfound.py') + tmpdir.ensure("normal", 'test_found.py') + + result = testdir.runpytest("--collectonly") + s = result.stdout.str() + assert "test_notfound" not in s + assert "test_found" in s + + def test_custom_norecursedirs(self, testdir): + testdir.makeini(""" + [pytest] + norecursedirs = mydir xyz* + """) + tmpdir = testdir.tmpdir + tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") + tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") + tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + rec = testdir.inline_run("xyz123/test_2.py") + rec.assertoutcome(failed=1) + +class TestCollectPluginHookRelay: + def test_pytest_collect_file(self, testdir): + wascalled = [] + class Plugin: + def pytest_collect_file(self, path, parent): + wascalled.append(path) + testdir.makefile(".abc", "xyz") + testdir.pytestmain([testdir.tmpdir], plugins=[Plugin()]) + assert len(wascalled) == 1 + assert wascalled[0].ext == '.abc' + + def test_pytest_collect_directory(self, testdir): + wascalled = [] + class Plugin: + def pytest_collect_directory(self, path, parent): + wascalled.append(path.basename) + testdir.mkdir("hello") + testdir.mkdir("world") + testdir.pytestmain(testdir.tmpdir, plugins=[Plugin()]) + assert "hello" in wascalled + assert "world" in wascalled + +class TestPrunetraceback: + def test_collection_error(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + result = testdir.runpytest(p) + assert "__import__" not in result.stdout.str(), "too long traceback" + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*mport*not_exists*" + ]) + + def test_custom_repr_failure(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + testdir.makeconftest(""" + import py + def pytest_collect_file(path, parent): + return MyFile(path, parent) + class MyError(Exception): + pass + class MyFile(py.test.collect.File): + def collect(self): + raise MyError() + def repr_failure(self, excinfo): + if excinfo.errisinstance(MyError): + return "hello world" + return py.test.collect.File.repr_failure(self, excinfo) + """) + + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*hello world*", + ]) + + @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") + def test_collect_report_postprocessing(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + testdir.makeconftest(""" + import py + def pytest_make_collect_report(__multicall__): + rep = __multicall__.execute() + rep.headerlines += ["header1"] + return rep + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*header1*", + ]) + + +class TestCustomConftests: + def test_ignore_collect_path(self, testdir): + testdir.makeconftest(""" + def pytest_ignore_collect(path, config): + return path.basename.startswith("x") or \ + path.basename == "test_one.py" + """) + testdir.mkdir("xy123").ensure("test_hello.py").write( + "syntax error" + ) + testdir.makepyfile("def test_hello(): pass") + testdir.makepyfile(test_one="syntax error") + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed*"]) + + def test_collectignore_exclude_on_option(self, testdir): + testdir.makeconftest(""" + collect_ignore = ['hello', 'test_world.py'] + def pytest_addoption(parser): + parser.addoption("--XX", action="store_true", default=False) + def pytest_configure(config): + if config.getvalue("XX"): + collect_ignore[:] = [] + """) + testdir.mkdir("hello") + testdir.makepyfile(test_world="def test_hello(): pass") + result = testdir.runpytest() + assert result.ret == 0 + assert "passed" not in result.stdout.str() + result = testdir.runpytest("--XX") + assert result.ret == 0 + assert "passed" in result.stdout.str() + + def test_pytest_fs_collect_hooks_are_seen(self, testdir): + conf = testdir.makeconftest(""" + import py + class MyModule(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule(path, parent) + """) + sub = testdir.mkdir("sub") + p = testdir.makepyfile("def test_x(): pass") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyModule*", + "*test_x*" + ]) + + def test_pytest_collect_file_from_sister_dir(self, testdir): + sub1 = testdir.mkpydir("sub1") + sub2 = testdir.mkpydir("sub2") + conf1 = testdir.makeconftest(""" + import py + class MyModule1(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule1(path, parent) + """) + conf1.move(sub1.join(conf1.basename)) + conf2 = testdir.makeconftest(""" + import py + class MyModule2(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule2(path, parent) + """) + conf2.move(sub2.join(conf2.basename)) + p = testdir.makepyfile("def test_x(): pass") + p.copy(sub1.join(p.basename)) + p.copy(sub2.join(p.basename)) + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyModule1*", + "*MyModule2*", + "*test_x*" + ]) class TestCollection: def test_parsearg(self, testdir): @@ -13,16 +290,15 @@ class TestCollection: subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Collection(config=config) - assert rcol.topdir == testdir.tmpdir + assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) - assert parts[0] == "sub" - assert parts[1] == p.basename + + assert parts[0] == target + assert len(parts) == 1 + parts = rcol._parsearg(p.basename + "::test_func") + assert parts[0] == target + assert parts[1] == "test_func" assert len(parts) == 2 - parts = rcol._parsearg(p.basename + "::test_func") - assert parts[0] == "sub" - assert parts[1] == p.basename - assert parts[2] == "test_func" - assert len(parts) == 3 def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -30,14 +306,14 @@ class TestCollection: config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Collection(config) - assert topdir == rcol.topdir - hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() - assert len(items) == 1 - root = items[0].listchain()[0] - root_id = rcol.getid(root) - root2 = rcol.getbyid(root_id)[0] - assert root2.fspath == root.fspath + assert topdir == rcol.fspath + rootid = rcol.nodeid + #root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] + #assert root2 == rcol, rootid + colitems = rcol.perform_collect([rcol.nodeid], genitems=False) + assert len(colitems) == 1 + assert colitems[0].fspath == p + def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -45,13 +321,14 @@ class TestCollection: config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Collection(config) - assert topdir == rcol.topdir + assert topdir == rcol.fspath hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 item = items[0] assert item.name == "test_func" - newid = rcol.getid(item) + newid = item.nodeid assert newid == id py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ @@ -60,8 +337,8 @@ class TestCollection: ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == p"), - ("pytest_collectreport", "report.fspath == topdir") + ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + ("pytest_collectreport", "report.nodeid == '.'") ]) def test_collect_protocol_method(self, testdir): @@ -70,19 +347,19 @@ class TestCollection: def test_method(self): pass """) - normid = p.basename + "::TestClass::test_method" + normid = p.basename + "::TestClass::()::test_method" for id in [p.basename, p.basename + "::TestClass", p.basename + "::TestClass::()", - p.basename + "::TestClass::()::test_method", normid, ]: config = testdir.parseconfig(id) rcol = Collection(config=config) - nodes = rcol.perform_collect() - assert len(nodes) == 1 - assert nodes[0].name == "test_method" - newid = rcol.getid(nodes[0]) + rcol.perform_collect() + items = rcol.items + assert len(items) == 1 + assert items[0].name == "test_method" + newid = items[0].nodeid assert newid == normid def test_collect_custom_nodes_multi_id(self, testdir): @@ -104,20 +381,21 @@ class TestCollection: config = testdir.parseconfig(id) rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items py.std.pprint.pprint(hookrec.hookrecorder.calls) assert len(items) == 2 hookrec.hookrecorder.contains([ ("pytest_collectstart", - "collector.fspath == collector.collection.topdir"), + "collector.fspath == collector.collection.fspath"), ("pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'"), ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == p"), - ("pytest_collectreport", - "report.fspath == %r" % str(rcol.topdir)), + ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + #("pytest_collectreport", + # "report.fspath == %r" % str(rcol.fspath)), ]) def test_collect_subdir_event_ordering(self, testdir): @@ -128,134 +406,87 @@ class TestCollection: config = testdir.parseconfig() rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == test_aaa"), - ("pytest_collectreport", "report.fspath == aaa"), + ("pytest_collectreport", + "report.nodeid.startswith('aaa/test_aaa.py')"), ]) def test_collect_two_commandline_args(self, testdir): p = testdir.makepyfile("def test_func(): pass") aaa = testdir.mkpydir("aaa") bbb = testdir.mkpydir("bbb") - p.copy(aaa.join("test_aaa.py")) - p.move(bbb.join("test_bbb.py")) + test_aaa = aaa.join("test_aaa.py") + p.copy(test_aaa) + test_bbb = bbb.join("test_bbb.py") + p.move(test_bbb) id = "." config = testdir.parseconfig(id) rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 2 py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == aaa"), - ("pytest_collectstart", "collector.fspath == bbb"), + ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"), + ("pytest_collectstart", "collector.fspath == test_bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == bbb"), + ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"), ]) def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") config = testdir.parseconfig() rcol = Collection(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 item, = items - id = rcol.getid(item) newcol = Collection(config) - item2, = newcol.getbyid(id) + item2, = newcol.perform_collect([item.nodeid], genitems=False) assert item2.name == item.name assert item2.fspath == item.fspath - item2b, = newcol.getbyid(id) - assert item2b is item2 - -class Test_gettopdir: - def test_gettopdir(self, testdir): - tmp = testdir.tmpdir - assert gettopdir([tmp]) == tmp - topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) - assert topdir == tmp - somefile = tmp.ensure("somefile.py") - assert gettopdir([somefile]) == tmp - - def test_gettopdir_pypkg(self, testdir): - tmp = testdir.tmpdir - a = tmp.ensure('a', dir=1) - b = tmp.ensure('a', 'b', '__init__.py') - c = tmp.ensure('a', 'b', 'c.py') - Z = tmp.ensure('Z', dir=1) - assert gettopdir([c]) == a - assert gettopdir([c, Z]) == tmp - assert gettopdir(["%s::xyc" % c]) == a - assert gettopdir(["%s::xyc::abc" % c]) == a - assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp + item2b, = newcol.perform_collect([item.nodeid], genitems=False) + assert item2b == item2 def getargnode(collection, arg): - return collection.getbyid(collection._normalizearg(str(arg)))[0] + argpath = arg.relto(collection.fspath) + return collection.perform_collect([argpath], genitems=False)[0] class Test_getinitialnodes: - def test_onedir(self, testdir): - config = testdir.reparseconfig([testdir.tmpdir]) - c = Collection(config) - col = getargnode(c, testdir.tmpdir) - assert isinstance(col, py.test.collect.Directory) - for col in col.listchain(): - assert col.config is config - t2 = getargnode(c, testdir.tmpdir) - assert col == t2 - - def test_curdir_and_subdir(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([tmpdir, a]) - c = Collection(config) - - col1 = getargnode(c, tmpdir) - col2 = getargnode(c, a) - assert col1.name == tmpdir.basename - assert col2.name == 'a' - for col in (col1, col2): - for subcol in col.listchain(): - assert col.config is config - def test_global_file(self, testdir, tmpdir): x = tmpdir.ensure("x.py") config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) + col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) assert col.name == 'x.py' - assert col.parent.name == tmpdir.basename + assert col.parent.name == testdir.tmpdir.basename assert col.parent.parent is None for col in col.listchain(): assert col.config is config - def test_global_dir(self, testdir, tmpdir): - x = tmpdir.ensure("a", dir=1) + def test_pkgfile(self, testdir): + testdir.chdir() + tmpdir = testdir.tmpdir + subdir = tmpdir.join("subdir") + x = subdir.ensure("x.py") + subdir.ensure("__init__.py") config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) - assert isinstance(col, py.test.collect.Directory) - print(col.listchain()) - assert col.name == 'a' - assert col.parent is None - assert col.config is config - - def test_pkgfile(self, testdir, tmpdir): - tmpdir = tmpdir.join("subdir") - x = tmpdir.ensure("x.py") - tmpdir.ensure("__init__.py") - config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) + col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == x.dirpath().basename - assert col.parent.parent.parent is None + print col.obj + print col.listchain() + assert col.name == 'subdir/x.py' + assert col.parent.parent is None for col in col.listchain(): assert col.config is config From commits-noreply at bitbucket.org Sat Nov 6 09:57:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:05 -0500 (CDT) Subject: [py-svn] pytest-xdist commit 922c00c8c2fa: adapt to pytest changes, add looponfailingdirs ini-option Message-ID: <20101106085705.20D6A241420@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289033880 -3600 # Node ID 922c00c8c2fa0bebe693cab54301a01c43159e55 # Parent 724fe27731c76cf8a91472cf8cdacfb336365765 adapt to pytest changes, add looponfailingdirs ini-option --- a/tox.ini +++ b/tox.ini @@ -16,5 +16,5 @@ deps= pytest pypi pexpect -[pytest] -addopts = -rf +#[pytest] +#addopts = -rf --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -301,7 +301,7 @@ class DSession: runner = self.config.pluginmanager.getplugin("runner") fspath = nodeid.split("::")[0] msg = "Slave %r crashed while running %r" %(slave.gateway.id, nodeid) - rep = runner.TestReport(nodeid, (), fspath, (fspath, None, fspath), (), + rep = runner.TestReport(nodeid, (fspath, None, fspath), (), "failed", msg, "???") enrich_report_with_platform_data(rep, slave) self.config.hook.pytest_runtest_logreport(report=rep) @@ -350,6 +350,6 @@ def enrich_report_with_platform_data(rep ver = "%s.%s.%s" % d['version_info'][:3] infoline = "[%s] %s -- Python %s %s" % ( d['id'], d['sysplatform'], ver, d['executable']) - # XXX more structured longrepr? + # XXX more structured longrepr? rep.longrepr = infoline + "\n\n" + str(rep.longrepr) --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a4' +__version__ = '1.5a5' --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -11,7 +11,7 @@ def pytest_funcarg__hookrecorder(request def pytest_funcarg__hook(request): from xdist import newhooks - from pytest._core import HookRelay, PluginManager + from pytest.main import HookRelay, PluginManager from pytest import hookspec return HookRelay([hookspec, newhooks], PluginManager()) --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -156,6 +156,8 @@ class TestSlaveInteractor: assert not ev.kwargs ev = slave.popevent() assert ev.name == "collectreport" + ev = slave.popevent() + assert ev.name == "collectreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.skipped ev = slave.popevent("collectionfinish") @@ -168,6 +170,8 @@ class TestSlaveInteractor: assert not ev.kwargs ev = slave.popevent() assert ev.name == "collectreport" + ev = slave.popevent() + assert ev.name == "collectreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.failed ev = slave.popevent("collectionfinish") --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -142,7 +142,7 @@ where the configuration file was found. """ import sys -import py +import py, pytest def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") @@ -178,6 +178,8 @@ def pytest_addoption(parser): ' remote distributed testing.', type="pathlist") parser.addini('rsyncignore', 'list of (relative) paths to be ignored ' 'for rsyncing.', type="pathlist") + parser.addini("looponfailroots", type="pathlist", + help="directories to check for changes", default=[py.path.local()]) # ------------------------------------------------------------------------- # distributed testing hooks @@ -218,10 +220,10 @@ def check_options(config): usepdb = config.option.usepdb # a core option if val("looponfail"): if usepdb: - raise config.Error("--pdb incompatible with --looponfail.") + raise pytest.UsageError("--pdb incompatible with --looponfail.") elif val("dist") != "no": if usepdb: - raise config.Error("--pdb incompatible with distributing tests.") + raise pytest.UsageError("--pdb incompatible with distributing tests.") def pytest_runtest_protocol(item): --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -7,21 +7,22 @@ the controlling process which should best never happen. """ -import py +import py, pytest import sys import execnet def looponfail_main(config): remotecontrol = RemoteControl(config) - # XXX better configure rootdir - gettopdir = config.pluginmanager.getplugin("session").gettopdir - rootdirs = [gettopdir(config.args)] + rootdirs = config.getini("looponfailroots") statrecorder = StatRecorder(rootdirs) try: while 1: remotecontrol.loop_once() if not remotecontrol.failures and remotecontrol.wasfailing: continue # the last failures passed, let's immediately rerun all + repr_pytest_looponfailinfo( + failreports=remotecontrol.failures, + rootdirs=rootdirs) statrecorder.waitonchange(checkinterval=2.0) except KeyboardInterrupt: print() @@ -29,7 +30,6 @@ def looponfail_main(config): class RemoteControl(object): def __init__(self, config): self.config = config - self.remote_topdir = None self.failures = [] def trace(self, *args): @@ -70,8 +70,8 @@ class RemoteControl(object): def runsession(self): try: - self.trace("sending", (self.remote_topdir, self.failures)) - self.channel.send((self.remote_topdir, self.failures)) + self.trace("sending", self.failures) + self.channel.send(self.failures) try: return self.channel.receive() except self.channel.RemoteError: @@ -85,15 +85,11 @@ class RemoteControl(object): self.setup() self.wasfailing = self.failures and len(self.failures) result = self.runsession() - topdir, failures, reports, collection_failed = result + failures, reports, collection_failed = result if collection_failed: reports = ["Collection failed, keeping previous failure set"] else: - self.remote_topdir, self.failures = topdir, failures - - repr_pytest_looponfailinfo( - failreports=reports, - rootdirs=[self.remote_topdir],) + self.failures = failures def repr_pytest_looponfailinfo(failreports, rootdirs): tr = py.io.TerminalWriter() @@ -147,23 +143,29 @@ class SlaveFailSession: def pytest_collection(self, session): self.session = session - self.collection = session.collection - self.topdir, self.trails = self.current_command - if self.topdir and self.trails: - self.topdir = py.path.local(self.topdir) - self.collection.topdir = self.topdir + self.collection = collection = session.collection + self.trails = self.current_command + hook = self.collection.ihook + try: + items = collection.perform_collect(self.trails or None) + except pytest.UsageError: + items = collection.perform_collect(None) + hook.pytest_collection_modifyitems(config=session.config, items=items) + hook.pytest_collection_finish(collection=collection) + return True + + if self.trails: col = self.collection items = [] for trail in self.trails: - names = col._parsearg(trail, base=self.topdir) + names = col._parsearg(trail) try: for node in col.matchnodes([col._topcollector], names): items.extend(col.genitems(node)) - except self.config.Error: + except pytest.UsageError: pass # ignore collect errors / vanished tests self.collection.items = items return True - self.topdir = session.collection.topdir def pytest_runtest_logreport(self, report): if report.failed: @@ -189,8 +191,7 @@ class SlaveFailSession: loc = rep.longrepr loc = str(getattr(loc, 'reprcrash', loc)) failreports.append(loc) - topdir = str(self.topdir) - self.channel.send((topdir, trails, failreports, self.collection_failed)) + self.channel.send((trails, failreports, self.collection_failed)) class StatRecorder: def __init__(self, rootdirlist): --- a/xdist/remote.py +++ b/xdist/remote.py @@ -53,8 +53,8 @@ class SlaveInteractor: if name == "runtests": ids = kwargs['ids'] for nodeid in ids: - for item in self.collection.getbyid(nodeid): - self.config.hook.pytest_runtest_protocol(item=item) + item = self._id2item[nodeid] + self.config.hook.pytest_runtest_protocol(item=item) elif name == "runtests_all": for item in self.collection.items: self.config.hook.pytest_runtest_protocol(item=item) @@ -63,9 +63,13 @@ class SlaveInteractor: return True def pytest_collection_finish(self, collection): - ids = [collection.getid(item) for item in collection.items] + self._id2item = {} + ids = [] + for item in collection.items: + self._id2item[item.nodeid] = item + ids.append(item.nodeid) self.sendevent("collectionfinish", - topdir=str(collection.topdir), + topdir=str(collection.fspath), ids=ids) #def pytest_runtest_logstart(self, nodeid, location, fspath): --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a4', + version='1.5a5', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit e4e9f0f7c4dd: document and refine py.test.fail helper and strike superflous ExceptionFailure class Message-ID: <20101106085701.09AAD243F48@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID e4e9f0f7c4dd2e8b39991b38db44f676802f1b99 # Parent 567a0d7b8fa2be3820a64b6a4d5ecb326b383f7a document and refine py.test.fail helper and strike superflous ExceptionFailure class refine builtin organisation and start a new doc --- /dev/null +++ b/doc/example/layout1/setup.cfg @@ -0,0 +1,4 @@ +[pytest] +testfilepatterns = + ${topdir}/tests/unit/test_${basename} + ${topdir}/tests/functional/*.py --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -1,4 +1,4 @@ -import py, sys +import pytest, py, sys from pytest.plugin import python as funcargs class TestModule: @@ -1118,3 +1118,60 @@ def test_show_funcarg(testdir): "*temporary directory*", ] ) + +class TestRaises: + def test_raises(self): + source = "int('qwe')" + excinfo = py.test.raises(ValueError, source) + code = excinfo.traceback[-1].frame.code + s = str(code.fullsource) + assert s == source + + def test_raises_exec(self): + py.test.raises(ValueError, "a,x = []") + + def test_raises_syntax_error(self): + py.test.raises(SyntaxError, "qwe qwe qwe") + + def test_raises_function(self): + py.test.raises(ValueError, int, 'hello') + + def test_raises_callable_no_exception(self): + class A: + def __call__(self): + pass + try: + py.test.raises(ValueError, A()) + except py.test.raises.Exception: + pass + + @py.test.mark.skipif('sys.version < "2.5"') + def test_raises_as_contextmanager(self, testdir): + testdir.makepyfile(""" + from __future__ import with_statement + import py + + def test_simple(): + with py.test.raises(ZeroDivisionError) as excinfo: + assert isinstance(excinfo, py.code.ExceptionInfo) + 1/0 + print (excinfo) + assert excinfo.type == ZeroDivisionError + + def test_noraise(): + with py.test.raises(py.test.raises.Exception): + with py.test.raises(ValueError): + int() + + def test_raise_wrong_exception_passes_by(): + with py.test.raises(ZeroDivisionError): + with py.test.raises(ValueError): + 1/0 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*3 passed*', + ]) + + + --- a/testing/plugin/test_skipping.py +++ b/testing/plugin/test_skipping.py @@ -417,7 +417,7 @@ def test_skipped_reasons_functional(test result.stdout.fnmatch_lines([ "*test_two.py S", "*test_one.py ss", - "*SKIP*3*conftest.py:3: 'test'", + "*SKIP*3*conftest.py:3: test", ]) assert result.ret == 0 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -7,7 +7,6 @@ import sys import pytest from py._code.code import TerminalRepr -import pytest cutdir = py.path.local(pytest.__file__).dirpath() @@ -22,11 +21,16 @@ def pytest_cmdline_main(config): showfuncargs(config) return 0 -def pytest_namespace(): - return {'collect': { +def pytest_namespace(__multicall__): + __multicall__.execute() + raises.Exception = pytest.fail.Exception + return { + 'raises' : raises, + 'collect': { 'Module': Module, 'Class': Class, 'Instance': Instance, 'Function': Function, 'Generator': Generator, - '_fillfuncargs': fillfuncargs}} + '_fillfuncargs': fillfuncargs} + } def pytest_funcarg__pytestconfig(request): """ the pytest config object with access to command line opts.""" @@ -300,17 +304,17 @@ class FunctionMixin(PyobjMixin): if teardown_func_or_meth is not None: teardown_func_or_meth(self.obj) - def _prunetraceback(self, traceback): + def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: code = py.code.Code(self.obj) path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) if ntraceback == traceback: ntraceback = ntraceback.cut(path=path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=cutdir) - traceback = ntraceback.filter() - return traceback + excinfo.traceback = ntraceback.filter() def _repr_failure_py(self, excinfo, style="long"): if excinfo.errisinstance(FuncargRequest.LookupError): @@ -746,3 +750,71 @@ def getlocation(function, curdir): if fn.relto(curdir): fn = fn.relto(curdir) return "%s:%d" %(fn, lineno+1) + +# builtin pytest.raises helper + +def raises(ExpectedException, *args, **kwargs): + """ assert that a code block/function call raises an exception. + + If using Python 2.5 or above, you may use this function as a + context manager:: + + >>> with raises(ZeroDivisionError): + ... 1/0 + + Or you can one of two forms: + + if args[0] is callable: raise AssertionError if calling it with + the remaining arguments does not raise the expected exception. + if args[0] is a string: raise AssertionError if executing the + the string in the calling scope does not raise expected exception. + examples: + >>> x = 5 + >>> raises(TypeError, lambda x: x + 'hello', x=x) + >>> raises(TypeError, "x + 'hello'") + """ + __tracebackhide__ = True + + if not args: + return RaisesContext(ExpectedException) + elif isinstance(args[0], str): + code, = args + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + #print "raises frame scope: %r" % frame.f_locals + try: + code = py.code.Source(code).compile() + py.builtin.exec_(code, frame.f_globals, loc) + # XXX didn'T mean f_globals == f_locals something special? + # this is destroyed here ... + except ExpectedException: + return py.code.ExceptionInfo() + else: + func = args[0] + try: + func(*args[1:], **kwargs) + except ExpectedException: + return py.code.ExceptionInfo() + k = ", ".join(["%s=%r" % x for x in kwargs.items()]) + if k: + k = ', ' + k + expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) + pytest.fail("DID NOT RAISE") + +class RaisesContext(object): + def __init__(self, ExpectedException): + self.ExpectedException = ExpectedException + self.excinfo = None + + def __enter__(self): + self.excinfo = object.__new__(py.code.ExceptionInfo) + return self.excinfo + + def __exit__(self, *tp): + __tracebackhide__ = True + if tp[0] is None: + pytest.fail("DID NOT RAISE") + self.excinfo.__init__(tp) + return issubclass(self.excinfo.type, self.ExpectedException) --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -1,5 +1,5 @@ """ -py.test / pytest API for unit and functional testing with Python. +unit and functional testing with Python. see http://pytest.org for documentation and details --- /dev/null +++ b/doc/example/builtin.txt @@ -0,0 +1,36 @@ + +writing well integrated assertion helpers +======================================================== + +If you have a test helper function called from a test you can +use the ``pytest.fail``_ builtin to cleanly fail a test with a message. +The test support function will never itself show up in the traceback. +Example:: + + # content of test_checkconfig.py + import pytest + def checkconfig(x): + __tracebackhide__ = True + if not hasattr(x, "config"): + pytest.fail("not configured: %s" %(x,)) + + def test_something(): + checkconfig(42) + +The ``__tracebackhide__`` setting influences py.test showing +of tracebacks: the ``checkconfig`` function will not be shown +unless the ``--fulltrace`` command line option is specified. +Let's run our little function:: + + $ py.test -q + F + ================================= FAILURES ================================= + ______________________________ test_something ______________________________ + + def test_something(): + > checkconfig(42) + E Failed: not configured: 42 + + test_checkconfig.py:8: Failed + 1 failed in 0.02 seconds + --- /dev/null +++ b/doc/builtin.txt @@ -0,0 +1,70 @@ + +pytest builtin helpers +================================================ + + +builtin function arguments +----------------------------------------------------- + +You can ask for available builtin or project-custom +:ref:`function arguments` by typing:: + + $ py.test --funcargs + pytestconfig + the pytest config object with access to command line opts. + capsys + captures writes to sys.stdout/sys.stderr and makes + them available successively via a ``capsys.readouterr()`` method + which returns a ``(out, err)`` tuple of captured snapshot strings. + + capfd + captures writes to file descriptors 1 and 2 and makes + snapshotted ``(out, err)`` string tuples available + via the ``capsys.readouterr()`` method. If the underlying + platform does not have ``os.dup`` (e.g. Jython) tests using + this funcarg will automatically skip. + + tmpdir + return a temporary directory path object + unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a `py.path.local`_ + path object. + + monkeypatch + The returned ``monkeypatch`` funcarg provides these + helper methods to modify objects, dictionaries or os.environ:: + + monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.delattr(obj, name, raising=True) + monkeypatch.setitem(mapping, name, value) + monkeypatch.delitem(obj, name, raising=True) + monkeypatch.setenv(name, value, prepend=False) + monkeypatch.delenv(name, value, raising=True) + monkeypatch.syspath_prepend(path) + + All modifications will be undone when the requesting + test function finished its execution. The ``raising`` + parameter determines if a KeyError or AttributeError + will be raised if the set/deletion operation has no target. + + recwarn + Return a WarningsRecorder instance that provides these methods: + + * ``pop(category=None)``: return last warning matching the category. + * ``clear()``: clear list of warnings + + +builtin py.test.* helpers +----------------------------------------------------- + +You can always use an interactive Python prompt and type:: + + import pytest + help(pytest) + +to get an overview on available globally available helpers. + +.. automodule:: pytest + :members: + --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -6,9 +6,10 @@ py.test reference documentation .. toctree:: :maxdepth: 2 - + + builtin.txt customize.txt - assert.txt + assert.txt funcargs.txt xunit_setup.txt capture.txt @@ -16,7 +17,7 @@ py.test reference documentation tmpdir.txt skipping.txt mark.txt - recwarn.txt + recwarn.txt unittest.txt doctest.txt - + --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -7,6 +7,7 @@ import py import pytest import os, sys +tracebackcutdir = py.path.local(pytest.__file__).dirpath() # exitcodes for the command line EXIT_OK = 0 @@ -403,14 +404,14 @@ class Node(object): current = current.parent return current - def _prunetraceback(self, traceback): - return traceback + def _prunetraceback(self, excinfo): + pass def _repr_failure_py(self, excinfo, style=None): if self.config.option.fulltrace: style="long" else: - excinfo.traceback = self._prunetraceback(excinfo.traceback) + self._prunetraceback(excinfo) # XXX should excinfo.getrepr record all data and toterminal() # process it? if style is None: @@ -448,14 +449,14 @@ class Collector(Node): """ internal helper method to cache results of calling collect(). """ return self._memoizedcall('_collected', self.collect) - def _prunetraceback(self, traceback): + def _prunetraceback(self, excinfo): if hasattr(self, 'fspath'): path = self.fspath + traceback = excinfo.traceback ntraceback = traceback.cut(path=self.fspath) if ntraceback == traceback: - ntraceback = ntraceback.cut(excludepath=py._pydir) - traceback = ntraceback.filter() - return traceback + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + excinfo.traceback = ntraceback.filter() class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, collection=None): --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -195,7 +195,7 @@ class TestCollectonly: assert len(cols) == 0 linecomp.assert_contains_lines(""" - !!! Skipped: 'nomod' !!! + !!! Skipped: nomod !!! """) def test_collectonly_failed_module(self, testdir, linecomp): --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -7,11 +7,9 @@ from py._code.code import TerminalRepr def pytest_namespace(): return { - 'raises' : raises, + 'fail' : fail, 'skip' : skip, 'importorskip' : importorskip, - 'fail' : fail, - 'xfail' : xfail, 'exit' : exit, } @@ -337,13 +335,12 @@ class OutcomeException(Exception): """ OutcomeException and its subclass instances indicate and contain info about test and collection outcomes. """ - def __init__(self, msg=None, excinfo=None): + def __init__(self, msg=None): self.msg = msg - self.excinfo = excinfo def __repr__(self): if self.msg: - return repr(self.msg) + return str(self.msg) return "<%s instance>" %(self.__class__.__name__,) __str__ = __repr__ @@ -356,19 +353,8 @@ class Failed(OutcomeException): """ raised from an explicit call to py.test.fail() """ __module__ = 'builtins' -class XFailed(OutcomeException): - """ raised from an explicit call to py.test.xfail() """ - __module__ = 'builtins' - -class ExceptionFailure(Failed): - """ raised by py.test.raises on an exception-assertion mismatch. """ - def __init__(self, expr, expected, msg=None, excinfo=None): - Failed.__init__(self, msg=msg, excinfo=excinfo) - self.expr = expr - self.expected = expected - class Exit(KeyboardInterrupt): - """ raised by py.test.exit for immediate program exits without tracebacks and reporter/summary. """ + """ raised for immediate program exits (no tracebacks/summaries)""" def __init__(self, msg="unknown reason"): self.msg = msg KeyboardInterrupt.__init__(self, msg) @@ -384,103 +370,20 @@ exit.Exception = Exit def skip(msg=""): """ skip an executing test with the given message. Note: it's usually - better use the py.test.mark.skipif marker to declare a test to be + better to use the py.test.mark.skipif marker to declare a test to be skipped under certain conditions like mismatching platforms or dependencies. See the pytest_skipping plugin for details. """ __tracebackhide__ = True raise Skipped(msg=msg) - skip.Exception = Skipped def fail(msg=""): """ explicitely fail an currently-executing test with the given Message. """ __tracebackhide__ = True raise Failed(msg=msg) - fail.Exception = Failed -def xfail(reason=""): - """ xfail an executing test or setup functions, taking an optional - reason string. - """ - __tracebackhide__ = True - raise XFailed(reason) -xfail.Exception = XFailed - -def raises(ExpectedException, *args, **kwargs): - """ assert that a code block/function call raises an exception. - - If using Python 2.5 or above, you may use this function as a - context manager:: - - >>> with raises(ZeroDivisionError): - ... 1/0 - - Or you can one of two forms: - - if args[0] is callable: raise AssertionError if calling it with - the remaining arguments does not raise the expected exception. - if args[0] is a string: raise AssertionError if executing the - the string in the calling scope does not raise expected exception. - examples: - >>> x = 5 - >>> raises(TypeError, lambda x: x + 'hello', x=x) - >>> raises(TypeError, "x + 'hello'") - """ - __tracebackhide__ = True - - if not args: - return RaisesContext(ExpectedException) - elif isinstance(args[0], str): - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - #print "raises frame scope: %r" % frame.f_locals - try: - code = py.code.Source(code).compile() - py.builtin.exec_(code, frame.f_globals, loc) - # XXX didn'T mean f_globals == f_locals something special? - # this is destroyed here ... - except ExpectedException: - return py.code.ExceptionInfo() - else: - func = args[0] - try: - func(*args[1:], **kwargs) - except ExpectedException: - return py.code.ExceptionInfo() - k = ", ".join(["%s=%r" % x for x in kwargs.items()]) - if k: - k = ', ' + k - expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) - raise ExceptionFailure(msg="DID NOT RAISE", - expr=args, expected=ExpectedException) - - -class RaisesContext(object): - - def __init__(self, ExpectedException): - self.ExpectedException = ExpectedException - self.excinfo = None - - def __enter__(self): - self.excinfo = object.__new__(py.code.ExceptionInfo) - return self.excinfo - - def __exit__(self, *tp): - __tracebackhide__ = True - if tp[0] is None: - raise ExceptionFailure(msg="DID NOT RAISE", - expr=(), - expected=self.ExpectedException) - self.excinfo.__init__(tp) - return issubclass(self.excinfo.type, self.ExpectedException) - - -raises.Exception = ExceptionFailure def importorskip(modname, minversion=None): """ return imported module if it has a higher __version__ than the @@ -503,5 +406,3 @@ def importorskip(modname, minversion=Non py.test.skip("module %r has __version__ %r, required is: %r" %( modname, verattr, minversion)) return mod - - --- a/doc/example/index.txt +++ b/doc/example/index.txt @@ -7,6 +7,7 @@ Usages and Examples .. toctree:: :maxdepth: 2 + builtin.txt pythoncollection.txt controlskip.txt mysetup.txt --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -319,61 +319,6 @@ def test_runtest_in_module_ordering(test "*2 passed*" ]) -class TestRaises: - def test_raises(self): - source = "int('qwe')" - excinfo = py.test.raises(ValueError, source) - code = excinfo.traceback[-1].frame.code - s = str(code.fullsource) - assert s == source - - def test_raises_exec(self): - py.test.raises(ValueError, "a,x = []") - - def test_raises_syntax_error(self): - py.test.raises(SyntaxError, "qwe qwe qwe") - - def test_raises_function(self): - py.test.raises(ValueError, int, 'hello') - - def test_raises_callable_no_exception(self): - class A: - def __call__(self): - pass - try: - py.test.raises(ValueError, A()) - except py.test.raises.Exception: - pass - - @py.test.mark.skipif('sys.version < "2.5"') - def test_raises_as_contextmanager(self, testdir): - testdir.makepyfile(""" - from __future__ import with_statement - import py - - def test_simple(): - with py.test.raises(ZeroDivisionError) as excinfo: - assert isinstance(excinfo, py.code.ExceptionInfo) - 1/0 - print (excinfo) - assert excinfo.type == ZeroDivisionError - - def test_noraise(): - with py.test.raises(py.test.raises.Exception): - with py.test.raises(ValueError): - int() - - def test_raise_wrong_exception_passes_by(): - with py.test.raises(ZeroDivisionError): - with py.test.raises(ValueError): - 1/0 - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '*3 passed*', - ]) - - def test_pytest_exit(): try: --- a/pytest/plugin/skipping.py +++ b/pytest/plugin/skipping.py @@ -1,154 +1,8 @@ """ -advanced skipping for python test functions, classes or modules. - -With this plugin you can mark test functions for conditional skipping -or as "xfail", expected-to-fail. Skipping a test will avoid running it -while xfail-marked tests will run and result in an inverted outcome: -a pass becomes a failure and a fail becomes a semi-passing one. - -The need for skipping a test is usually connected to a condition. -If a test fails under all conditions then it's probably better -to mark your test as 'xfail'. - -By passing ``-rxs`` to the terminal reporter you will see extra -summary information on skips and xfail-run tests at the end of a test run. - -.. _skipif: - -Skipping a single function -------------------------------------------- - -Here is an example for marking a test function to be skipped -when run on a Python3 interpreter:: - - @py.test.mark.skipif("sys.version_info >= (3,0)") - def test_function(): - ... - -During test function setup the skipif condition is -evaluated by calling ``eval(expr, namespace)``. The namespace -contains the ``sys`` and ``os`` modules and the test -``config`` object. The latter allows you to skip based -on a test configuration value e.g. like this:: - - @py.test.mark.skipif("not config.getvalue('db')") - def test_function(...): - ... - -Create a shortcut for your conditional skip decorator -at module level like this:: - - win32only = py.test.mark.skipif("sys.platform != 'win32'") - - @win32only - def test_function(): - ... - - -skip groups of test functions --------------------------------------- - -As with all metadata function marking you can do it at -`whole class- or module level`_. Here is an example -for skipping all methods of a test class based on platform:: - - class TestPosixCalls: - pytestmark = py.test.mark.skipif("sys.platform == 'win32'") - - def test_function(self): - # will not be setup or run under 'win32' platform - # - -The ``pytestmark`` decorator will be applied to each test function. -If your code targets python2.6 or above you can equivalently use -the skipif decorator on classes:: - - @py.test.mark.skipif("sys.platform == 'win32'") - class TestPosixCalls: - - def test_function(self): - # will not be setup or run under 'win32' platform - # - -It is fine in general to apply multiple "skipif" decorators -on a single function - this means that if any of the conditions -apply the function will be skipped. - -.. _`whole class- or module level`: mark.html#scoped-marking - -.. _xfail: - -mark a test function as **expected to fail** -------------------------------------------------------- - -You can use the ``xfail`` marker to indicate that you -expect the test to fail:: - - @py.test.mark.xfail - def test_function(): - ... - -This test will be run but no traceback will be reported -when it fails. Instead terminal reporting will list it in the -"expected to fail" or "unexpectedly passing" sections. - -Same as with skipif_ you can also selectively expect a failure -depending on platform:: - - @py.test.mark.xfail("sys.version_info >= (3,0)") - def test_function(): - ... - -To not run a test and still regard it as "xfailed":: - - @py.test.mark.xfail(..., run=False) - -To specify an explicit reason to be shown with xfailure detail:: - - @py.test.mark.xfail(..., reason="my reason") - -imperative xfail from within a test or setup function ------------------------------------------------------- - -If you cannot declare xfail-conditions at import time -you can also imperatively produce an XFail-outcome from -within test or setup code. Example:: - - def test_function(): - if not valid_config(): - py.test.xfail("unsuppored configuration") - - -skipping on a missing import dependency --------------------------------------------------- - -You can use the following import helper at module level -or within a test or test setup function:: - - docutils = py.test.importorskip("docutils") - -If ``docutils`` cannot be imported here, this will lead to a -skip outcome of the test. You can also skip dependeing if -if a library does not come with a high enough version:: - - docutils = py.test.importorskip("docutils", minversion="0.3") - -The version will be read from the specified module's ``__version__`` attribute. - -imperative skip from within a test or setup function ------------------------------------------------------- - -If for some reason you cannot declare skip-conditions -you can also imperatively produce a Skip-outcome from -within test or setup code. Example:: - - def test_function(): - if not valid_config(): - py.test.skip("unsuppored configuration") - +plugin providing skip and xfail functionality. """ -import py +import py, pytest def pytest_addoption(parser): group = parser.getgroup("general") @@ -156,6 +10,18 @@ def pytest_addoption(parser): action="store_true", dest="runxfail", default=False, help="run tests even if they are marked xfail") +def pytest_namespace(): + return dict(xfail=xfail) + +class XFailed(pytest.fail.Exception): + """ raised from an explicit call to py.test.xfail() """ + +def xfail(reason=""): + """ xfail an executing test or setup functions with the given reason.""" + __tracebackhide__ = True + raise XFailed(reason) +xfail.Exception = XFailed + class MarkEvaluator: def __init__(self, item, name): self.item = item --- a/testing/plugin/test_resultlog.py +++ b/testing/plugin/test_resultlog.py @@ -89,7 +89,7 @@ class TestWithFunctionIntegration: assert lines[0].startswith("S ") assert lines[0].endswith("test_collection_skip.py") assert lines[1].startswith(" ") - assert lines[1].endswith("test_collection_skip.py:1: Skipped: 'hello'") + assert lines[1].endswith("test_collection_skip.py:1: Skipped: hello") lines = self.getresultlog(testdir, fail) assert lines --- a/pytest/_core.py +++ b/pytest/_core.py @@ -7,7 +7,7 @@ assert py.__version__.split(".")[:2] >= "%s is too old, remove or upgrade 'py'" % (py.__version__)) default_plugins = ( - "config session terminal python runner pdb capture unittest mark skipping " + "config session terminal runner python pdb capture unittest mark skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml doctest").split() From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 3253d770b03c: remove imperative xfail, this test passes Message-ID: <20101106085700.7C135241053@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288768153 -3600 # Node ID 3253d770b03c29b46f2d5c96f46b00b01f882551 # Parent 9a0938131cfa4fb581bfbe872bff1625dfbad4d2 remove imperative xfail, this test passes --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -424,7 +424,6 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 - py.test.xfail("fix dist-testing") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 9ff5086b4e67: introduce norecursedirs config option, remove recfilter() Message-ID: <20101106085700.AEE4D24194E@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288909286 -3600 # Node ID 9ff5086b4e67207b3caf98c5554269e1f126557d # Parent 2dfb0db0864d075300712366a86cdf642c7c68d9 introduce norecursedirs config option, remove recfilter() --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -44,8 +44,11 @@ def showhelp(config): tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") tw.line() - for name, (help, type) in sorted(config._parser._inidict.items()): - line = " %-15s %s" %(name, help) + for name, (help, type, default) in sorted(config._parser._inidict.items()): + if type is None: + type = "string" + spec = "%s (%s)" % (name, type) + line = " %-24s %s" %(spec, help) tw.line(line[:tw.fullwidth]) tw.line() ; tw.line() @@ -68,7 +71,7 @@ conftest_options = [ def pytest_report_header(config): lines = [] if config.option.debug or config.option.traceconfig: - lines.append("using: pytest-%s pylib-%s" % + lines.append("using: pytest-%s pylib-%s" % (pytest.__version__,py.__version__)) if config.option.traceconfig: --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -10,10 +10,6 @@ def pytest_cmdline_parse(pluginmanager, config.parse(args) return config -def pytest_addoption(parser): - parser.addini('addopts', 'default command line arguments') - parser.addini('minversion', 'minimally required pytest version') - class Parser: """ Parser for command line arguments. """ @@ -72,9 +68,10 @@ class Parser: setattr(option, name, value) return args - def addini(self, name, description, type=None): + def addini(self, name, help, type=None, default=None): """ add an ini-file option with the given name and description. """ - self._inidict[name] = (description, type) + assert type in (None, "pathlist", "args") + self._inidict[name] = (help, type, default) class OptionGroup: def __init__(self, name, description="", parser=None): @@ -293,13 +290,15 @@ class Config(object): sys.stderr.write(err) raise + def _initini(self, args): + self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) + self._parser.addini('addopts', 'extra command line options', 'args') + self._parser.addini('minversion', 'minimally required pytest version') + def _preparse(self, args, addopts=True): - self.inicfg = {} - self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) - if self.inicfg and addopts: - newargs = self.inicfg.get("addopts", None) - if newargs: - args[:] = py.std.shlex.split(newargs) + args + self._initini(args) + if addopts: + args[:] = self.getini("addopts") + args self._checkversion() self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() @@ -358,20 +357,25 @@ class Config(object): specified name hasn't been registered through a prior ``parse.addini`` call (usually from a plugin), a ValueError is raised. """ try: - description, type = self._parser._inidict[name] + description, type, default = self._parser._inidict[name] except KeyError: raise ValueError("unknown configuration value: %r" %(name,)) try: value = self.inicfg[name] except KeyError: - return # None indicates nothing found + if default is not None: + return default + return {'pathlist': [], 'args': [], None: ''}.get(type) if type == "pathlist": dp = py.path.local(self.inicfg.config.path).dirpath() l = [] for relpath in py.std.shlex.split(value): l.append(dp.join(relpath, abs=True)) return l + elif type == "args": + return py.std.shlex.split(value) else: + assert type is None return value def _getconftest_pathlist(self, name, path=None): --- a/testing/test_collect.py +++ b/testing/test_collect.py @@ -99,14 +99,25 @@ class TestCollectFS: tmpdir.ensure(".whatever", 'test_notfound.py') tmpdir.ensure(".bzr", 'test_notfound.py') tmpdir.ensure("normal", 'test_found.py') - tmpdir.ensure('test_found.py') - col = testdir.getnode(testdir.parseconfig(tmpdir), tmpdir) - items = col.collect() - names = [x.name for x in items] - assert len(items) == 2 - assert 'normal' in names - assert 'test_found.py' in names + result = testdir.runpytest("--collectonly") + s = result.stdout.str() + assert "test_notfound" not in s + assert "test_found" in s + + def test_custom_norecursedirs(self, testdir): + testdir.makeini(""" + [pytest] + norecursedirs = mydir xyz* + """) + tmpdir = testdir.tmpdir + tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") + tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") + tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + rec = testdir.inline_run("xyz123/test_2.py") + rec.assertoutcome(failed=1) def test_found_certain_testfiles(self, testdir): p1 = testdir.makepyfile(test_found = "pass", found_test="pass") --- a/testing/test_config.py +++ b/testing/test_config.py @@ -152,6 +152,23 @@ class TestConfigAPI: assert l[1] == p.dirpath('world/sub.py') py.test.raises(ValueError, config.getini, 'other') + def test_addini_args(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + parser.addini("args", "new args", type="args") + parser.addini("a2", "", "args", default="1 2 3".split()) + """) + p = testdir.makeini(""" + [pytest] + args=123 "123 hello" "this" + """) + config = testdir.parseconfig() + l = config.getini("args") + assert len(l) == 3 + assert l == ["123", "123 hello", "this"] + l = config.getini("a2") + assert l == list("123") + def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): reprec = testdir.inline_run(*opts) --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -16,7 +16,8 @@ EXIT_INTERNALERROR = 3 EXIT_NOHOSTS = 4 def pytest_addoption(parser): - + parser.addini("norecursedirs", "directory patterns to avoid for recursion", + type="args", default=('.*', 'CVS', '_darcs', '{arch}')) group = parser.getgroup("general", "running and selection options") group._addoption('-x', '--exitfirst', action="store_true", default=False, dest="exitfirst", @@ -107,13 +108,15 @@ def pytest_ignore_collect(path, config): return path in ignore_paths def pytest_collect_directory(path, parent): - if not parent.recfilter(path): # by default special ".cvs", ... - # check if cmdline specified this dir or a subdir directly - for arg in parent.collection._argfspaths: - if path == arg or arg.relto(path): - break - else: - return + # check if cmdline specified this dir or a subdir directly + for arg in parent.collection._argfspaths: + if path == arg or arg.relto(path): + break + else: + patterns = parent.config.getini("norecursedirs") + for pat in patterns or []: + if path.check(fnmatch=pat): + return return Directory(path, parent=parent) class Session(object): @@ -465,10 +468,6 @@ class File(FSCollector): """ base class for collecting tests from a file. """ class Directory(FSCollector): - def recfilter(self, path): - if path.check(dir=1, dotfile=0): - return path.basename not in ('CVS', '_darcs', '{arch}') - def collect(self): l = [] for path in self.fspath.listdir(sort=True): From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 567a0d7b8fa2: some more refinements to docs Message-ID: <20101106085700.D8EB2241D22@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996645 -3600 # Node ID 567a0d7b8fa2be3820a64b6a4d5ecb326b383f7a # Parent 9ff5086b4e67207b3caf98c5554269e1f126557d some more refinements to docs --- a/doc/examples.txt +++ /dev/null @@ -1,13 +0,0 @@ - -.. _examples: - -Usages and Examples -=========================================== - -.. toctree:: - :maxdepth: 2 - - example/controlskip.txt - example/mysetup.txt - example/detectpytest.txt - example/nonpython.txt --- a/doc/example/test_collectonly.py +++ /dev/null @@ -1,11 +0,0 @@ - -# run this with $ py.test --collectonly test_collectonly.py -# -def test_function(): - pass - -class TestClass: - def test_method(self): - pass - def test_anothermethod(self): - pass --- /dev/null +++ b/doc/example/simple.txt @@ -0,0 +1,137 @@ + +.. highlightlang:: python + +simple patterns using hooks +========================================================== + +pass different values to a test function, depending on command line options +---------------------------------------------------------------------------- + +Suppose we want to write a test that depends on a command line option. +Here is a basic pattern how to achieve this:: + + # content of test_sample.py + def test_answer(cmdopt): + if cmdopt == "type1": + print ("first") + elif cmdopt == "type2": + print ("second") + assert 0 # to see what was printed + + +For this to work we need to add a command line option and +provide the ``cmdopt`` through a function argument factory:: + + # content of conftest.py + def pytest_addoption(parser): + parser.addoption("--cmdopt", action="store", default="type1", + help="my option: type1 or type2") + + def pytest_funcarg__cmdopt(request): + return request.config.option.cmdopt + +Let's run this without supplying our new command line option:: + + $ py.test -q + F + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ + + cmdopt = 'type1' + + def test_answer(cmdopt): + if cmdopt == "type1": + print ("first") + elif cmdopt == "type2": + print ("second") + > assert 0 # to see what was printed + E assert 0 + + test_sample.py:6: AssertionError + ----------------------------- Captured stdout ------------------------------ + first + 1 failed in 0.02 seconds + +And now with supplying a command line option:: + + $ py.test -q --cmdopt=type2 + F + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ + + cmdopt = 'type2' + + def test_answer(cmdopt): + if cmdopt == "type1": + print ("first") + elif cmdopt == "type2": + print ("second") + > assert 0 # to see what was printed + E assert 0 + + test_sample.py:6: AssertionError + ----------------------------- Captured stdout ------------------------------ + second + 1 failed in 0.02 seconds + +Ok, this completes the basic pattern. However, one often rather +wants to process command line options outside of the test and +rather pass in different or more complex objects. See the +next example or refer to :ref:`mysetup` for more information +on real-life examples. + +generating parameters combinations, depending on command line +---------------------------------------------------------------------------- + +Let's say we want to execute a test with different parameters +and the parameter range shall be determined by a command +line argument. Let's first write a simple computation test:: + + # content of test_compute.py + + def test_compute(param1): + assert param1 < 4 + +Now we add a test configuration like this:: + + # content of conftest.py + + def pytest_addoption(parser): + parser.addoption("--all", action="store_true", + help="run all combinations") + + def pytest_generate_tests(metafunc): + if 'param1' in metafunc.funcargnames: + if metafunc.config.option.all: + end = 5 + else: + end = 2 + for i in range(end): + metafunc.addcall(funcargs={'param1': i}) + +This means that we only run 2 tests if we do not pass ``--all``:: + + $ py.test -q test_compute.py + .. + 2 passed in 0.01 seconds + +We run only two computations, so we see two dots. +let's run the full monty:: + + $ py.test -q --all test_compute.py + ....F + ================================= FAILURES ================================= + _____________________________ test_compute[4] ______________________________ + + param1 = 4 + + def test_compute(param1): + > assert param1 < 4 + E assert 4 < 4 + + test_compute.py:3: AssertionError + 1 failed, 4 passed in 0.03 seconds + + +As expected when running the full range of ``param1`` values +we'll get an error on the last one. --- /dev/null +++ b/doc/example/index.txt @@ -0,0 +1,15 @@ + +.. _examples: + +Usages and Examples +=========================================== + +.. toctree:: + :maxdepth: 2 + + pythoncollection.txt + controlskip.txt + mysetup.txt + detectpytest.txt + nonpython.txt + simple.txt --- /dev/null +++ b/doc/example/pythoncollection.txt @@ -0,0 +1,29 @@ +Changing standard (Python) test discovery +=============================================== + +changing directory recursion +----------------------------------------------------- + +You can set the :confval:`norecursedirs` option in an ini-file, for example your ``setup.cfg`` in the project root directory:: + + # content of setup.cfg + [pytest] + norecursedirs = .svn _build tmp* + +This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. + + +finding out what is collected +----------------------------------------------- + +You can always peek at the collection tree without running tests like this:: + + $ py.test --collectonly collectonly.py + + + + + + + + --- a/doc/conf.py +++ b/doc/conf.py @@ -258,7 +258,7 @@ epub_copyright = u'2010, holger krekel e # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {} # 'http://docs.python.org/': None} def setup(app): #from sphinx.ext.autodoc import cut_lines #app.connect('autodoc-process-docstring', cut_lines(4, what=['module'])) --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -4,6 +4,8 @@ creating and managing test function argu .. currentmodule:: pytest.plugin.python + +.. _`funcargs`: .. _`funcarg mechanism`: Test function arguments and factories @@ -34,18 +36,18 @@ Running the test looks like this:: =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_simplefactory.py - + test_simplefactory.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + myfuncarg = 42 - + def test_function(myfuncarg): > assert myfuncarg == 17 E assert 42 == 17 - + test_simplefactory.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -118,7 +120,8 @@ example: Basic generated test example ---------------------------- -Let's consider this test module:: +Let's consider a test module which uses the ``pytest_generate_tests`` +hook to generate several calls to the same test function:: # content of test_example.py def pytest_generate_tests(metafunc): @@ -135,23 +138,24 @@ Running this:: =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_example.py - + test_example.py .........F - + ================================= FAILURES ================================= _______________________________ test_func[9] _______________________________ - + numiter = 9 - + def test_func(numiter): > assert numiter < 9 E assert 9 < 9 - + test_example.py:7: AssertionError ==================== 1 failed, 9 passed in 0.03 seconds ==================== Note that the ``pytest_generate_tests(metafunc)`` hook is called during -the test collection phase. You can have a look at it with this:: +the test collection phase which is separate from the actual test running. +Let's just look at what is collected:: $ py.test --collectonly test_example.py @@ -173,7 +177,7 @@ If you want to select only the run with =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python test path 1: test_example.py - + test_example.py:6: test_func[0] PASSED test_example.py:6: test_func[1] PASSED test_example.py:6: test_func[2] PASSED @@ -184,16 +188,16 @@ If you want to select only the run with test_example.py:6: test_func[7] PASSED test_example.py:6: test_func[8] PASSED test_example.py:6: test_func[9] FAILED - + ================================= FAILURES ================================= _______________________________ test_func[9] _______________________________ - + numiter = 9 - + def test_func(numiter): > assert numiter < 9 E assert 9 < 9 - + test_example.py:7: AssertionError ==================== 1 failed, 9 passed in 0.04 seconds ==================== --- a/doc/customize.txt +++ b/doc/customize.txt @@ -35,13 +35,13 @@ If no path was provided at all the curre builtin configuration file options ---------------------------------------------- -.. confval:: minversion = VERSTRING +.. confval:: minversion - specifies the minimal pytest version that is needed for this test suite. + specifies a minimal pytest version needed for running tests. minversion = 2.1 # will fail if we run with pytest-2.0 -.. confval:: addopts = OPTS +.. confval:: addopts add the specified ``OPTS`` to the set of command line arguments as if they had been specified by the user. Example: if you have this ini file content:: @@ -53,5 +53,28 @@ builtin configuration file options py.test --maxfail=2 -rf test_hello.py -.. _`function arguments`: funcargs.html + Default is to add no options. +.. confval:: norecursedirs + + Set the directory basename patterns to avoid when recursing + for test discovery. The individual (fnmatch-style) patterns are + applied to the basename of a directory to decide if to recurse into it. + Pattern matching characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + Default patterns are ``.* _* CVS {args}``. Setting a ``norecurse`` + replaces the default. Here is a customizing example for avoiding + a different set of directories:: + + # content of setup.cfg + [pytest] + norecursedirs = .svn _build tmp* + + This would tell py.test to not recurse into typical subversion or + sphinx-build directories or into any ``tmp`` prefixed directory. + --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -1,8 +1,6 @@ Installation and Getting Started =================================== -.. _`easy_install`: - **Compatibility**: Python 2.4-3.2, Jython, PyPy on Unix/Posix and Windows Installation @@ -17,27 +15,26 @@ To check your installation has installed $ py.test --version -If you get an error, checkout :ref:`installation issues`. - +If you get an error checkout :ref:`installation issues`. Our first test run ---------------------------------------------------------- -Let's create a small file with a test function testing a function -computes a certain value:: +Let's create a first test file with a simple test function:: # content of test_sample.py def func(x): return x + 1 + def test_answer(): assert func(3) == 5 -You can execute the test function:: +That's it. You can execute the test function now:: - $ py.test test_sample.py + $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: test_sample.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev18 + test path 1: /tmp/doc-exec-211 test_sample.py F @@ -49,19 +46,26 @@ You can execute the test function:: E assert 4 == 5 E + where 4 = func(3) - test_sample.py:4: AssertionError + test_sample.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= -We told py.test to run the ``test_sample.py`` file and it :ref:`discovered` the -``test_answer`` function because of the ``test_`` prefix. We got a -failure because our little ``func(3)`` call did not return ``5``. +py.test found the ``test_answer`` function by following :ref:`standard test discovery rules `, basically detecting the ``test_`` prefixes. We got a failure report because our little ``func(3)`` call did not return ``5``. The report is formatted using the :ref:`standard traceback reporting`. .. note:: - You can simply use the `assert statement`_ for coding expectations because - intermediate values will be presented to you. Or to put it bluntly, - there is no need to learn all `the JUnit legacy methods`_ for expressing - assertions. + You can simply use the ``assert`` statement for coding expectations because + intermediate values will be presented to you. This is much easier than + learning all the `the JUnit legacy methods`_ which are even inconsistent + with Python's own coding guidelines (but consistent with + Java-style naming). + + There is only one seldomly hit caveat to using asserts: if your + assertion expression fails and has side effects then re-evaluating + it for presenting intermediate values can go wrong. It's easy to fix: + compute the value ahead of the assert and then do the + assertion or use the assert "message" syntax:: + + assert expr, "message" # show "message" if expr is not True .. _`the JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases @@ -88,7 +92,7 @@ Running it with, this time in "quiet" re . 1 passed in 0.01 seconds -.. todo:: For further ways to assert exceptions see the :pyfunc:`raises` +.. todo:: For further ways to assert exceptions see the `raises` Grouping multiple tests in a class -------------------------------------------------------------- @@ -107,16 +111,16 @@ tests in a class like this:: x = "hello" assert hasattr(x, 'check') -The two tests will be discovered because of the default `automatic test -discovery`_. There is no need to subclass anything. If we now run -the module we'll see one passed and one failed test:: +The two tests are found because of the standard :ref:`test discovery`. +There is no need to subclass anything. We can simply +run the module by passing its filename:: $ py.test -q test_class.py .F ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -126,27 +130,74 @@ the module we'll see one passed and one test_class.py:8: AssertionError 1 failed, 1 passed in 0.02 seconds -where to go from here +The first test passed, the second failed. Again we can easily see +the intermediate values used in the assertion, helping us to +understand the reason for the failure. + +Going functional: requesting a unique temporary directory +-------------------------------------------------------------- + +For functional tests one often needs to create some files +and pass them to application objects. py.test provides +the versatile :ref:`funcarg mechanism` which allows to request +arbitrary resources, for example a unique temporary directory:: + + # content of test_tmpdir.py + def test_needsfiles(tmpdir): + print tmpdir + assert 0 + +We list the name ``tmpdir`` in the test function signature and +py.test will lookup and call a factory to create the resource +before performing the test function call. Let's just run it:: + + $ py.test -q test_tmpdir.py + F + ================================= FAILURES ================================= + _____________________________ test_needsfiles ______________________________ + + tmpdir = local('/tmp/pytest-1306/test_needsfiles0') + + def test_needsfiles(tmpdir): + print tmpdir + > assert 0 + E assert 0 + + test_tmpdir.py:3: AssertionError + ----------------------------- Captured stdout ------------------------------ + /tmp/pytest-1306/test_needsfiles0 + 1 failed in 0.04 seconds + +Before the test runs, a unique-per-test-invocation temporary directory +was created. More info at :ref:`tmpdir handling`. + +You can find out what kind of builtin :ref:`funcargs` exist by typing:: + + py.test --funcargs # shows builtin and custom function arguments + +where to go next ------------------------------------- Here are a few suggestions where to go next: * :ref:`cmdline` for command line invocation examples * :ref:`good practises` for virtualenv, test layout, genscript support -* :ref:`apiref` for documentation and examples on writing Python tests +* :ref:`apiref` for documentation and examples on using py.test +* :ref:`plugins` managing and writing plugins .. _`installation issues`: -Installation issues +Known Installation issues ------------------------------ easy_install or pip not found? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Consult distribute_ to install the ``easy_install`` tool on your machine. -You may also use the original but somewhat older `setuptools`_ project -although we generally recommend to use ``distribute`` because it contains -more bug fixes and also works for Python3. +Consult `distribute docs `_ to install the ``easy_install`` +tool on your machine. You may also use the original but somewhat older +`setuptools`_ project although we generally recommend to use +``distribute`` because it contains more bug fixes and also works for +Python3. For Python2 you can also consult pip_ for the popular ``pip`` tool. --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -1,3 +1,5 @@ + +.. _`tmpdir handling`: temporary directories and files ================================================ @@ -18,7 +20,7 @@ and more. Here is an example test usage p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" - assert len(os.listdir(str(tmpdir))) == 1 + assert tmpdir.listdir() == 1 assert 0 Running this would result in a passed test except for the last @@ -41,8 +43,6 @@ Running this would result in a passed te p.write("content") assert p.read() == "content" assert len(os.listdir(str(tmpdir))) == 1 - > assert 0 - E assert 0 test_tmpdir.py:7: AssertionError ========================= 1 failed in 0.04 seconds ========================= --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -1,3 +1,6 @@ + +.. _`unittest.TestCase`: + unittest.TestCase support ===================================================================== --- /dev/null +++ b/doc/extracol @@ -0,0 +1,32 @@ +changing Python test discovery patterns +-------------------------------------------------- + +You can influence python test file, function and class prefixes through +the :confval:`python_patterns` configuration valueto determine which +files are checked and which test functions are found. Example for using +a scheme that builds on ``check`` rather than on ``test`` prefixes:: + + + # content of setup.cfg + [pytest] + python_patterns = + files: check_*.py + functions: check_ + classes: Check + +See + :confval:`python_funcprefixes` and :confval:`python_classprefixes` + + + changing test file discovery + ----------------------------------------------------- + + You can specify patterns where python tests are found:: + + python_testfilepatterns = + testing/**/{purebasename}.py + testing/*.py + + .. note:: + + conftest.py files are never considered for test discovery --- a/doc/example/nonpython/conftest.py +++ b/doc/example/nonpython/conftest.py @@ -11,22 +11,22 @@ class YamlFile(py.test.collect.File): import yaml # we need a yaml parser, e.g. PyYAML raw = yaml.load(self.fspath.open()) for name, spec in raw.items(): - yield UsecaseItem(name, self, spec) + yield YamlItem(name, self, spec) -class UsecaseItem(py.test.collect.Item): +class YamlItem(py.test.collect.Item): def __init__(self, name, parent, spec): - super(UsecaseItem, self).__init__(name, parent) + super(YamlItem, self).__init__(name, parent) self.spec = spec def runtest(self): for name, value in self.spec.items(): # some custom test execution (dumb example follows) if name != value: - raise UsecaseException(self, name, value) + raise YamlException(self, name, value) def repr_failure(self, excinfo): """ called when self.runtest() raises an exception. """ - if excinfo.errisinstance(UsecaseException): + if isinstance(excinfo.value, YamlException): return "\n".join([ "usecase execution failed", " spec failed: %r: %r" % excinfo.value.args[1:3], @@ -36,5 +36,5 @@ class UsecaseItem(py.test.collect.Item): def reportinfo(self): return self.fspath, 0, "usecase: %s" % self.name -class UsecaseException(Exception): +class YamlException(Exception): """ custom exception for error reporting. """ --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -5,7 +5,7 @@ Good Integration Practises ================================================= -work with virtual environments +Work with virtual environments ----------------------------------------------------------- We recommend to work with virtualenv_ environments and use easy_install_ @@ -21,6 +21,24 @@ server Hudson_. .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip +.. _`test discovery`: + +Conventions for Python test discovery +------------------------------------------------- + +``py.test`` implements the following standard test discovery: + +* collection starts from initial command line arguments + which may be directories, filenames or test ids. +* recurse into directories, unless they match :confval:`norecursedirs` +* ``test_*.py`` or ``*_test.py`` files, imported by their `package name`_. +* ``Test`` prefixed test classes (without an ``__init__`` method) +* ``test_`` prefixed test functions or methods are test items + +For changing and customization example, see :doc:`example/pythoncollection`. + +py.test additionally discovers tests using the standard +:ref:`unittest.TestCase ` subclassing technique. Choosing a test layout / import rules ------------------------------------------ @@ -57,6 +75,8 @@ You can always run your tests by pointin py.test # run all tests below current dir ... +.. _`package name`: + .. note:: Test modules are imported under their fully qualified name as follows: --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -4,6 +4,8 @@ Working with non-python tests ==================================================== +.. _`yaml plugin`: + a basic example for specifying tests in Yaml files -------------------------------------------------------------- @@ -39,7 +41,17 @@ now execute the test specification:: You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more -interesting interpretation of the yaml-values. Note that ``reportinfo()`` +interesting interpretation of the yaml-values. You can easily write +your own domain specific testing language this way. + +.. note:: + + ``repr_failure(excinfo)`` is called for representing test failures. + If you create custom collection nodes you can return an error + representation string of your choice. It + will be reported as a (red) string. + + ``reportinfo()`` is used for representing the test location and is also consulted for reporting in ``verbose`` mode:: --- a/doc/index.txt +++ b/doc/index.txt @@ -12,7 +12,7 @@ Welcome to ``py.test`` documentation: overview apiref plugins - examples + example/index talks develop --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -3,7 +3,7 @@ Writing, managing and understanding plug .. _`local plugin`: -py.test 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 locations types:: +py.test 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 locations types: * builtin plugins: loaded from py.test's own `pytest/plugin`_ directory. * `external plugins`_: modules discovered through `setuptools entry points`_ @@ -55,7 +55,7 @@ earlier than further away ones. .. _`installing plugins`: .. _`external plugins`: -Installing External Plugins +Installing External Plugins / Searching ------------------------------------------------------ Installing a plugin happens through any usual Python installation @@ -72,9 +72,7 @@ de-install it. You can find a list of v .. _`available installable plugins`: .. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search -.. _`setuptools entry points`: - -Writing an installable plugin +Writing a plugin by looking at examples ------------------------------------------------------ .. _`Distribute`: http://pypi.python.org/pypi/distribute @@ -83,9 +81,18 @@ Writing an installable plugin 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 comprise py.test's own functionality * around 10 `external plugins`_ providing additional features +All of these plugins are using the documented `well specified hooks`_ +to implement their wide-ranging functionality. + +.. _`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 ``py.test`` finds your plugin module. Entry points are @@ -149,9 +156,6 @@ will be loaded as well. You can also us which will import the specified module as a py.test plugin. -.. _`setuptools entry points`: -.. _registered: - Accessing another plugin by name -------------------------------------------- --- /dev/null +++ b/doc/example/collectonly.py @@ -0,0 +1,11 @@ + +# run this with $ py.test --collectonly test_collectonly.py +# +def test_function(): + pass + +class TestClass: + def test_method(self): + pass + def test_anothermethod(self): + pass --- a/doc/discovery.txt +++ /dev/null @@ -1,45 +0,0 @@ - -Test collection and discovery -====================================================== - -.. _`discovered`: - -Default filesystem test discovery ------------------------------------------------ - -Test collection starts from paths specified at the command line or from -the current directory. Tests are collected ahead of running the first test. -(This used to be different in earlier versions of ``py.test`` where -collection and running was interweaved which made test randomization -and distributed testing harder). - -Collection nodes which have children are called "Collectors" and otherwise -they are called "Items" or "test items". Here is an example of such a -tree:: - - example $ py.test --collectonly test_collectonly.py - - - - - - - - -By default all directories not starting with a dot are traversed, -looking for ``test_*.py`` and ``*_test.py`` files. Those Python -files are imported under their `package name`_. - -The Module collector looks for test functions -and test classes and methods. Test functions and methods -are prefixed ``test`` by default. Test classes must -start with a capitalized ``Test`` prefix. - -Customizing error messages -------------------------------------------------- - -On test and collection nodes ``py.test`` will invoke -the ``node.repr_failure(excinfo)`` function which -you may override and make it return an error -representation string of your choice. It -will be reported as a (red) string. From commits-noreply at bitbucket.org Sat Nov 6 11:37:26 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 05:37:26 -0500 (CDT) Subject: [py-svn] pytest commit 5f89191fcd0a: some more improvements and updates to docs, add release announcements Message-ID: <20101106103726.6B9261E135A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289039933 -3600 # Node ID 5f89191fcd0a77727f9c8e5a3a73f8cc2e964e31 # Parent 3da7c9990c7de697f8a395956d68a277fff5c2fb some more improvements and updates to docs, add release announcements --- a/pytest/plugin/monkeypatch.py +++ b/pytest/plugin/monkeypatch.py @@ -66,7 +66,7 @@ class monkeypatch: def setenv(self, name, value, prepend=None): """ set environment variable ``name`` to ``value``. if ``prepend`` - is a character, read the current environment variable value + is a character, read the current environment variable value and prepend the ``value`` adjoined with the ``prepend`` character.""" value = str(value) if prepend and name in os.environ: --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -28,25 +28,25 @@ You can ask for available builtin or pro captures writes to sys.stdout/sys.stderr and makes them available successively via a ``capsys.readouterr()`` method which returns a ``(out, err)`` tuple of captured snapshot strings. - + capfd captures writes to file descriptors 1 and 2 and makes snapshotted ``(out, err)`` string tuples available via the ``capsys.readouterr()`` method. If the underlying platform does not have ``os.dup`` (e.g. Jython) tests using this funcarg will automatically skip. - + tmpdir return a temporary directory path object unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. - + monkeypatch The returned ``monkeypatch`` funcarg provides these helper methods to modify objects, dictionaries or os.environ:: - + monkeypatch.setattr(obj, name, value, raising=True) monkeypatch.delattr(obj, name, raising=True) monkeypatch.setitem(mapping, name, value) @@ -54,15 +54,15 @@ You can ask for available builtin or pro monkeypatch.setenv(name, value, prepend=False) monkeypatch.delenv(name, value, raising=True) monkeypatch.syspath_prepend(path) - + All modifications will be undone when the requesting test function finished its execution. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. - + recwarn Return a WarningsRecorder instance that provides these methods: - + * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - + --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -1,6 +1,4 @@ -""" -funcargs and support code for testing py.test's own functionality. -""" +""" (disabled by default) support for testing py.test and py.test plugins. """ import py, pytest import sys, os --- a/doc/example/builtin.txt +++ b/doc/example/builtin.txt @@ -3,8 +3,9 @@ writing well integrated assertion helper ======================================================== If you have a test helper function called from a test you can -use the ``pytest.fail``_ builtin to cleanly fail a test with a message. -The test support function will never itself show up in the traceback. +use the ``pytest.fail`` marker to fail a test with a certain message. +The test support function will not show up in the traceback if you +set the ``__tracebackhide__`` option somewhere in the helper function. Example:: # content of test_checkconfig.py @@ -33,4 +34,3 @@ Let's run our little function:: test_checkconfig.py:8: Failed 1 failed in 0.02 seconds - --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -34,20 +34,20 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_simplefactory.py - + test_simplefactory.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + myfuncarg = 42 - + def test_function(myfuncarg): > assert myfuncarg == 17 E assert 42 == 17 - + test_simplefactory.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -136,70 +136,52 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_example.py - + test_example.py .........F - + ================================= FAILURES ================================= _______________________________ test_func[9] _______________________________ - + numiter = 9 - + def test_func(numiter): > assert numiter < 9 E assert 9 < 9 - + test_example.py:7: AssertionError - ==================== 1 failed, 9 passed in 0.03 seconds ==================== + ==================== 1 failed, 9 passed in 0.04 seconds ==================== Note that the ``pytest_generate_tests(metafunc)`` hook is called during the test collection phase which is separate from the actual test running. Let's just look at what is collected:: $ py.test --collectonly test_example.py - - - - - - - - - - - - + + + + + + + + + + + + If you want to select only the run with the value ``7`` you could do:: $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python test path 1: test_example.py - - test_example.py:6: test_func[0] PASSED - test_example.py:6: test_func[1] PASSED - test_example.py:6: test_func[2] PASSED - test_example.py:6: test_func[3] PASSED - test_example.py:6: test_func[4] PASSED - test_example.py:6: test_func[5] PASSED - test_example.py:6: test_func[6] PASSED - test_example.py:6: test_func[7] PASSED - test_example.py:6: test_func[8] PASSED - test_example.py:6: test_func[9] FAILED - - ================================= FAILURES ================================= - _______________________________ test_func[9] _______________________________ - - numiter = 9 - - def test_func(numiter): - > assert numiter < 9 - E assert 9 < 9 - - test_example.py:7: AssertionError - ==================== 1 failed, 9 passed in 0.04 seconds ==================== + + test_example.py <- test_example.py:6: test_func[7] PASSED + + ======================== 9 tests deselected by '7' ========================= + ================== 1 passed, 9 deselected in 0.01 seconds ================== .. _`metafunc object`: --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -1,5 +1,6 @@ -""" logging of test results in JUnit-XML format, for use with Hudson - and build integration servers. Based on initial code from Ross Lawley. +""" report test results in JUnit-XML format, for use with Hudson and build integration servers. + +Based on initial code from Ross Lawley. """ import py --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,8 +88,8 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-171 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-407 test_mark.py .. test_mark_classlevel.py .. @@ -100,8 +100,8 @@ And you can also run all tests except th $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-171 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-407 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-171 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-407 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -24,7 +24,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_unittest.py test_unittest.py F @@ -32,7 +32,7 @@ Running it yields:: ================================= FAILURES ================================= ____________________________ MyTest.test_method ____________________________ - self = + self = def test_method(self): x = 1 @@ -41,7 +41,7 @@ Running it yields:: test_unittest.py:8: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - self = , first = 1, second = 3 + self = , first = 1, second = 3 msg = None def failUnlessEqual(self, first, second, msg=None): --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -1,4 +1,4 @@ -""" collect and execute doctests from modules and test files.""" +""" discover and run doctests in modules and test files.""" import py from py._code.code import TerminalRepr, ReprFileLocation --- a/pytest/plugin/capture.py +++ b/pytest/plugin/capture.py @@ -1,6 +1,4 @@ -""" plugin for configurable per-test stdout/stderr capturing mechanisms and -``capsys`` and ``capfd`` function arguments. -""" +""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ import py import os --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -20,15 +20,15 @@ and more. Here is an example test usage p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" - assert tmpdir.listdir() == 1 + assert len(tmpdir.listdir()) == 1 assert 0 -Running this would result in a passed test except for the last +Running this would result in a passed test except for the last ``assert 0`` line which we use to look at values:: - + $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_tmpdir.py test_tmpdir.py F @@ -36,13 +36,15 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-1248/test_create_file0') + tmpdir = local('/tmp/pytest-243/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" - assert len(os.listdir(str(tmpdir))) == 1 + assert len(tmpdir.listdir()) == 1 + > assert 0 + E assert 0 test_tmpdir.py:7: AssertionError ========================= 1 failed in 0.04 seconds ========================= @@ -52,7 +54,7 @@ Running this would result in a passed te the default base temporary directory ----------------------------------------------- -.. +.. You can create directories by calling one of two methods on the config object: - ``config.mktemp(basename)``: create and return a new tempdir @@ -61,14 +63,14 @@ the default base temporary directory Temporary directories are by default created as sub directories of the system temporary directory. The name will be ``pytest-NUM`` where ``NUM`` will be incremenated with each test run. Moreover, entries older -than 3 temporary directories will be removed. +than 3 temporary directories will be removed. You can override the default temporary directory logic and set it like this:: py.test --basetemp=mydir -When distributing tests on the local machine, ``py.test`` takes care to -configure a basetemp directory for the sub processes such that all +When distributing tests on the local machine, ``py.test`` takes care to +configure a basetemp directory for the sub processes such that all temporary data lands below below a single per-test run basetemp directory. .. _`py.path.local`: http://pylib.org/path.html --- /dev/null +++ b/doc/announce/release-2.0.0.txt @@ -0,0 +1,50 @@ +py.test 2.0.0: standalone, features++, implementation++, docs++ +=========================================================================== + +XXX PENDING + +Welcome to pytest-2.0.0! With this release py.test becomes its own standalone +PyPI distribution, named ``pytest``, installing the ``py.test`` command line +tool. Apart from a great internal cleanup this release comes with tons +of improvements and new features and a completely revamped extensive +documentation, including many continously tested examples. See + + http://pytest.org + +New Features +----------------------- + +- new invocations through Python interpreter and from Python:: + + python -m pytest # on all pythons >= 2.7 + python -m pytest.main # on all pythons >= 2.5 + import pytest ; pytest.main(args, plugins) + + see http://pytest.org/2.0.0/invoke.html for details. + +- new configuration through ini-files (setup.cfg or tox.ini recognized), + for example:: + + [pytest] + norecursedirs = .hg _build + python_collect_funcprefix = test_ + python_collect_classprefix = Test + + see http://pytest.org/2.0.0/customize.html + +- + +Thanks to issue reporters, people asking questions, complaining and +generally to Ronny Pfannschmidt for his awesome help on many issues. + +cheers, +holger krekel + +Changes between 1.3.3 and 1.3.4 +================================================== + +- fix issue111: improve install documentation for windows +- fix issue119: fix custom collectability of __init__.py as a module +- fix issue116: --doctestmodules work with __init__.py files as well +- fix issue115: unify internal exception passthrough/catching/GeneratorExit +- fix issue118: new --tb=native for presenting cpython-standard exceptions --- /dev/null +++ b/doc/nose.txt @@ -0,0 +1,42 @@ +Running test written for nose +======================================= + +.. include:: links.inc + +py.test has basic support for running tests written for nose_. +This is implemented in :pymod:`pytest.plugin.nose`. + +Usage +------------- + +type:: + + py.test # instead of 'nosetests' + +and you should be able to run your nose style tests and at the same +make full use of py.test's capabilities. + +Supported nose Idioms +---------------------- + +* setup and teardown at module/class/method level +* SkipTest exceptions and markers +* setup/teardown decorators +* yield-based tests and their setup +* general usage of nose utilities + +Unsupported idioms / issues +---------------------------------- + +- nose-style doctests are not collected and executed correctly, + also fixtures don't work. + +- no nose-configuration is recognized + +If you find other issues or have suggestions please run:: + + py.test --pastebin=all + +and send the resulting URL to a py.test contact channel, +at best to the mailing list. +""" --- a/pytest/plugin/mark.py +++ b/pytest/plugin/mark.py @@ -70,7 +70,7 @@ def matchonekeyword(key, itemkeywords): return True class MarkGenerator: - """ Factory for :class:`MarkDecorator` objects - exposed as + """ Factory for :class:`MarkDecorator` objects - exposed as a ``py.test.mark`` singleton instance. Example:: import py @@ -88,8 +88,8 @@ class MarkGenerator: class MarkDecorator: """ A decorator for test functions and test classes. When applied - it will create :class:`MarkInfo` objects which may be - :ref:`retrieved by hooks as item keywords` MarkDecorator instances + it will create :class:`MarkInfo` objects which may be + :ref:`retrieved by hooks as item keywords` MarkDecorator instances are usually created by writing:: mark1 = py.test.mark.NAME # simple MarkDecorator --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,7 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-197 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-400 ============================= in 0.00 seconds ============================= --- a/doc/example/xunit_setup.txt +++ b/doc/example/xunit_setup.txt @@ -19,8 +19,8 @@ custom options:: .. _optparse: http://docs.python.org/library/optparse.html -Working Examples -================ +order of setup/teardown module/class/item methods +==================================================== managing state at module, class and method level ------------------------------------------------------------ --- a/pytest/plugin/recwarn.py +++ b/pytest/plugin/recwarn.py @@ -1,4 +1,4 @@ -""" record warnings to allow assertions about them. """ +""" recording warnings during test function execution. """ import py import sys, os --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,12 +122,12 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-198/conftest.py:22: 'specify ssh host with --ssh' + SKIP [1] /tmp/doc-exec-438/conftest.py:22: specify ssh host with --ssh ======================== 1 skipped in 0.02 seconds ========================= --- a/doc/index.txt +++ b/doc/index.txt @@ -19,6 +19,7 @@ Welcome to ``py.test`` documentation: example/index talks develop + announce/index .. toctree:: :hidden: --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -5,7 +5,7 @@ Writing, managing and understanding plug py.test 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 locations types: -* builtin plugins: loaded from py.test's own `pytest/plugin`_ directory. +* `builtin plugins`_: loaded from py.test's own ``pytest/plugin`` directory. * `external plugins`_: modules discovered through `setuptools entry points`_ * `conftest.py plugins`_: modules auto-discovered in test directories @@ -48,8 +48,8 @@ earlier than further away ones. python package directory (i.e. one containing an ``__init__.py``) then "import conftest" can be ambigous 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 + 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. .. _`installing plugins`: @@ -90,6 +90,7 @@ to implement their wide-ranging function .. _`setuptools entry points`: + Making your plugin installable by others ----------------------------------------------- @@ -97,8 +98,8 @@ If you want to make your plugin external may define a so called entry point for your distribution so that ``py.test`` finds your plugin module. Entry points are a feature that is provided by `setuptools`_ or `Distribute`_. -The concrete entry point is ``pytest11``. To make your plugin -available you can insert the following lines in your +The concrete entry point is ``pytest11``. To make your plugin +available you can insert the following lines in your setuptools/distribute-based setup-invocation: .. sourcecode:: python @@ -171,6 +172,37 @@ the plugin manager like this: If you want to look at the names of existing plugins, use the ``--traceconfig`` option. + +.. _`builtin plugins`: + +py.test default plugin reference +==================================== + +.. autosummary:: + + pytest.plugin.assertion + pytest.plugin.capture + pytest.plugin.config + pytest.plugin.doctest + pytest.plugin.genscript + pytest.plugin.helpconfig + pytest.plugin.junitxml + pytest.plugin.mark + pytest.plugin.monkeypatch + pytest.plugin.nose + pytest.plugin.pastebin + pytest.plugin.pdb + pytest.plugin.pytester + pytest.plugin.python + pytest.plugin.recwarn + pytest.plugin.resultlog + pytest.plugin.runner + pytest.plugin.session + pytest.plugin.skipping + pytest.plugin.terminal + pytest.plugin.tmpdir + pytest.plugin.unittest + .. _`well specified hooks`: py.test hook reference @@ -183,7 +215,7 @@ py.test calls hook functions to implemen test execution and reporting. When py.test loads a plugin it validates that all hook functions conform to their respective hook specification. Each hook function name and its argument names need to match a hook -specification exactly but it is allowed for a hook function to accept +specification exactly but it is allowed for a hook function to accept *less* parameters than specified. If you mistype argument names or the hook name itself you get useful errors. @@ -261,7 +293,7 @@ Reference of important objects involved .. autoclass:: pytest.plugin.session.Node(name, parent) :members: -.. +.. .. autoclass:: pytest.plugin.session.File(fspath, parent) :members: --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -1,6 +1,4 @@ -""" -collect and run test items and create reports. -""" +""" basic collect and runtest protocol implementations """ import py, sys from py._code.code import TerminalRepr @@ -92,11 +90,12 @@ def call_runtest_hook(item, when): return CallInfo(lambda: ihook(item=item), when=when) class CallInfo: - """ Call Information about a hook call. """ + """ Result/Exception info a function invocation. """ #: None or ExceptionInfo object. excinfo = None def __init__(self, func, when): - #: one of "setup", "call", "teardown" specifying the runtest phase. + #: context of invocation: one of "setup", "call", + #: "teardown", "memocollect" self.when = when try: self.result = func() --- a/pytest/plugin/pdb.py +++ b/pytest/plugin/pdb.py @@ -1,6 +1,5 @@ -""" -interactive debugging with the Python Debugger. -""" +""" interactive debugging with PDB, the Python Debugger. """ + import py import sys --- a/pytest/plugin/assertion.py +++ b/pytest/plugin/assertion.py @@ -1,3 +1,6 @@ +""" +support for presented detailed information in failing assertions. +""" import py import sys --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-172 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-408 ============================= in 0.00 seconds ============================= --- a/pytest/plugin/tmpdir.py +++ b/pytest/plugin/tmpdir.py @@ -1,13 +1,4 @@ -"""provide temporary directories to test functions. - -usage example:: - - def test_plugin(tmpdir): - tmpdir.join("hello").write("hello") - -.. _`py.path.local`: ../../path.html - -""" +""" support for providing temporary directories to test functions. """ import pytest, py def pytest_configure(config): --- a/pytest/plugin/nose.py +++ b/pytest/plugin/nose.py @@ -1,42 +1,5 @@ -"""nose-compatibility plugin: allow to run nose test suites natively. +"""run test suites written for nose. """ -This is an experimental plugin for allowing to run tests written -in 'nosetests style with py.test. - -Usage -------------- - -type:: - - py.test # instead of 'nosetests' - -and you should be able to run nose style tests and at the same -time can make full use of py.test's capabilities. - -Supported nose Idioms ----------------------- - -* setup and teardown at module/class/method level -* SkipTest exceptions and markers -* setup/teardown decorators -* yield-based tests and their setup -* general usage of nose utilities - -Unsupported idioms / issues ----------------------------------- - -- nose-style doctests are not collected and executed correctly, - also fixtures don't work. - -- no nose-configuration is recognized - -If you find other issues or have suggestions please run:: - - py.test --pastebin=all - -and send the resulting URL to a py.test contact channel, -at best to the mailing list. -""" import py import inspect import sys --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -36,9 +36,9 @@ Let's run this without supplying our new F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + cmdopt = 'type1' - + def test_answer(cmdopt): if cmdopt == "type1": print ("first") @@ -46,7 +46,7 @@ Let's run this without supplying our new print ("second") > assert 0 # to see what was printed E assert 0 - + test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ first @@ -58,9 +58,9 @@ And now with supplying a command line op F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + cmdopt = 'type2' - + def test_answer(cmdopt): if cmdopt == "type1": print ("first") @@ -68,7 +68,7 @@ And now with supplying a command line op print ("second") > assert 0 # to see what was printed E assert 0 - + test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ second @@ -122,16 +122,15 @@ let's run the full monty:: ....F ================================= FAILURES ================================= _____________________________ test_compute[4] ______________________________ - + param1 = 4 - + def test_compute(param1): > assert param1 < 4 E assert 4 < 4 - + test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.03 seconds - + 1 failed, 4 passed in 0.02 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. --- a/pytest/plugin/resultlog.py +++ b/pytest/plugin/resultlog.py @@ -1,4 +1,4 @@ -""" create machine readable plain text file with results. """ +""" (disabled by default) create result information in a plain text file. """ import py from py.builtin import print_ --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -1,10 +1,10 @@ +""" command line configuration, ini-file and conftest.py processing. """ import py import sys, os from pytest.main import PluginManager import pytest - def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) config.parse(args) --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -14,6 +14,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version + This is py.test version 2.0.0.dev19, imported from /home/hpk/p/pytest/pytest If you get an error checkout :ref:`installation issues`. @@ -33,8 +34,8 @@ That's it. You can execute the test func $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev18 - test path 1: /tmp/doc-exec-211 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-404 test_sample.py F @@ -120,7 +121,7 @@ run the module by passing its filename:: ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -156,7 +157,7 @@ before performing the test function call ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-1306/test_needsfiles0') + tmpdir = local('/tmp/pytest-240/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -165,7 +166,7 @@ before performing the test function call test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-1306/test_needsfiles0 + /tmp/pytest-240/test_needsfiles0 1 failed in 0.04 seconds Before the test runs, a unique-per-test-invocation temporary directory --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -1,8 +1,4 @@ -""" basic test session implementation. - -* drives collection of tests -* triggers executions of tests -""" +""" core implementation of testing process: init, collection, runtest loop. """ import py import pytest @@ -174,10 +170,10 @@ class Node(object): #: the test config object self.config = config or parent.config - #: the collection this node is part of. + #: the collection this node is part of self.collection = collection or parent.collection - #: the file where this item is contained/collected from. + #: filesystem path where this node was collected from self.fspath = getattr(parent, 'fspath', None) self.ihook = self.collection.gethookproxy(self.fspath) self.keywords = {self.name: True} --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -1,6 +1,4 @@ -""" -automatically discover and run traditional "unittest.py" style tests. -""" +""" support discovery and running of traditional "unittest.py" style tests. """ import py import sys --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -20,5 +20,6 @@ py.test reference documentation mark.txt recwarn.txt unittest.txt + nose.txt doctest.txt --- a/doc/announce/releases.txt +++ /dev/null @@ -1,16 +0,0 @@ -============= -Release notes -============= - -Contents: - -.. toctree:: - :maxdepth: 2 - -.. include: release-1.1.0 -.. include: release-1.0.2 - - release-1.0.1 - release-1.0.0 - release-0.9.2 - release-0.9.0 --- a/pytest/plugin/genscript.py +++ b/pytest/plugin/genscript.py @@ -1,3 +1,4 @@ +""" generate a single-file self-contained version of py.test """ import py import pickle import zlib --- a/pytest/plugin/skipping.py +++ b/pytest/plugin/skipping.py @@ -1,6 +1,4 @@ -""" -plugin providing skip and xfail functionality. -""" +""" support for skip/xfail functions and markers. """ import py, pytest --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -1,5 +1,4 @@ -""" -Implements terminal reporting of the full testing process. +""" terminal reporting of the full testing process. This is a good source for looking at the various reporting hooks. """ --- a/doc/contact.txt +++ b/doc/contact.txt @@ -1,11 +1,13 @@ .. _`contact channels`: +.. _`contact`: Contact channels =================================== -- `new issue tracker`_ to report bugs or suggest features. - See also the `old issue tracker`_ but don't submit bugs there. +- `new issue tracker`_ to report bugs or suggest features (for version + 2.0 and above). You may also peek at the `old issue tracker`_ but please + don't submit bugs there anymore. - `Testing In Python`_: a mailing list for Python testing tools and discussion. --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -1,6 +1,4 @@ -""" -Python related collection nodes. -""" +""" Python test discovery, setup and run of test functions. """ import py import inspect import sys --- a/doc/example/index.txt +++ b/doc/example/index.txt @@ -4,6 +4,9 @@ Usages and Examples =========================================== +This is a (growing) list of examples. :ref:`Contact ` us if you +need more examples or have questions. + .. toctree:: :maxdepth: 2 @@ -14,3 +17,4 @@ Usages and Examples detectpytest.txt nonpython.txt simple.txt + xunit_setup.txt --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -18,12 +18,11 @@ finding out what is collected You can always peek at the collection tree without running tests like this:: - $ py.test --collectonly collectonly.py - - - - - - - - + . $ py.test --collectonly collectonly.py + + + + + + + --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. @@ -65,7 +65,7 @@ release = '2.0.0dev0' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['links.inc', '_build', 'test', 'announce'] # XXX +exclude_patterns = ['links.inc', '_build', 'test', ] # XXX # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -36,12 +36,12 @@ and when running it will see a skipped " $ py.test test_module.py -rs # "-rs" means report on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-195/conftest.py:9: 'need --runslow option to run' + SKIP [1] /tmp/doc-exec-435/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_module.py test_module.py .. --- a/doc/assert.txt +++ b/doc/assert.txt @@ -21,7 +21,7 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_assert1.py test_assert1.py F @@ -101,7 +101,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_assert2.py test_assert2.py F --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -1,5 +1,4 @@ -""" provide version info, conftest/environment config names. -""" +""" version info, help messages, tracing configuration. """ import py import pytest import inspect, sys --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -27,17 +27,17 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_simple.yml - + test_simple.yml .F - + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.37 seconds ==================== + ==================== 1 failed, 1 passed in 0.42 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -47,22 +47,21 @@ your own domain specific testing languag .. note:: ``repr_failure(excinfo)`` is called for representing test failures. - If you create custom collection nodes you can return an error + If you create custom collection nodes you can return an error representation string of your choice. It will be reported as a (red) string. - ``reportinfo()`` -is used for representing the test location and is also consulted for +``reportinfo()`` is used for representing the test location and is also consulted for reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython - - test_simple.yml:1: usecase: ok PASSED - test_simple.yml:1: usecase: hello FAILED - + + test_simple.yml <- test_simple.yml:1: usecase: ok PASSED + test_simple.yml <- test_simple.yml:1: usecase: hello FAILED + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed @@ -74,7 +73,7 @@ While developing your custom test collec interesting to just look at the collection tree:: nonpython $ py.test --collectonly - - - - + + + + --- /dev/null +++ b/doc/announce/index.txt @@ -0,0 +1,22 @@ + +Release announcements +=========================================== + +.. toctree:: + :maxdepth: 2 + + release-2.0.0 + release-1.3.4 + release-1.3.3 + release-1.3.2 + release-1.3.1 + release-1.3.0 + release-1.2.1 + release-1.2.0 + release-1.1.1 + release-1.1.0 + release-1.0.2 + release-1.0.1 + release-1.0.0 + release-0.9.2 + release-0.9.0 From commits-noreply at bitbucket.org Sat Nov 6 19:44:53 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 13:44:53 -0500 (CDT) Subject: [py-svn] pytest commit e7083ff9d8ef: show traces for all hook invocations not just "path/node" based ones Message-ID: <20101106184453.0D9DD2419C5@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289069184 -3600 # Node ID e7083ff9d8efeb354f4b5bdfff77158802a1aa06 # Parent 5f89191fcd0a77727f9c8e5a3a73f8cc2e964e31 show traces for all hook invocations not just "path/node" based ones --- a/pytest/main.py +++ b/pytest/main.py @@ -94,7 +94,6 @@ class PluginManager(object): self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.trace("registered", plugin) if not prepend: self._plugins.append(plugin) else: @@ -380,13 +379,15 @@ class HookCaller: def __call__(self, **kwargs): methods = self.hookrelay._pm.listattr(self.name) - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() + return self._docall(methods, kwargs) def pcall(self, plugins, **kwargs): + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) + return self._docall(methods, kwargs) + + def _docall(self, methods, kwargs): self.trace(self.name, kwargs) self.trace.root.indent += 1 - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) res = mc.execute() if res: From commits-noreply at bitbucket.org Sat Nov 6 20:05:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 14:05:02 -0500 (CDT) Subject: [py-svn] pytest commit 3986b64bdce8: test and fix tracing indentation in case of exceptions Message-ID: <20101106190502.EF3396C111B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289070392 -3600 # Node ID 3986b64bdce8659f16e115c086b9c6dc075fe58c # Parent e7083ff9d8efeb354f4b5bdfff77158802a1aa06 test and fix tracing indentation in case of exceptions --- a/testing/test_main.py +++ b/testing/test_main.py @@ -266,18 +266,26 @@ class TestBootstrapping: l = list(plugins.listattr('x')) assert l == [41, 42, 43] - def test_register_trace(self): + def test_hook_tracing(self): pm = PluginManager() + saveindent = [] class api1: x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pm.trace.root.indent) + raise ValueError(42) l = [] - pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) + pm.trace.root.setwriter(l.append) + indent = pm.trace.root.indent p = api1() pm.register(p) + + assert pm.trace.root.indent == indent assert len(l) == 1 - kw, args = l[0] - assert args[0] == "registered" - assert args[1] == p + assert 'pytest_plugin_registered' in l[0] + py.test.raises(ValueError, lambda: pm.register(api1())) + assert pm.trace.root.indent == indent + assert saveindent[0] > indent class TestPytestPluginInteractions: --- a/pytest/main.py +++ b/pytest/main.py @@ -389,10 +389,12 @@ class HookCaller: self.trace(self.name, kwargs) self.trace.root.indent += 1 mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - res = mc.execute() - if res: - self.trace(res) - self.trace.root.indent -= 1 + try: + res = mc.execute() + if res: + self.trace(res) + finally: + self.trace.root.indent -= 1 return res _preinit = [PluginManager(load=True)] # triggers default plugin importing From commits-noreply at bitbucket.org Sat Nov 6 23:44:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 17:44:50 -0500 (CDT) Subject: [py-svn] pytest commit c6e8dde3a8da: show return values of hooks more explicitely Message-ID: <20101106224450.D08866C1425@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289070765 -3600 # Node ID c6e8dde3a8da522120ea8259772da3d5e3f27217 # Parent 3986b64bdce8659f16e115c086b9c6dc075fe58c show return values of hooks more explicitely --- a/pytest/main.py +++ b/pytest/main.py @@ -392,7 +392,7 @@ class HookCaller: try: res = mc.execute() if res: - self.trace(res) + self.trace("finish", self.name, "-->", res) finally: self.trace.root.indent -= 1 return res From commits-noreply at bitbucket.org Sat Nov 6 23:44:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 17:44:50 -0500 (CDT) Subject: [py-svn] pytest commit 809c2914a73d: introduce an option that avoids discovery of classes other than unittest.TestCase in modules Message-ID: <20101106224450.C3E8F6C133E@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289083548 -3600 # Node ID 809c2914a73dd1604f9188842f13f9cb8f42a2ff # Parent c75f441489d31835abeeabd20c0f6cfd7441b269 introduce an option that avoids discovery of classes other than unittest.TestCase in modules importing unittest. --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -294,6 +294,7 @@ class TestInvocationVariants: assert not retcode out, err = capsys.readouterr() assert "--help" in out + pytest.raises(ValueError, lambda: pytest.main(retcode)) def test_invoke_with_path(self, testdir, capsys): retcode = testdir.pytestmain(testdir.tmpdir) @@ -330,3 +331,18 @@ class TestInvocationVariants: result.stderr.fnmatch_lines([ "ERROR*file*or*package*not*found*", ]) + + + def test_noclass_discovery_if_not_testcase(self, testdir): + testpath = testdir.makepyfile(""" + import unittest + import py + class TestHello(object): + def test_hello(self): + assert self.attr + + class RealTest(TestHello, unittest.TestCase): + attr = 42 + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=1) --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -59,6 +59,8 @@ def pytest_pycollect_makeitem(__multical if res is not None: return res if collector._istestclasscandidate(name, obj): + if hasattr(collector.obj, 'unittest'): + return # we assume it's a mixin class for a TestCase derived one return Class(name, parent=collector) elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): if is_generator(obj): @@ -124,6 +126,7 @@ class PyobjMixin(object): return self._fslineno def reportinfo(self): + # XXX caching? obj = self.obj if hasattr(obj, 'compat_co_firstlineno'): # nose compatibility --- a/testing/plugin/test_assertion.py +++ b/testing/plugin/test_assertion.py @@ -161,6 +161,15 @@ def test_functional(testdir): result = testdir.runpytest("--no-assert") assert "3 == 4" not in result.stdout.str() +def test_AssertionErrorIdentity(testdir): + testdir.makepyfile(""" + def test_hello(): + import exceptions + assert AssertionError is exceptions.AssertionError + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" def test_hello(): --- a/tox.ini +++ b/tox.ini @@ -52,5 +52,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -addargs=-rfx +addopts=-rfx --pyargs rsyncdirs=pytest testing + --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -1,12 +1,13 @@ -""" support discovery and running of traditional "unittest.py" style tests. """ +""" discovery and running of std-library "unittest" style tests. """ import py import sys def pytest_pycollect_makeitem(collector, name, obj): - if 'unittest' not in sys.modules: - return # nobody derived unittest.TestCase + unittest = sys.modules.get('unittest') + if unittest is None: + return # nobody can have derived unittest.TestCase try: - isunit = issubclass(obj, py.std.unittest.TestCase) + isunit = issubclass(obj, unittest.TestCase) except KeyboardInterrupt: raise except Exception: --- a/pytest/plugin/assertion.py +++ b/pytest/plugin/assertion.py @@ -3,6 +3,7 @@ support for presented detailed informati """ import py import sys +from pytest.plugin.monkeypatch import monkeypatch def pytest_addoption(parser): group = parser.getgroup("debugconfig") @@ -15,25 +16,21 @@ def pytest_configure(config): # py._code._assertionnew to detect this plugin was loaded and in # turn call the hooks defined here as part of the # DebugInterpreter. + config._monkeypatch = m = monkeypatch() if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() - config._oldassertion = py.builtin.builtins.AssertionError - config._oldbinrepr = py.code._reprcompare - py.builtin.builtins.AssertionError = py.code._AssertionError def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) for new_expl in hook_result: if new_expl: return '\n~'.join(new_expl) - py.code._reprcompare = callbinrepr + m.setattr(py.builtin.builtins, + 'AssertionError', py.code._AssertionError) + m.setattr(py.code, '_reprcompare', callbinrepr) def pytest_unconfigure(config): - if hasattr(config, '_oldassertion'): - py.builtin.builtins.AssertionError = config._oldassertion - py.code._reprcompare = config._oldbinrepr - del config._oldassertion - del config._oldbinrepr + config._monkeypatch.undo() def warn_about_missing_assertion(): try: --- a/pytest/main.py +++ b/pytest/main.py @@ -403,7 +403,11 @@ def main(args=None, plugins=None): if args is None: args = sys.argv[1:] elif not isinstance(args, (tuple, list)): - args = py.std.shlex.split(str(args)) + if isinstance(args, py.path.local): + args = str(args) + if not isinstance(args, str): + raise ValueError("not a string or argument list: %r" % (args,)) + args = py.std.shlex.split(args) if _preinit: _pluginmanager = _preinit.pop(0) else: # subsequent calls to main will create a fresh instance From commits-noreply at bitbucket.org Sat Nov 6 23:44:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 17:44:50 -0500 (CDT) Subject: [py-svn] pytest commit c75f441489d3: introduce new --testpkg importpath option, add more meat to draft release announcement Message-ID: <20101106224450.B089C6C111B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289078253 -3600 # Node ID c75f441489d31835abeeabd20c0f6cfd7441b269 # Parent c6e8dde3a8da522120ea8259772da3d5e3f27217 introduce new --testpkg importpath option, add more meat to draft release announcement --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -309,3 +309,24 @@ class TestInvocationVariants: out, err = capsys.readouterr() assert "--myopt" in out + def test_cmdline_python_package(self, testdir): + path = testdir.mkpydir("tpkg") + path.join("test_hello.py").write("def test_hello(): pass") + path.join("test_world.py").write("def test_world(): pass") + result = testdir.runpytest("--pyargs", "tpkg") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + result = testdir.runpytest("--pyargs", "tpkg.test_hello") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_cmdline_python_package_not_exists(self, testdir): + result = testdir.runpytest("--pyargs", "tpkgwhatv") + assert result.ret + result.stderr.fnmatch_lines([ + "ERROR*file*or*package*not*found*", + ]) --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -9,8 +9,8 @@ cutdir = py.path.local(pytest.__file__). def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group._addoption('--funcargs', + group = parser.getgroup("general") + group.addoption('--funcargs', action="store_true", dest="showfuncargs", default=False, help="show available function arguments, sorted by plugin") --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev19' +__version__ = '2.0.0.dev20' __all__ = ['config', 'cmdline'] --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -31,6 +31,8 @@ def pytest_addoption(parser): group.addoption('--collectonly', action="store_true", dest="collectonly", help="only collect tests, don't execute them."), + group.addoption('--pyargs', action="store_true", + help="try to interpret all arguments as python packages.") group.addoption("--ignore", action="append", metavar="path", help="ignore path during collection (multi-allowed).") group.addoption('--confcutdir', dest="confcutdir", default=None, @@ -429,12 +431,29 @@ class Collection(FSCollector): ihook.pytest_collect_directory(path=path, parent=self) return True + def _tryconvertpyarg(self, x): + try: + mod = __import__(x, None, None, ['__doc__']) + except ImportError: + return x + p = py.path.local(mod.__file__) + if p.purebasename == "__init__": + p = p.dirpath() + return p + def _parsearg(self, arg): """ return (fspath, names) tuple after checking the file exists. """ + arg = str(arg) + if self.config.option.pyargs: + arg = self._tryconvertpyarg(arg) parts = str(arg).split("::") path = self.fspath.join(parts[0], abs=True) if not path.check(): - raise pytest.UsageError("file not found: %s" %(path,)) + if self.config.option.pyargs: + msg = "file or package not found: " + else: + msg = "file not found: " + raise pytest.UsageError(msg + arg) parts[0] = path return parts --- a/doc/usage.txt +++ b/doc/usage.txt @@ -25,39 +25,18 @@ To stop the testing process after the fi py.test -x # stop after first failure py.test -maxfail=2 # stop after two failures -calling pytest from Python code ----------------------------------------------------- +specifying tests / selecting tests +--------------------------------------------------- -.. versionadded: 2.0 +Several test run options:: -You can invoke ``py.test`` from Python code directly:: + py.test test_mod.py # run tests in module + py.test somepath # run all tests below path + py.test -k string # only run tests whose names contain a string - pytest.main() +Import 'pkg' and use its filesystem location to find and run tests:: -this acts as if you would call "py.test" from the command line. -It will not raise ``SystemExit`` but return the exitcode instead. -You can pass in options and arguments:: - - pytest.main(['x', 'mytestdir']) - -or pass in a string:: - - pytest.main("-x mytestdir") - -You can specify additional plugins to ``pytest.main``:: - - # content of myinvoke.py - import pytest - class MyPlugin: - def pytest_addoption(self, parser): - raise pytest.UsageError("hi from our plugin") - - pytest.main(plugins=[MyPlugin()]) - -Running it will exit quickly:: - - $ python myinvoke.py - ERROR: hi from our plugin + py.test --testpkg=pypkg # run all tests found below directory of pypkg calling pytest through ``python -m pytest`` ----------------------------------------------------- @@ -162,4 +141,39 @@ for example ``-x`` if you only want to s Currently only pasting to the http://paste.pocoo.org service is implemented. +calling pytest from Python code +---------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke ``py.test`` from Python code directly:: + + pytest.main() + +this acts as if you would call "py.test" from the command line. +It will not raise ``SystemExit`` but return the exitcode instead. +You can pass in options and arguments:: + + pytest.main(['x', 'mytestdir']) + +or pass in a string:: + + pytest.main("-x mytestdir") + +You can specify additional plugins to ``pytest.main``:: + + # content of myinvoke.py + import pytest + class MyPlugin: + def pytest_addoption(self, parser): + raise pytest.UsageError("hi from our plugin") + + pytest.main(plugins=[MyPlugin()]) + +Running it will exit quickly:: + + $ python myinvoke.py + ERROR: hi from our plugin + + .. include:: links.inc --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev19', + version='2.0.0.dev20', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -3,14 +3,24 @@ py.test 2.0.0: standalone, features++, i XXX PENDING -Welcome to pytest-2.0.0! With this release py.test becomes its own standalone -PyPI distribution, named ``pytest``, installing the ``py.test`` command line -tool. Apart from a great internal cleanup this release comes with tons -of improvements and new features and a completely revamped extensive -documentation, including many continously tested examples. See +Welcome to pytest-2.0.0, rapid and easy testing for and with Python. +py.test now comes as its own PyPI distribution named ``pytest`` which +installs the ``py.test`` tool. It removes most long-deprecated code, +providing for a much smaller and easier to understand code base. There +are also many new features and much improved documentation. See http://pytest.org +for details or below for some more information. + +Thanks to all issue reporters and people asking questions or +complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt +for their great coding contributions. + +best, +holger + + New Features ----------------------- @@ -20,31 +30,117 @@ New Features python -m pytest.main # on all pythons >= 2.5 import pytest ; pytest.main(args, plugins) - see http://pytest.org/2.0.0/invoke.html for details. + see http://pytest.org/2.0.0/usage.html for details. + +- new and better reporting information in assert expressions + which compare lists, sequences or strings. + + see http://pytest.org/2.0.0/assert.html for details. - new configuration through ini-files (setup.cfg or tox.ini recognized), for example:: [pytest] - norecursedirs = .hg _build - python_collect_funcprefix = test_ - python_collect_classprefix = Test + norecursedirs = .hg data* # don't ever recurse in such dirs + addopts = -x --pyargs # add these options by default see http://pytest.org/2.0.0/customize.html -- +- improved standard unittest support. For example you can now run + the tests of an installed 'unittest' package with py.test:: -Thanks to issue reporters, people asking questions, complaining and -generally to Ronny Pfannschmidt for his awesome help on many issues. + py.test --pyargs unittest -cheers, -holger krekel +- add a new "-q" option which decreases verbosity and prints a more + nose/unittest-style "dot" output. -Changes between 1.3.3 and 1.3.4 +Fixes +----------------------- + +- fix issue126 - introduce py.test.set_trace() to trace execution via + PDB during the running of tests even if capturing is ongoing. +- fix issue124 - make reporting more resilient against tests opening + files on filedescriptor 1 (stdout). +- fix issue109 - sibling conftest.py files will not be loaded. + (and Directory collectors cannot be customized anymore from a Directory's + conftest.py - this needs to happen at least one level up). +- fix issue88 (finding custom test nodes from command line arg) +- fix issue93 stdout/stderr is captured while importing conftest.py +- fix bug: unittest collected functions now also can have "pytestmark" + applied at class/module level + +Important Note on importing "pytest" versus "py.test" +------------------------------------------------------- + +The usual way in pre-2.0 times to use py.test in python code was +to import "py" and then e.g. use "py.test.raises" for the helper. +This remains valid and is not planned to be deprecated. However, +in most examples and internal code you'll find "import pytest" +and "pytest.raises" used as the recommended default way. + +(Incompatible) Removals +----------------------------- + +- py.test.config is now only available if you are in a test run. + +- the following (mostly already deprecated) functionality was removed: + - removed support for Module/Class/... collection node definitions + in conftest.py files. They will cause nothing special. + - removed support for calling the pre-1.0 collection API of "run()" and "join" + - removed reading option values from conftest.py files or env variables. + This can now be done much much better and easier through the ini-file + mechanism and the "addopts" entry in particular. + - removed the "disabled" attribute in test classes. Use the skipping + and pytestmark mechanism to skip or xfail a test class. + +- py.test.collect.Directory does not exist anymore and it + is not possible to provide an own "Directory" object. + If you have used this and don#t know what to do, get + in contact. We'll figure someting out. + + Note that pytest_collect_directory() is still called but + any return value will be ignored. This allows to keep + old code working that performed for example "py.test.skip()" + in collect() to prevent recursion into directory trees + if a certain dependency or command line option is missing. + + +More Detailed Changes between 1.3.4 and 2.0.0 ================================================== -- fix issue111: improve install documentation for windows -- fix issue119: fix custom collectability of __init__.py as a module -- fix issue116: --doctestmodules work with __init__.py files as well -- fix issue115: unify internal exception passthrough/catching/GeneratorExit -- fix issue118: new --tb=native for presenting cpython-standard exceptions +- pytest-2.0 is now its own package and depends on pylib-2.0 +- new ability: python -m pytest / python -m pytest.main ability +- new python invcation: pytest.main(args, plugins) to load + some custom plugins early. +- try harder to run unittest test suites in a more compatible manner + by deferring setup/teardown semantics to the unittest package. +- introduce a new way to set config options via ini-style files, + by default setup.cfg and tox.ini files are searched. The old + ways (certain environment variables, dynamic conftest.py reading + is removed). +- add a new "-q" option which decreases verbosity and prints a more + nose/unittest-style "dot" output. +- fix issue126 - introduce py.test.set_trace() to trace execution via + PDB during the running of tests even if capturing is ongoing. +- fix issue123 - new "python -m py.test" invocation for py.test + (requires Python 2.5 or above) +- fix issue124 - make reporting more resilient against tests opening + files on filedescriptor 1 (stdout). +- fix issue109 - sibling conftest.py files will not be loaded. + (and Directory collectors cannot be customized anymore from a Directory's + conftest.py - this needs to happen at least one level up). +- introduce (customizable) assertion failure representations and enhance + output on assertion failures for comparisons and other cases (Floris Bruynooghe) +- nose-plugin: pass through type-signature failures in setup/teardown + functions instead of not calling them (Ed Singleton) +- remove py.test.collect.Directory (follows from a major refactoring + and simplification of the collection process) +- majorly reduce py.test core code, shift function/python testing to own plugin +- fix issue88 (finding custom test nodes from command line arg) +- refine 'tmpdir' creation, will now create basenames better associated + with test names (thanks Ronny) +- "xpass" (unexpected pass) tests don't cause exitcode!=0 +- fix issue131 / issue60 - importing doctests in __init__ files used as namespace packages +- fix issue93 stdout/stderr is captured while importing conftest.py +- fix bug: unittest collected functions now also can have "pytestmark" + applied at class/module level From commits-noreply at bitbucket.org Sun Nov 7 00:21:06 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 18:21:06 -0500 (CDT) Subject: [py-svn] pytest commit 1df9ce97afeb: some fixes for --pyargs situations and the docs, remove wrongly added test Message-ID: <20101106232106.EF0961E1178@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289085736 -3600 # Node ID 1df9ce97afeb503b2114f2de77684f7e88297537 # Parent 809c2914a73dd1604f9188842f13f9cb8f42a2ff some fixes for --pyargs situations and the docs, remove wrongly added test --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -310,7 +310,8 @@ class TestInvocationVariants: out, err = capsys.readouterr() assert "--myopt" in out - def test_cmdline_python_package(self, testdir): + def test_cmdline_python_package(self, testdir, monkeypatch): + monkeypatch.delenv('PYTHONDONTWRITEBYTECODE') path = testdir.mkpydir("tpkg") path.join("test_hello.py").write("def test_hello(): pass") path.join("test_world.py").write("def test_world(): pass") --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -12,6 +12,18 @@ You can set the :confval:`norecursedirs` This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. +always try to interpret arguments as Python packages +----------------------------------------------------- + +You can use the ``--pyargs`` option to make py.test try +interpreting arguments as python package names, deriving +their file system path and then running the test. Through +an ini-file and the :confval:`addopts` option you can make +this change more permanently:: + + # content of setup.cfg or tox.ini + [pytest] + addopts = --pyargs finding out what is collected ----------------------------------------------- --- a/testing/plugin/test_assertion.py +++ b/testing/plugin/test_assertion.py @@ -161,15 +161,6 @@ def test_functional(testdir): result = testdir.runpytest("--no-assert") assert "3 == 4" not in result.stdout.str() -def test_AssertionErrorIdentity(testdir): - testdir.makepyfile(""" - def test_hello(): - import exceptions - assert AssertionError is exceptions.AssertionError - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*1 passed*"]) - def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" def test_hello(): --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -366,9 +366,10 @@ class Collection(FSCollector): self.trace.root.indent += 1 self._notfound = [] self._initialpaths = set() - self._initialargs = args + self._initialparts = [] for arg in args: parts = self._parsearg(arg) + self._initialparts.append(parts) self._initialpaths.add(parts[0]) self.ihook.pytest_collectstart(collector=self) rep = self.ihook.pytest_make_collect_report(collector=self) @@ -376,7 +377,7 @@ class Collection(FSCollector): self.trace.root.indent -= 1 if self._notfound: for arg, exc in self._notfound: - line = "no name %r in any of %r" % (exc.args[1], exc.args[0]) + line = "(no name %r in any of %r)" % (arg, exc.args[0]) raise pytest.UsageError("not found: %s\n%s" %(arg, line)) if not genitems: return rep.result @@ -388,8 +389,9 @@ class Collection(FSCollector): return items def collect(self): - for arg in self._initialargs: - self.trace("processing arg", arg) + for parts in self._initialparts: + arg = "::".join(map(str, parts)) + self.trace("processing argument", arg) self.trace.root.indent += 1 try: for x in self._collect(arg): @@ -417,8 +419,9 @@ class Collection(FSCollector): def _collectfile(self, path): ihook = self.gethookproxy(path) - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () return ihook.pytest_collect_file(path=path, parent=self) def _recurse(self, path): @@ -439,6 +442,8 @@ class Collection(FSCollector): p = py.path.local(mod.__file__) if p.purebasename == "__init__": p = p.dirpath() + else: + p = p.new(basename=p.purebasename+".py") return p def _parsearg(self, arg): --- a/doc/usage.txt +++ b/doc/usage.txt @@ -36,7 +36,7 @@ Several test run options:: Import 'pkg' and use its filesystem location to find and run tests:: - py.test --testpkg=pypkg # run all tests found below directory of pypkg + py.test --pyargs pkg # run all tests found below directory of pypkg calling pytest through ``python -m pytest`` ----------------------------------------------------- --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -213,6 +213,19 @@ class TestCustomConftests: assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) + def test_ignore_collect_not_called_on_argument(self, testdir): + testdir.makeconftest(""" + def pytest_ignore_collect(path, config): + return True + """) + p = testdir.makepyfile("def test_hello(): pass") + result = testdir.runpytest(p) + assert result.ret == 0 + assert "1 passed" in result.stdout.str() + result = testdir.runpytest() + assert result.ret == 0 + assert "1 passed" not in result.stdout.str() + def test_collectignore_exclude_on_option(self, testdir): testdir.makeconftest(""" collect_ignore = ['hello', 'test_world.py'] From commits-noreply at bitbucket.org Sun Nov 7 00:33:42 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 18:33:42 -0500 (CDT) Subject: [py-svn] pytest commit 43dccff20598: replace with list comp Message-ID: <20101106233342.7EA136C1304@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289086440 18000 # Node ID 43dccff205987b4737e5d8c4fa077224f38b78c0 # Parent 1df9ce97afeb503b2114f2de77684f7e88297537 replace with list comp --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -376,7 +376,7 @@ class Config(object): elif type == "args": return py.std.shlex.split(value) elif type == "linelist": - return filter(None, map(lambda x: x.strip(), value.split("\n"))) + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] else: assert type is None return value From commits-noreply at bitbucket.org Sun Nov 7 00:36:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 18:36:05 -0500 (CDT) Subject: [py-svn] pytest commit 8dad8f0e7649: PYTHONDONTWRITEBYTECODE might not be set Message-ID: <20101106233605.007956C1304@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289086584 18000 # Node ID 8dad8f0e7649d22b6ffae74c9a806324460c871b # Parent 43dccff205987b4737e5d8c4fa077224f38b78c0 PYTHONDONTWRITEBYTECODE might not be set --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -311,7 +311,7 @@ class TestInvocationVariants: assert "--myopt" in out def test_cmdline_python_package(self, testdir, monkeypatch): - monkeypatch.delenv('PYTHONDONTWRITEBYTECODE') + monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False) path = testdir.mkpydir("tpkg") path.join("test_hello.py").write("def test_hello(): pass") path.join("test_world.py").write("def test_world(): pass") From commits-noreply at bitbucket.org Sun Nov 7 01:08:44 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 19:08:44 -0500 (CDT) Subject: [py-svn] pytest commit 0ce9db200a51: some python3 related fixes Message-ID: <20101107000844.E0DC72410C8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289088615 -3600 # Node ID 0ce9db200a51ce2de73ba3b2a009c08b975a058d # Parent 8dad8f0e7649d22b6ffae74c9a806324460c871b some python3 related fixes --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -325,6 +325,11 @@ class TestInvocationVariants: result.stdout.fnmatch_lines([ "*1 passed*" ]) + result = testdir.runpytest("--pyargs", ".") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) def test_cmdline_python_package_not_exists(self, testdir): result = testdir.runpytest("--pyargs", "tpkgwhatv") @@ -342,7 +347,7 @@ class TestInvocationVariants: def test_hello(self): assert self.attr - class RealTest(TestHello, unittest.TestCase): + class RealTest(unittest.TestCase, TestHello): attr = 42 """) reprec = testdir.inline_run(testpath) --- a/testing/test_config.py +++ b/testing/test_config.py @@ -40,6 +40,17 @@ class TestParseIni: "*tox.ini:2*requires*9.0*actual*" ]) + @py.test.mark.xfail(reason="probably not needed") + def test_confcutdir(self, testdir): + sub = testdir.mkdir("sub") + sub.chdir() + testdir.makeini(""" + [pytest] + addopts = --qwe + """) + result = testdir.runpytest("--confcutdir=.") + assert result.ret == 0 + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): config = testdir.reparseconfig([testdir.tmpdir]) --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -437,7 +437,7 @@ class Collection(FSCollector): def _tryconvertpyarg(self, x): try: mod = __import__(x, None, None, ['__doc__']) - except ImportError: + except (ValueError, ImportError): return x p = py.path.local(mod.__file__) if p.purebasename == "__init__": --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -33,6 +33,10 @@ class UnitTestCase(py.test.collect.Class meth() class TestCaseFunction(py.test.collect.Function): + def setup(self): + pass + def teardown(self): + pass def startTest(self, testcase): pass def addError(self, testcase, rawexcinfo): --- a/pytest/main.py +++ b/pytest/main.py @@ -68,8 +68,11 @@ class PluginManager(object): self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): err = sys.stderr - if hasattr(os, 'dup'): - err = py.io.dupfile(err) + encoding = getattr(err, 'encoding', 'utf8') + try: + err = py.io.dupfile(err, encoding=encoding) + except Exception: + pass self.trace.root.setwriter(err.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -496,8 +496,6 @@ class Test_getinitialnodes: config = testdir.reparseconfig([x]) col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) - print col.obj - print col.listchain() assert col.name == 'subdir/x.py' assert col.parent.parent is None for col in col.listchain(): From commits-noreply at bitbucket.org Sun Nov 7 01:12:07 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 19:12:07 -0500 (CDT) Subject: [py-svn] pytest commit 98575d7ad93e: fix bug showing up on windows Message-ID: <20101107001207.94CEF1E12AA@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289088820 -3600 # Node ID 98575d7ad93e5bce342be4f64089798b73a61ed1 # Parent 0ce9db200a51ce2de73ba3b2a009c08b975a058d fix bug showing up on windows --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -323,7 +323,7 @@ class FSCollector(Collector): return "." relpath = self.collection.fspath.bestrelpath(self.fspath) if os.sep != "/": - relpath = str(path).replace(os.sep, "/") + relpath = relpath.replace(os.sep, "/") return relpath class File(FSCollector): From commits-noreply at bitbucket.org Sun Nov 7 07:13:18 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 01:13:18 -0500 (CDT) Subject: [py-svn] pytest commit c6eebf5ff629: fix test, bump version Message-ID: <20101107061318.E2BAD2410C8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289110490 -3600 # Node ID c6eebf5ff629d2eb8f5ad3e1192f90f5ff1e834f # Parent 98575d7ad93e5bce342be4f64089798b73a61ed1 fix test, bump version --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev20', + version='2.0.0.dev21', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -273,6 +273,7 @@ class TestInvocationVariants: res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) assert res.ret == 1 + @py.test.mark.skipif("sys.version_info < (2,5)") def test_python_pytest_main(self, testdir): p1 = testdir.makepyfile("def test_pass(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev20' +__version__ = '2.0.0.dev21' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Sun Nov 7 09:04:24 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 02:04:24 -0600 (CST) Subject: [py-svn] pytest commit 2bffb78b3c42: bump version Message-ID: <20101107080424.3517A6C1304@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289117154 -3600 # Node ID 2bffb78b3c42f40f963bc62f5095f01b75465b68 # Parent c6eebf5ff629d2eb8f5ad3e1192f90f5ff1e834f bump version --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev21', + version='2.0.0.dev22', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev21' +__version__ = '2.0.0.dev22' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Sun Nov 7 10:23:55 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 03:23:55 -0600 (CST) Subject: [py-svn] pytest commit 6a5bbc78bad9: probably the last major internal cleanup action: rename collection to Message-ID: <20101107092355.1F7E71E135A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289121598 -3600 # Node ID 6a5bbc78bad973e969317db8b86f151e6539c42e # Parent 2bffb78b3c42f40f963bc62f5095f01b75465b68 probably the last major internal cleanup action: rename collection to session which now is the root collection node. This means that session, collection and config objects have a more defined relationship (previously there was no way to get from a collection node or even from a runtest hook to the session object which was strange). --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev22' +__version__ = '2.0.0.dev23' __all__ = ['config', 'cmdline'] --- a/doc/example/builtin.txt +++ b/doc/example/builtin.txt @@ -27,10 +27,10 @@ Let's run our little function:: F ================================= FAILURES ================================= ______________________________ test_something ______________________________ - + def test_something(): > checkconfig(42) E Failed: not configured: 42 - + test_checkconfig.py:8: Failed 1 failed in 0.02 seconds --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -34,7 +34,7 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_simplefactory.py test_simplefactory.py F @@ -136,7 +136,7 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_example.py test_example.py .........F @@ -158,7 +158,6 @@ the test collection phase which is separ Let's just look at what is collected:: $ py.test --collectonly test_example.py - @@ -175,7 +174,7 @@ If you want to select only the run with $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 -- /home/hpk/venv/0/bin/python test path 1: test_example.py test_example.py <- test_example.py:6: test_func[7] PASSED --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,20 +88,20 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-407 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-527 test_mark.py .. test_mark_classlevel.py .. - ========================= 4 passed in 0.01 seconds ========================= + ========================= 4 passed in 0.02 seconds ========================= And you can also run all tests except the ones that match the keyword:: $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-407 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-527 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-407 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-527 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -24,7 +24,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_unittest.py test_unittest.py F @@ -56,7 +56,7 @@ Running it yields:: /usr/lib/python2.6/unittest.py:350: AssertionError ----------------------------- Captured stdout ------------------------------ hello - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.12 seconds ========================= .. _`unittest.py style`: http://docs.python.org/library/unittest.html --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -28,7 +28,7 @@ Running this would result in a passed te $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_tmpdir.py test_tmpdir.py F @@ -36,7 +36,7 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-243/test_create_file0') + tmpdir = local('/tmp/pytest-447/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -47,7 +47,7 @@ Running this would result in a passed te E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.04 seconds ========================= + ========================= 1 failed in 0.15 seconds ========================= .. _`base temporary directory`: --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -244,7 +244,7 @@ class TestExecutionForked(BaseFunctional assert rep.failed assert rep.when == "???" -class TestCollectionReports: +class TestSessionReports: def test_collect_result(self, testdir): col = testdir.getmodulecol(""" def test_func1(): --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -516,7 +516,7 @@ def test_traceconfig(testdir, monkeypatc def test_debug(testdir, monkeypatch): result = testdir.runpytest("--debug") result.stderr.fnmatch_lines([ - "*registered*session*", + "*pytest_sessionstart*session*", ]) assert result.ret == 0 --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -130,7 +130,7 @@ let's run the full monty:: E assert 4 < 4 test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.02 seconds + 1 failed, 4 passed in 0.03 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,7 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-400 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-519 ============================= in 0.00 seconds ============================= --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,14 +122,14 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-438/conftest.py:22: specify ssh host with --ssh + SKIP [1] /tmp/doc-exec-560/conftest.py:22: specify ssh host with --ssh - ======================== 1 skipped in 0.02 seconds ========================= + ======================== 1 skipped in 0.03 seconds ========================= If you specify a command line option like ``py.test --ssh=python.org`` the test will execute as expected. --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -16,7 +16,7 @@ conftest.py: local per-directory plugins -------------------------------------------------------------- local ``conftest.py`` plugins contain directory-specific hook -implementations. Collection and test running activities will +implementations. Session and test running activities will invoke all hooks defined in "higher up" ``conftest.py`` files. Example: Assume the following layout and content of files:: @@ -268,7 +268,7 @@ you can use the following hook: reporting hooks ------------------------------ -Collection related reporting hooks: +Session related reporting hooks: .. autofunction: pytest_collectstart .. autofunction: pytest_itemcollected --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev22', + version='2.0.0.dev23', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-408 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-528 ============================= in 0.00 seconds ============================= --- a/doc/faq.txt +++ b/doc/faq.txt @@ -12,11 +12,11 @@ On naming, nosetests, licensing and magi Why a ``py.test`` instead of a ``pytest`` command? ++++++++++++++++++++++++++++++++++++++++++++++++++ -Some historic, some practical reasons: ``py.test`` used to be part of +Some historic, some practical reasons: ``py.test`` used to be part of the ``py`` package which provided several developer utitilities, all starting with ``py.``, providing nice TAB-completion. If you install ``pip install pycmd`` you get these tools from a separate -package. These days the command line tool could be ``pytest`` +package. These days the command line tool could be ``pytest`` but then many people have gotten used to the old name and there also is another tool with this same which would lead to some clashes. @@ -37,11 +37,11 @@ What's this "magic" with py.test? Around 2007 (version ``0.8``) some several people claimed that py.test was using too much "magic". It has been refactored a lot. It is today -probably one of the smallest, most universally runnable and most -customizable testing frameworks for Python. It remains true -that ``py.test`` uses metaprogramming techniques, i.e. it views -test code similar to how compilers view programs, using a -somewhat abstract internal model. +probably one of the smallest, most universally runnable and most +customizable testing frameworks for Python. It remains true +that ``py.test`` uses metaprogramming techniques, i.e. it views +test code similar to how compilers view programs, using a +somewhat abstract internal model. It's also true that the no-boilerplate testing is implemented by making use of the Python assert statement through "re-interpretation": @@ -64,7 +64,7 @@ function arguments, parametrized tests a Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -For simple applications and for people experienced with nose_ or +For simple applications and for people experienced with nose_ or unittest-style test setup using `xUnit style setup`_ feels natural. For larger test suites, parametrized testing or setup of complex test resources using funcargs_ is recommended. --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -14,7 +14,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.0.0.dev19, imported from /home/hpk/p/pytest/pytest + This is py.test version 2.0.0.dev22, imported from /home/hpk/p/pytest/pytest If you get an error checkout :ref:`installation issues`. @@ -34,8 +34,8 @@ That's it. You can execute the test func $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-404 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-523 test_sample.py F @@ -121,7 +121,7 @@ run the module by passing its filename:: ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -129,7 +129,7 @@ run the module by passing its filename:: E assert hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.02 seconds + 1 failed, 1 passed in 0.03 seconds The first test passed, the second failed. Again we can easily see the intermediate values used in the assertion, helping us to @@ -157,7 +157,7 @@ before performing the test function call ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-240/test_needsfiles0') + tmpdir = local('/tmp/pytest-446/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -166,8 +166,8 @@ before performing the test function call test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-240/test_needsfiles0 - 1 failed in 0.04 seconds + /tmp/pytest-446/test_needsfiles0 + 1 failed in 0.07 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -1,4 +1,4 @@ -""" core implementation of testing process: init, collection, runtest loop. """ +""" core implementation of testing process: init, session, runtest loop. """ import py import pytest @@ -54,7 +54,7 @@ def pytest_configure(config): config.option.maxfail = 1 def pytest_cmdline_main(config): - """ default command line protocol for initialization, collection, + """ default command line protocol for initialization, session, running tests and reporting. """ session = Session(config) session.exitstatus = EXIT_OK @@ -83,20 +83,17 @@ def pytest_cmdline_main(config): return session.exitstatus def pytest_collection(session): - collection = session.collection - assert not hasattr(collection, 'items') - - collection.perform_collect() + session.perform_collect() hook = session.config.hook - items = collection.items - hook.pytest_collection_modifyitems(config=session.config, items=items) - hook.pytest_collection_finish(collection=collection) + hook.pytest_collection_modifyitems(session=session, + config=session.config, items=session.items) + hook.pytest_collection_finish(session=session) return True def pytest_runtestloop(session): if session.config.option.collectonly: return True - for item in session.collection.items: + for item in session.session.items: item.config.hook.pytest_runtest_protocol(item=item) if session.shouldstop: raise session.Interrupted(session.shouldstop) @@ -121,7 +118,7 @@ class Session(object): self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False - self.collection = Collection(config) # XXX move elswehre + self.session = Session(config) # XXX move elswehre def pytest_collectstart(self): if self.shouldstop: @@ -154,7 +151,7 @@ def compatproperty(name): def fget(self): #print "retrieving %r property from %s" %(name, self.fspath) py.log._apiwarn("2.0", "use py.test.collect.%s for " - "Collection classes" % name) + "Session classes" % name) return getattr(pytest.collect, name) return property(fget) @@ -162,7 +159,7 @@ class Node(object): """ base class for all Nodes in the collection tree. Collector subclasses have children, Items are terminal nodes.""" - def __init__(self, name, parent=None, config=None, collection=None): + def __init__(self, name, parent=None, config=None, session=None): #: a unique name with the scope of the parent self.name = name @@ -173,11 +170,11 @@ class Node(object): self.config = config or parent.config #: the collection this node is part of - self.collection = collection or parent.collection + self.session = session or parent.session #: filesystem path where this node was collected from self.fspath = getattr(parent, 'fspath', None) - self.ihook = self.collection.gethookproxy(self.fspath) + self.ihook = self.session.gethookproxy(self.fspath) self.keywords = {self.name: True} Module = compatproperty("Module") @@ -312,16 +309,16 @@ class Collector(Node): excinfo.traceback = ntraceback.filter() class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None, collection=None): + def __init__(self, fspath, parent=None, config=None, session=None): fspath = py.path.local(fspath) # xxx only for test_resultlog.py? name = parent and fspath.relto(parent.fspath) or fspath.basename - super(FSCollector, self).__init__(name, parent, config, collection) + super(FSCollector, self).__init__(name, parent, config, session) self.fspath = fspath def _makeid(self): - if self == self.collection: + if self == self.session: return "." - relpath = self.collection.fspath.bestrelpath(self.fspath) + relpath = self.session.fspath.bestrelpath(self.fspath) if os.sep != "/": relpath = relpath.replace(os.sep, "/") return relpath @@ -346,13 +343,33 @@ class Item(Node): self._location = location return location -class Collection(FSCollector): +class Session(FSCollector): + class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + __module__ = 'builtins' # for py3 + def __init__(self, config): - super(Collection, self).__init__(py.path.local(), parent=None, - config=config, collection=self) + super(Session, self).__init__(py.path.local(), parent=None, + config=config, session=self) + self.config.pluginmanager.register(self, name="session", prepend=True) + self._testsfailed = 0 + self.shouldstop = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") + def pytest_collectstart(self): + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + def pytest_runtest_logreport(self, report): + if report.failed and 'xfail' not in getattr(report, 'keywords', []): + self._testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self._testsfailed >= maxfail: + self.shouldstop = "stopping after %d failures" % ( + self._testsfailed) + pytest_collectreport = pytest_runtest_logreport + def isinitpath(self, path): return path in self._initialpaths @@ -509,3 +526,4 @@ class Collection(FSCollector): yield x node.ihook.pytest_collectreport(report=rep) +Session = Session --- a/doc/usage.txt +++ b/doc/usage.txt @@ -175,5 +175,4 @@ Running it will exit quickly:: $ python myinvoke.py ERROR: hi from our plugin - .. include:: links.inc --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -6,7 +6,7 @@ import re import inspect import time from fnmatch import fnmatch -from pytest.plugin.session import Collection +from pytest.plugin.session import Session from py.builtin import print_ from pytest.main import HookRelay @@ -273,34 +273,34 @@ class TmpTestdir: p.ensure("__init__.py") return p - Collection = Collection + Session = Session def getnode(self, config, arg): - collection = Collection(config) + session = Session(config) assert '::' not in str(arg) p = py.path.local(arg) - x = collection.fspath.bestrelpath(p) - return collection.perform_collect([x], genitems=False)[0] + x = session.fspath.bestrelpath(p) + return session.perform_collect([x], genitems=False)[0] def getpathnode(self, path): config = self.parseconfig(path) - collection = Collection(config) - x = collection.fspath.bestrelpath(path) - return collection.perform_collect([x], genitems=False)[0] + session = Session(config) + x = session.fspath.bestrelpath(path) + return session.perform_collect([x], genitems=False)[0] def genitems(self, colitems): - collection = colitems[0].collection + session = colitems[0].session result = [] for colitem in colitems: - result.extend(collection.genitems(colitem)) + result.extend(session.genitems(colitem)) return result def inline_genitems(self, *args): #config = self.parseconfig(*args) config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - collection = Collection(config) - collection.perform_collect() - return collection.items, rec + session = Session(config) + session.perform_collect() + return session.items, rec def runitem(self, source): # used from runner functional tests --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -376,8 +376,9 @@ class CollectonlyReporter: self._tw.line("INTERNALERROR> " + line) def pytest_collectstart(self, collector): - self.outindent(collector) - self.indent += self.INDENT + if collector.session != collector: + self.outindent(collector) + self.indent += self.INDENT def pytest_itemcollected(self, item): self.outindent(item) --- a/testing/plugin/test_resultlog.py +++ b/testing/plugin/test_resultlog.py @@ -5,10 +5,10 @@ from pytest.plugin.resultlog import gene from pytest.plugin.session import Node, Item, FSCollector def test_generic_path(testdir): - from pytest.plugin.session import Collection + from pytest.plugin.session import Session config = testdir.parseconfig() - collection = Collection(config) - p1 = Node('a', config=config, collection=collection) + session = Session(config) + p1 = Node('a', config=config, session=session) #assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -17,7 +17,7 @@ def test_generic_path(testdir): res = generic_path(item) assert res == 'a.B().c' - p0 = FSCollector('proj/test', config=config, collection=collection) + p0 = FSCollector('proj/test', config=config, session=session) p1 = FSCollector('proj/test/a', parent=p0) p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -205,15 +205,15 @@ class TestFunction: def test_function_equality(self, testdir, tmpdir): config = testdir.reparseconfig() - collection = testdir.Collection(config) + session = testdir.Session(config) f1 = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance, collection=collection) + args=(1,), callobj=isinstance, session=session) f2 = py.test.collect.Function(name="name",config=config, - args=(1,), callobj=py.builtin.callable, collection=collection) + args=(1,), callobj=py.builtin.callable, session=session) assert not f1 == f2 assert f1 != f2 f3 = py.test.collect.Function(name="name", config=config, - args=(1,2), callobj=py.builtin.callable, collection=collection) + args=(1,2), callobj=py.builtin.callable, session=session) assert not f3 == f2 assert f3 != f2 @@ -221,7 +221,7 @@ class TestFunction: assert f3 != f1 f1_b = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance, collection=collection) + args=(1,), callobj=isinstance, session=session) assert f1 == f1_b assert not f1 != f1_b @@ -235,11 +235,11 @@ class TestFunction: param = 1 funcargs = {} id = "world" - collection = testdir.Collection(config) + session = testdir.Session(config) f5 = py.test.collect.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance, collection=collection) + callspec=callspec1, callobj=isinstance, session=session) f5b = py.test.collect.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance, collection=collection) + callspec=callspec2, callobj=isinstance, session=session) assert f5 != f5b assert not (f5 == f5b) @@ -396,7 +396,7 @@ def test_generate_tests_only_done_in_sub def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) trail = modcol.nodeid - newcol = modcol.collection.perform_collect([trail], genitems=0)[0] + newcol = modcol.session.perform_collect([trail], genitems=0)[0] assert modcol.name == newcol.name --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -47,7 +47,7 @@ def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or - parent.collection.isinitpath(path)): + parent.session.isinitpath(path)): return parent.ihook.pytest_pycollect_makemodule( path=path, parent=parent) @@ -393,9 +393,9 @@ class Function(FunctionMixin, pytest.col """ _genid = None def __init__(self, name, parent=None, args=None, config=None, - callspec=None, callobj=_dummy, keywords=None, collection=None): + callspec=None, callobj=_dummy, keywords=None, session=None): super(Function, self).__init__(name, parent, - config=config, collection=collection) + config=config, session=session) self._args = args if self._isyieldedfunction(): assert not callspec, ( @@ -711,13 +711,13 @@ class FuncargRequest: raise self.LookupError(msg) def showfuncargs(config): - from pytest.plugin.session import Collection - collection = Collection(config) - collection.perform_collect() - if collection.items: - plugins = getplugins(collection.items[0]) + from pytest.plugin.session import Session + session = Session(config) + session.perform_collect() + if session.items: + plugins = getplugins(session.items[0]) else: - plugins = getplugins(collection) + plugins = getplugins(session) curdir = py.path.local() tw = py.io.TerminalWriter() verbose = config.getvalue("verbose") --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -31,7 +31,6 @@ finding out what is collected You can always peek at the collection tree without running tests like this:: . $ py.test --collectonly collectonly.py - --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -36,12 +36,12 @@ and when running it will see a skipped " $ py.test test_module.py -rs # "-rs" means report on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-435/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-557/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_module.py test_module.py .. --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -38,7 +38,8 @@ def pytest_unconfigure(config): """ called before test process is exited. """ def pytest_runtestloop(session): - """ called for performing the main runtest loop (after collection. """ + """ called for performing the main runtest loop + (after collection finished). """ pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- @@ -49,11 +50,11 @@ def pytest_collection(session): """ perform the collection protocol for the given session. """ pytest_collection.firstresult = True -def pytest_collection_modifyitems(config, items): +def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order the items in-place.""" -def pytest_collection_finish(collection): +def pytest_collection_finish(session): """ called after collection has been performed and modified. """ def pytest_ignore_collect(path, config): @@ -64,8 +65,7 @@ def pytest_ignore_collect(path, config): pytest_ignore_collect.firstresult = True def pytest_collect_directory(path, parent): - """ return collection Node or None for the given path. Any new node - needs to have the specified ``parent`` as a parent.""" + """ called before traversing a directory for collection files. """ pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): --- a/doc/assert.txt +++ b/doc/assert.txt @@ -21,7 +21,7 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_assert1.py test_assert1.py F @@ -35,7 +35,7 @@ assertion fails you will see the value o E + where 3 = f() test_assert1.py:5: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.05 seconds ========================= Reporting details about the failing assertion is achieved by re-evaluating the assert expression and recording intermediate values. @@ -101,7 +101,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_assert2.py test_assert2.py F --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -27,17 +27,19 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_simple.yml - + test_simple.yml .F - + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.42 seconds ==================== + ========================= short test summary info ========================== + FAIL test_simple.yml::hello + ==================== 1 failed, 1 passed in 0.43 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -56,24 +58,25 @@ reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython - + test_simple.yml <- test_simple.yml:1: usecase: ok PASSED test_simple.yml <- test_simple.yml:1: usecase: hello FAILED - + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. + ========================= short test summary info ========================== + FAIL test_simple.yml::hello ==================== 1 failed, 1 passed in 0.07 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ py.test --collectonly - --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import py -from pytest.plugin.session import Collection +from pytest.plugin.session import Session class TestCollector: def test_collect_versus_item(self): @@ -86,7 +86,7 @@ class TestCollector: node = testdir.getpathnode(hello) assert isinstance(node, py.test.collect.File) assert node.name == "hello.xxx" - nodes = node.collection.perform_collect([node.nodeid], genitems=False) + nodes = node.session.perform_collect([node.nodeid], genitems=False) assert len(nodes) == 1 assert isinstance(nodes[0], py.test.collect.File) @@ -292,7 +292,7 @@ class TestCustomConftests: "*test_x*" ]) -class TestCollection: +class TestSession: def test_parsearg(self, testdir): p = testdir.makepyfile("def test_func(): pass") subdir = testdir.mkdir("sub") @@ -302,7 +302,7 @@ class TestCollection: testdir.chdir() subdir.chdir() config = testdir.parseconfig(p.basename) - rcol = Collection(config=config) + rcol = Session(config=config) assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) @@ -318,7 +318,7 @@ class TestCollection: id = "::".join([p.basename, "test_func"]) config = testdir.parseconfig(id) topdir = testdir.tmpdir - rcol = Collection(config) + rcol = Session(config) assert topdir == rcol.fspath rootid = rcol.nodeid #root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] @@ -333,7 +333,7 @@ class TestCollection: id = "::".join([p.basename, "test_func"]) config = testdir.parseconfig(id) topdir = testdir.tmpdir - rcol = Collection(config) + rcol = Session(config) assert topdir == rcol.fspath hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -367,7 +367,7 @@ class TestCollection: normid, ]: config = testdir.parseconfig(id) - rcol = Collection(config=config) + rcol = Session(config=config) rcol.perform_collect() items = rcol.items assert len(items) == 1 @@ -392,7 +392,7 @@ class TestCollection: id = p.basename config = testdir.parseconfig(id) - rcol = Collection(config) + rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() items = rcol.items @@ -400,7 +400,7 @@ class TestCollection: assert len(items) == 2 hookrec.hookrecorder.contains([ ("pytest_collectstart", - "collector.fspath == collector.collection.fspath"), + "collector.fspath == collector.session.fspath"), ("pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'"), ("pytest_collectstart", @@ -417,7 +417,7 @@ class TestCollection: test_aaa = aaa.join("test_aaa.py") p.move(test_aaa) config = testdir.parseconfig() - rcol = Collection(config) + rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() items = rcol.items @@ -441,7 +441,7 @@ class TestCollection: id = "." config = testdir.parseconfig(id) - rcol = Collection(config) + rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() items = rcol.items @@ -459,12 +459,13 @@ class TestCollection: def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") config = testdir.parseconfig() - rcol = Collection(config) + rcol = Session(config) rcol.perform_collect() items = rcol.items assert len(items) == 1 item, = items - newcol = Collection(config) + rcol.config.pluginmanager.unregister(name="session") + newcol = Session(config) item2, = newcol.perform_collect([item.nodeid], genitems=False) assert item2.name == item.name assert item2.fspath == item.fspath From commits-noreply at bitbucket.org Sun Nov 7 10:24:51 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 03:24:51 -0600 (CST) Subject: [py-svn] pytest-xdist commit 8cea75c22706: adapt to collection -> session renaming of pytest core Message-ID: <20101107092451.B1DE52419C5@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289121964 -3600 # Node ID 8cea75c227061419aa3044cad31e756596716bec # Parent 922c00c8c2fa0bebe693cab54301a01c43159e55 adapt to collection -> session renaming of pytest core --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a5', + version='1.5a6', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', --- a/xdist/remote.py +++ b/xdist/remote.py @@ -32,7 +32,6 @@ class SlaveInteractor: def pytest_sessionstart(self, session): self.session = session - self.collection = session.collection slaveinfo = getinfodict() self.sendevent("slaveready", slaveinfo=slaveinfo) @@ -56,20 +55,20 @@ class SlaveInteractor: item = self._id2item[nodeid] self.config.hook.pytest_runtest_protocol(item=item) elif name == "runtests_all": - for item in self.collection.items: + for item in session.items: self.config.hook.pytest_runtest_protocol(item=item) elif name == "shutdown": break return True - def pytest_collection_finish(self, collection): + def pytest_collection_finish(self, session): self._id2item = {} ids = [] - for item in collection.items: + for item in session.items: self._id2item[item.nodeid] = item ids.append(item.nodeid) self.sendevent("collectionfinish", - topdir=str(collection.fspath), + topdir=str(session.fspath), ids=ids) #def pytest_runtest_logstart(self, nodeid, location, fspath): --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -143,30 +143,16 @@ class SlaveFailSession: def pytest_collection(self, session): self.session = session - self.collection = collection = session.collection self.trails = self.current_command - hook = self.collection.ihook + hook = self.session.ihook try: - items = collection.perform_collect(self.trails or None) + items = session.perform_collect(self.trails or None) except pytest.UsageError: - items = collection.perform_collect(None) - hook.pytest_collection_modifyitems(config=session.config, items=items) - hook.pytest_collection_finish(collection=collection) + items = session.perform_collect(None) + hook.pytest_collection_modifyitems(session=session, config=session.config, items=items) + hook.pytest_collection_finish(session=session) return True - if self.trails: - col = self.collection - items = [] - for trail in self.trails: - names = col._parsearg(trail) - try: - for node in col.matchnodes([col._topcollector], names): - items.extend(col.genitems(node)) - except pytest.UsageError: - pass # ignore collect errors / vanished tests - self.collection.items = items - return True - def pytest_runtest_logreport(self, report): if report.failed: self.recorded_failures.append(report) --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a5' +__version__ = '1.5a6' From commits-noreply at bitbucket.org Sun Nov 7 12:04:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 05:04:00 -0600 (CST) Subject: [py-svn] pytest commit ea0185ce5bed: remove duplicate code, normalize relative path names to fix windows running tests Message-ID: <20101107110400.BCDB91E1394@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289127932 -3600 # Node ID ea0185ce5bed6006371bb2b878203aa075b5d45e # Parent 6a5bbc78bad973e969317db8b86f151e6539c42e remove duplicate code, normalize relative path names to fix windows running tests --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -39,7 +39,7 @@ class LogXML(object): def _opentestcase(self, report): names = report.nodeid.split("::") - names[0] = names[0].replace(os.sep, '.') + names[0] = names[0].replace("/", '.') names = tuple(names) d = {'time': self._durations.pop(names, "0")} names = [x.replace(".py", "") for x in names if x != "()"] --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -354,6 +354,7 @@ class Generator(FunctionMixin, PyCollect # invoke setup/teardown on popular request # (induced by the common "test_*" naming shared with normal tests) self.config._setupstate.prepare(self) + l = [] seen = {} for i, x in enumerate(self.obj()): --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -108,34 +108,6 @@ def pytest_ignore_collect(path, config): ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -class Session(object): - class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ - __module__ = 'builtins' # for py3 - - def __init__(self, config): - self.config = config - self.config.pluginmanager.register(self, name="session", prepend=True) - self._testsfailed = 0 - self.shouldstop = False - self.session = Session(config) # XXX move elswehre - - def pytest_collectstart(self): - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - def pytest_runtest_logreport(self, report): - if report.failed and 'xfail' not in getattr(report, 'keywords', []): - self._testsfailed += 1 - maxfail = self.config.getvalue("maxfail") - if maxfail and self._testsfailed >= maxfail: - self.shouldstop = "stopping after %d failures" % ( - self._testsfailed) - pytest_collectreport = pytest_runtest_logreport - -class NoMatch(Exception): - """ raised if matching cannot locate a matching names. """ - class HookProxy: def __init__(self, fspath, config): self.fspath = fspath @@ -311,7 +283,12 @@ class Collector(Node): class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, session=None): fspath = py.path.local(fspath) # xxx only for test_resultlog.py? - name = parent and fspath.relto(parent.fspath) or fspath.basename + name = fspath.basename + if parent is not None: + rel = fspath.relto(parent.fspath) + if rel: + name = rel + name = name.replace(os.sep, "/") super(FSCollector, self).__init__(name, parent, config, session) self.fspath = fspath @@ -343,6 +320,9 @@ class Item(Node): self._location = location return location +class NoMatch(Exception): + """ raised if matching cannot locate a matching names. """ + class Session(FSCollector): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ @@ -469,7 +449,8 @@ class Session(FSCollector): if self.config.option.pyargs: arg = self._tryconvertpyarg(arg) parts = str(arg).split("::") - path = self.fspath.join(parts[0], abs=True) + relpath = parts[0].replace("/", os.sep) + path = self.fspath.join(relpath, abs=True) if not path.check(): if self.config.option.pyargs: msg = "file or package not found: " From commits-noreply at bitbucket.org Sun Nov 7 16:08:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 09:08:58 -0600 (CST) Subject: [py-svn] pytest commit 6996b4fa53a1: refine initilization: read config also from a "pytest.ini" file if exists Message-ID: <20101107150858.BC03E2410C8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289142622 -3600 # Node ID 6996b4fa53a19dfa17558c38c870e43b485d0df9 # Parent ea0185ce5bed6006371bb2b878203aa075b5d45e refine initilization: read config also from a "pytest.ini" file if exists and revert earlier commandline option and group ordering change. --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -21,6 +21,7 @@ class Parser: self._processopt = processopt self._usage = usage self._inidict = {} + self._ininames = [] self.hints = [] def processoption(self, option): @@ -55,12 +56,12 @@ class Parser: def parse(self, args): self.optparser = optparser = MyOptionParser(self) - groups = list(reversed(self._groups)) + [self._anonymous] + groups = self._groups + [self._anonymous] for group in groups: if group.options: desc = group.description or group.name optgroup = py.std.optparse.OptionGroup(optparser, desc) - optgroup.add_options(reversed(group.options)) + optgroup.add_options(group.options) optparser.add_option_group(optgroup) return self.optparser.parse_args([str(x) for x in args]) @@ -74,6 +75,7 @@ class Parser: """ add an ini-file option with the given name and description. """ assert type in (None, "pathlist", "args", "linelist") self._inidict[name] = (help, type, default) + self._ininames.append(name) class OptionGroup: def __init__(self, name, description="", parser=None): @@ -290,7 +292,7 @@ class Config(object): raise def _initini(self, args): - self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) + self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) self._parser.addini('addopts', 'extra command line options', 'args') self._parser.addini('minversion', 'minimally required pytest version') @@ -303,7 +305,7 @@ class Config(object): self.pluginmanager.consider_env() self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) - self.hook.pytest_addoption(parser=self._parser) + self.pluginmanager.do_addoption(self._parser) def _checkversion(self): minver = self.inicfg.get('minversion', None) @@ -426,13 +428,16 @@ def getcfg(args, inibasenames): args = [x for x in args if str(x)[0] != "-"] if not args: args = [py.path.local()] - for inibasename in inibasenames: - for p in args: - x = findupwards(p, inibasename) - if x is not None: - iniconfig = py.iniconfig.IniConfig(x) - if 'pytest' in iniconfig.sections: - return iniconfig['pytest'] + for arg in args: + arg = py.path.local(arg) + if arg.check(): + for base in arg.parts(reverse=True): + for inibasename in inibasenames: + p = base.join(inibasename) + if p.check(): + iniconfig = py.iniconfig.IniConfig(p) + if 'pytest' in iniconfig.sections: + return iniconfig['pytest'] return {} def findupwards(current, basename): --- a/doc/customize.txt +++ b/doc/customize.txt @@ -4,7 +4,7 @@ basic test configuration Command line options and configuration file settings ----------------------------------------------------------------- -You can get help on options and configuration options by running:: +You can get help on options and ini-config values by running:: py.test -h # prints options _and_ config file settings @@ -14,23 +14,31 @@ which were registered by installed plugi how test configuration is read from setup/tox ini-files -------------------------------------------------------- -py.test looks for the first ``[pytest]`` section in either the first ``setup.cfg`` or the first ``tox.ini`` file found upwards from the arguments. Example:: +py.test searched for the first matching ini-style configuration file +in the directories of command line argument and the directories above. +It looks for filenames in this order:: + + pytest.ini + tox.ini + setup.cfg + +Searching stops when the first ``[pytest]`` section is found. +There is no merging of configuration values from multiple files. Example:: py.test path/to/testdir will look in the following dirs for a config file:: + path/to/testdir/pytest.ini + path/to/testdir/tox.ini path/to/testdir/setup.cfg + path/to/pytest.ini + path/to/tox.ini path/to/setup.cfg - path/setup.cfg - setup.cfg - ... # up until root of filesystem - path/to/testdir/tox.ini - path/to/tox.ini - path/tox.ini ... # up until root of filesystem -If no path was provided at all the current working directory is used for the lookup. +If argument is provided to a py.test run, the current working directory +is used to start the search. builtin configuration file options ---------------------------------------------- --- a/testing/plugin/test_genscript.py +++ b/testing/plugin/test_genscript.py @@ -28,7 +28,6 @@ def test_gen(testdir, anypython, standal result = standalone.run(anypython, testdir, p) assert result.ret != 0 - at py.test.mark.xfail(reason="fix-dist", run=False) def test_rundist(testdir, pytestconfig, standalone): pytestconfig.pluginmanager.skipifmissing("xdist") testdir.makepyfile(""" --- a/testing/test_config.py +++ b/testing/test_config.py @@ -40,6 +40,30 @@ class TestParseIni: "*tox.ini:2*requires*9.0*actual*" ]) + @py.test.mark.multi(name="setup.cfg tox.ini pytest.ini".split()) + def test_ini_names(self, testdir, name): + testdir.tmpdir.join(name).write(py.std.textwrap.dedent(""" + [pytest] + minversion = 1.0 + """)) + config = Config() + config.parse([testdir.tmpdir]) + assert config.getini("minversion") == "1.0" + + def test_toxini_before_lower_pytestini(self, testdir): + sub = testdir.tmpdir.mkdir("sub") + sub.join("tox.ini").write(py.std.textwrap.dedent(""" + [pytest] + minversion = 2.0 + """)) + testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent(""" + [pytest] + minversion = 1.5 + """)) + config = Config() + config.parse([sub]) + assert config.getini("minversion") == "2.0" + @py.test.mark.xfail(reason="probably not needed") def test_confcutdir(self, testdir): sub = testdir.mkdir("sub") --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -418,7 +418,6 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 - pytest.xfail("repair xdist") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -43,7 +43,8 @@ def showhelp(config): tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") tw.line() - for name, (help, type, default) in sorted(config._parser._inidict.items()): + for name in config._parser._ininames: + help, type, default = config._parser._inidict[name] if type is None: type = "string" spec = "%s (%s)" % (name, type) --- a/pytest/main.py +++ b/pytest/main.py @@ -236,6 +236,11 @@ class PluginManager(object): for hint in self._hints: tw.line("hint: %s" % hint) + def do_addoption(self, parser): + mname = "pytest_addoption" + methods = reversed(self.listattr(mname)) + MultiCall(methods, {'parser': parser}).execute() + def do_configure(self, config): assert not hasattr(self, '_config') self._config = config --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=py26,py27,py31,py27-xdist,py25,py24 +envlist=py26,py27,py31,py32,py27-xdist,py25,py24 indexserver= default http://pypi.testrun.org pypi http://pypi.python.org/simple From commits-noreply at bitbucket.org Sun Nov 7 16:25:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 09:25:01 -0600 (CST) Subject: [py-svn] pylib commit cc6128f129a0: bump version Message-ID: <20101107152501.5F54624140D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289143596 -3600 # Node ID cc6128f129a0d2723c998efc521b2158f91762aa # Parent 5367bea2f47738bdefa9eb55752e5a2a4b0e841b bump version --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def main(): long_description = long_description, install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev5', + version='2.0.0.dev6', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev5' +__version__ = '2.0.0.dev6' from py import _apipkg From commits-noreply at bitbucket.org Sun Nov 7 16:25:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 09:25:09 -0600 (CST) Subject: [py-svn] pytest commit 2203d078dd48: bump version Message-ID: <20101107152509.5A5C424140D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289143604 -3600 # Node ID 2203d078dd485640a5be1d6e8c8782e15e33065f # Parent 6996b4fa53a19dfa17558c38c870e43b485d0df9 bump version --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev23', + version='2.0.0.dev24', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev23' +__version__ = '2.0.0.dev24' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Sun Nov 7 17:19:54 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 10:19:54 -0600 (CST) Subject: [py-svn] pylib commit 50db633de97b: redo documentation for pylib: convert to sphinx, add an enhance. Message-ID: <20101107161954.B96561E1347@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289146873 -3600 # Node ID 50db633de97b3626c0a07fbe8911856e2c14988b # Parent cc6128f129a0d2723c998efc521b2158f91762aa redo documentation for pylib: convert to sphinx, add an enhance. --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +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 " 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 " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pylib.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pylib.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/pylib" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pylib" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." --- a/doc/install.txt +++ b/doc/install.txt @@ -1,128 +1,50 @@ -.. _`index page`: http://pypi.python.org/pypi/py/ +.. _`pylib`: +.. _`index page`: http://pypi.python.org/pypi/pylib/ -py.test/pylib installation info in a nutshell +installation info in a nutshell =================================================== -**Pythons**: 2.4, 2.5, 2.6, 2.7, 3.0, 3.1.x, Jython-2.5.1, PyPy-1.2 +**PyPI name**: pylib_ + +**Pythons**: 2.4, 2.5, 2.6, 2.7, 3.0, 3.1.x, Jython-2.5.1, PyPy-1.3 **Operating systems**: Linux, Windows, OSX, Unix **Requirements**: setuptools_ or Distribute_ -**Installers**: easy_install_ and pip_ or `standalone`_ (new for 1.2) +**Installers**: ``easy_install`` and ``pip`` -**Distribution names**: +**hg repository**: https://bitbucket.org/hpk42/pylib -* PyPI name: ``py`` (see `index page`_ for versions) -* redhat fedora: ``python-py`` -* debian: ``python-codespeak-lib`` -* gentoo: ``pylib`` -**Installed scripts**: see `bin`_ for which and how scripts are installed. - -**hg repository**: https://bitbucket.org/hpk42/py-trunk - -.. _`bin`: bin.html - - -.. _`easy_install`: - -Installation using easy_install -=================================================== +easy install +----------------------------- Both `Distribute`_ and setuptools_ provide the ``easy_install`` installation tool with which you can type into a command line window:: - easy_install -U py + easy_install -U pylib -to install the latest release of the py lib and py.test. The ``-U`` switch +to install the latest release of the py lib. The ``-U`` switch will trigger an upgrade if you already have an older version installed. Note that setuptools works ok with Python2 interpreters while `Distribute`_ additionally works with Python3 and also avoid some issues on Windows. -Known issues: - -- **Windows**: If "easy_install" or "py.test" are not found - please see here for preparing your environment for running - command line tools: `Python for Windows`_. You may alternatively - use an `ActivePython install`_ which makes command line tools - automatically available under Windows. - -.. _`ActivePython install`: http://www.activestate.com/activepython/downloads - -.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 - -- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ - so ``py.test`` will not work correctly. You may install py.test on - CPython and type ``py.test --genscript=mytest`` and then use - ``jython mytest`` to run py.test for your tests to run in Jython. - -- **On Linux**: If ``easy_install`` fails because it needs to run - as the superuser you are trying to install things globally - and need to put ``sudo`` in front of the command. - - -.. _quickstart: test/quickstart.html - - -Recommendation: install tool and dependencies virtually -=========================================================== - -It is recommended to work with virtual environments -(e.g. virtualenv_ or buildout_ based) and use easy_install_ -(or pip_) for installing py.test/pylib and any dependencies -you need to run your tests. Local virtual Python environments -(as opposed to system-wide "global" environments) make for a more -reproducible and reliable test environment. - -.. _`virtualenv`: http://pypi.python.org/pypi/virtualenv -.. _`buildout`: http://www.buildout.org/ -.. _pip: http://pypi.python.org/pypi/pip - -.. _standalone: - -Generating a py.test standalone Script -============================================ - -If you are a maintainer or application developer and want users -to run tests you can use a facility to generate a standalone -"py.test" script that you can tell users to run:: - - py.test --genscript=mytest - -will generate a ``mytest`` script that is, in fact, a ``py.test`` under -disguise. You can tell people to download and then e.g. run it like this:: - - python mytest --pastebin=all - -and ask them to send you the resulting URL. The resulting script has -all core features and runs unchanged under Python2 and Python3 interpreters. - -.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html - -.. _mercurial: http://mercurial.selenic.com/wiki/ -.. _`Distribute`: -.. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions -.. _`distribute installation`: http://pypi.python.org/pypi/distribute -.. _checkout: -.. _tarball: - Working from version control or a tarball -================================================= +----------------------------------------------- To follow development or start experiments, checkout the complete code and documentation source with mercurial_:: - hg clone https://bitbucket.org/hpk42/py-trunk/ + hg clone https://bitbucket.org/hpk42/pylib Development takes place on the 'trunk' branch. You can also go to the python package index and download and unpack a TAR file:: - http://pypi.python.org/pypi/py/ - + http://pypi.python.org/pypi/pylib/ activating a checkout with setuptools -------------------------------------------- @@ -137,52 +59,26 @@ in order to work inline with the tools a .. _`directly use a checkout`: -directly use a checkout or tarball / avoid setuptools -------------------------------------------------------------- - -Get a checkout_ or tarball_ and add paths to your environment variables: - -* ``PYTHONPATH`` needs to contain the root directory (where ``py`` resides) -* ``PATH`` needs to contain ``ROOT/bin`` or ``ROOT\bin\win32`` respectively. - -There also are helper scripts that set the variables accordingly. On windows -execute:: - - # inside autoexec.bat or shell startup - c:\\path\to\checkout\bin\env.cmd - -on linux/OSX add this to your shell initialization:: - - # inside e.g. .bashrc - eval `python ~/path/to/checkout/bin/env.py` - -both of which which will get you good settings. If you install -the pylib this way you can easily ``hg pull && hg up`` or download -a new tarball_ to follow the development tree. - - -Note that the scripts manually added like this will look for -py libs in the chain of parent directories of the current working dir. -For example, if you have a layout like this:: - - mypkg/ - subpkg1/ - tests/ - tests/ - py/ - -issuing ``py.test subpkg1`` will use the py lib -from that projects root directory. If in doubt over where -the pylib comes from you can always do:: - - py.test --version - -to see where py.test is imported from. - -.. _`command line scripts`: bin.html -.. _contact: contact.html - -.. _`RPM`: http://translate.sourceforge.net/releases/testing/fedora/pylib-0.9.2-1.fc9.noarch.rpm - .. _`setuptools`: http://pypi.python.org/pypi/setuptools + +Contact and Communication points +-------------------------------------- + +- `py-dev developers list`_ and `commit mailing list`_. + +- #pylib on irc.freenode.net IRC channel for random questions. + +- `bitbucket issue tracker`_ use this bitbucket issue tracker to report + bugs or request features. + +.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/pylib/issues/ + +.. _codespeak: http://codespeak.net/ +.. _`py-dev`: +.. _`development mailing list`: +.. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev +.. _`py-svn`: +.. _`commit mailing list`: http://codespeak.net/mailman/listinfo/py-svn + +.. include:: links.inc --- a/py/_path/common.py +++ b/py/_path/common.py @@ -36,7 +36,7 @@ class Checkers: return self.path.relto(arg) def fnmatch(self, arg): - return FNMatcher(arg)(self.path) + return self.path.fnmatch(arg) def endswith(self, arg): return str(self.path).endswith(arg) @@ -161,22 +161,45 @@ newline will be removed from the end of return repr(str(self)) def check(self, **kw): - """ check a path for existence, or query its properties + """ check a path for existence and properties. - without arguments, this returns True if the path exists (on the - filesystem), False if not + Without arguments, return True if the path exists, otherwise False. - with (keyword only) arguments, the object compares the value - of the argument with the value of a property with the same name - (if it has one, else it raises a TypeError) + valid checkers:: - when for example the keyword argument 'ext' is '.py', this will - return True if self.ext == '.py', False otherwise + file=1 # is a file + file=0 # is not a file (may not even exist) + dir=1 # is a dir + link=1 # is a link + exists=1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file """ if not kw: kw = {'exists' : 1} return self.Checkers(self)._evaluate(kw) + def fnmatch(self, pattern): + """return true if the basename/fullname matches the glob-'pattern'. + + valid pattern characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + If the pattern contains a path-separator then the full path + is used for pattern matching and a '*' is prepended to the + pattern. + + if the pattern doesn't contain a path-separator the pattern + is only matched against the basename. + """ + return FNMatcher(pattern)(self) + def relto(self, relpath): """ return a string which is the relative part of the path to the given 'relpath'. @@ -339,20 +362,6 @@ class FNMatcher: def __init__(self, pattern): self.pattern = pattern def __call__(self, path): - """return true if the basename/fullname matches the glob-'pattern'. - - * matches everything - ? matches any single character - [seq] matches any character in seq - [!seq] matches any char not in seq - - if the pattern contains a path-separator then the full path - is used for pattern matching and a '*' is prepended to the - pattern. - - if the pattern doesn't contain a path-separator the pattern - is only matched against the basename. - """ pattern = self.pattern if pattern.find(path.sep) == -1: name = path.basename --- a/doc/io.txt +++ b/doc/io.txt @@ -41,3 +41,19 @@ filedescriptors you may invoke:: >>> out,err = capture.reset() >>> err 'world' + +py.io object reference +============================ + +.. autoclass:: py.io.StdCaptureFD + :members: + :inherited-members: + +.. autoclass:: py.io.StdCapture + :members: + :inherited-members: + +.. autoclass:: py.io.TerminalWriter + :members: + :inherited-members: + --- a/CHANGELOG +++ b/CHANGELOG @@ -5,8 +5,8 @@ Changes between 1.3.4 and 2.0.0dev0 - py.test was moved to a separate "pytest" package. What remains is a stub hook which will proxy ``import py.test`` to ``pytest``. - all command line tools ("py.cleanup/lookup/countloc/..." moved - to "pytools" package) -- removed the old deprecated "py.magic" namespace + to "pycmd" package) +- removed the old and deprecated "py.magic" namespace - use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() --- a/doc/code.txt +++ b/doc/code.txt @@ -46,6 +46,11 @@ A quick example:: >>> str(c.source()).split('\n')[0] "def read(self, mode='r'):" +.. autoclass:: py.code.Code + :members: + :inherited-members: + + ``py.code.Source`` --------------------- @@ -67,6 +72,9 @@ Example:: >>> str(sub).strip() # XXX why is the strip() required?!? 'print "foo"' +.. autoclass:: py.code.Source + :members: + ``py.code.Traceback`` ------------------------ @@ -92,6 +100,9 @@ Example:: >>> str(first.statement).strip().startswith('raise ValueError') True +.. autoclass:: py.code.Traceback + :members: + ``py.code.Frame`` -------------------- @@ -111,6 +122,9 @@ Example (using the 'first' TracebackItem >>> [namevalue[0] for namevalue in frame.getargs()] ['cls', 'path'] +.. autoclass:: py.code.Frame + :members: + ``py.code.ExceptionInfo`` ---------------------------- @@ -132,3 +146,6 @@ Example:: >>> excinfo.exconly() "NameError: name 'foobar' is not defined" +.. autoclass:: py.code.ExceptionInfo + :members: + --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# +# pylib documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 21 08:30:10 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# 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 +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pylib' +copyright = u'2010, holger krekel et. al.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '2.0' +# The full version, including alpha/beta/rc tags. +release = '2.0.0.dev0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pylibdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pylib.tex', u'pylib Documentation', + u'holger krekel et. al.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pylib', u'pylib Documentation', + [u'holger krekel et. al.'], 1) +] + +autodoc_member_order = "bysource" +autodoc_default_flags = "inherited-members" + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'pylib' +epub_author = u'holger krekel et. al.' +epub_publisher = u'holger krekel et. al.' +epub_copyright = u'2010, holger krekel et. al.' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} --- a/doc/path.txt +++ b/doc/path.txt @@ -8,13 +8,13 @@ object to fs-like object trees (reading files/directories, examining the types and structure, etc.), and out-of-the-box provides a number of implementations of this API. -Path implementations provided by ``py.path`` +py.test.local - local file system path =============================================== .. _`local`: -``py.path.local`` --------------------- +basic interactive example +------------------------------------- The first and most obvious of the implementations is a wrapper around a local filesystem. It's just a bit nicer in usage than the regular Python APIs, and @@ -40,8 +40,15 @@ a ``py.path.local`` object for us (which >>> foofile.read(1) 'b' +reference documentation +--------------------------------- + +.. autoclass:: py._path.local.LocalPath + :members: + :inherited-members: + ``py.path.svnurl`` and ``py.path.svnwc`` ----------------------------------------------- +================================================== Two other ``py.path`` implementations that the py lib provides wrap the popular `Subversion`_ revision control system: the first (called 'svnurl') @@ -78,8 +85,23 @@ Example usage of ``py.path.svnwc``: .. _`Subversion`: http://subversion.tigris.org/ -Common vs. specific API -======================= +svn path related API reference +----------------------------------------- + +.. autoclass:: py._path.svnwc.SvnWCCommandPath + :members: + :inherited-members: + +.. autoclass:: py._path.svnurl.SvnCommandPath + :members: + :inherited-members: + +.. autoclass:: py._path.svnwc.SvnAuth + :members: + :inherited-members: + +Common vs. specific API, Examples +======================================== All Path objects support a common set of operations, suitable for many use cases and allowing to transparently switch the @@ -93,15 +115,12 @@ children, etc. Only things that are not such as handling metadata (e.g. the Subversion "properties") require using specific APIs. -Examples ---------------------------------- - A quick 'cookbook' of small examples that will be useful 'in real life', which also presents parts of the 'common' API, and shows some non-common methods: Searching `.txt` files -.......................... +-------------------------------- Search for a particular string inside all files with a .txt extension in a specific directory. @@ -123,7 +142,7 @@ specific directory. ['textfile1.txt', 'textfile2.txt', 'textfile2.txt'] Working with Paths -....................... +---------------------------- This example shows the ``py.path`` features to deal with filesystem paths Note that the filesystem is never touched, @@ -160,7 +179,7 @@ one, or a database or object tree, these with their own notion of path seperators and dealing with conversions, etc.). Checking path types -....................... +------------------------------- Now we will show a bit about the powerful 'check()' method on paths, which allows you to check whether a file exists, what type it is, etc.: @@ -186,7 +205,7 @@ allows you to check whether a file exist True Setting svn-properties -....................... +-------------------------------- As an example of 'uncommon' methods, we'll show how to read and write properties in an ``py.path.svnwc`` instance: @@ -206,7 +225,7 @@ properties in an ``py.path.svnwc`` insta 0 SVN authentication -....................... +---------------------------- Some uncommon functionality can also be provided as extensions, such as SVN authentication: @@ -238,36 +257,4 @@ Known problems / limitations there is no attention yet on making unicode paths work or deal with the famous "8.3" filename issues. -Future plans -============ -The Subversion path implementations are based -on the `svn` command line, not on the bindings. -It makes sense now to directly use the bindings. - -Moreover, it would be good, also considering -`execnet`_ distribution of programs, to -be able to manipulate Windows Paths on Linux -and vice versa. So we'd like to consider -refactoring the path implementations -to provide this choice (and getting rid -of platform-dependencies as much as possible). - -There is some experimental small approach -(``py/path/gateway/``) aiming at having -a convenient Remote Path implementation. - -There are various hacks out there to have -Memory-Filesystems and even path objects -being directly mountable under Linux (via `fuse`). -However, the Path object implementations -do not internally have a clean abstraction -of going to the filesystem - so with some -refactoring it should become easier to -have very custom Path objects, still offering -the quite full interface without requiring -to know about all details of the full path -implementation. - -.. _`execnet`: execnet.html - --- /dev/null +++ b/doc/links.inc @@ -0,0 +1,16 @@ + +.. _`skipping plugin`: plugin/skipping.html +.. _`funcargs mechanism`: funcargs.html +.. _`doctest.py`: http://docs.python.org/library/doctest.html +.. _`xUnit style setup`: xunit_setup.html +.. _`pytest_nose`: plugin/nose.html +.. _`reStructured Text`: http://docutils.sourceforge.net +.. _`Python debugger`: http://docs.python.org/lib/module-pdb.html +.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ +.. _pytest: http://pypi.python.org/pypi/pytest +.. _mercurial: http://mercurial.selenic.com/wiki/ +.. _`setuptools`: http://pypi.python.org/pypi/setuptools +.. _`distribute`: http://pypi.python.org/pypi/distribute +.. _`pip`: http://pypi.python.org/pypi/pip +.. _`virtualenv`: http://pypi.python.org/pypi/virtualenv +.. _hudson: http://hudson-ci.org/ --- a/doc/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -#XXX make work: excludedirs = ['_build'] -import py -pytest_plugins = ['pytest_restdoc'] -collect_ignore = ['test/attic.txt'] - -def pytest_runtest_setup(item): - if item.fspath.ext == ".txt": - py.test.importorskip("pygments") # for raising an error --- a/py/_path/local.py +++ b/py/_path/local.py @@ -203,14 +203,14 @@ class LocalPath(FSBase): def new(self, **kw): """ create a modified version of this path. - the following keyword arguments modify various path parts: + the following keyword arguments modify various path parts:: a:/some/path/to/a/file.ext - || drive - |-------------| dirname - |------| basename - |--| purebasename - |--| ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext """ obj = object.__new__(self.__class__) drive, dirname, basename, purebasename,ext = self._getbyspec( @@ -461,8 +461,8 @@ class LocalPath(FSBase): return self.strpath def pypkgpath(self, pkgname=None): - """ return the path's package path by looking for the given - pkgname. If pkgname is None then look for the last + """ return the Python package path by looking for a + pkgname. If pkgname is None look for the last directory upwards which still contains an __init__.py and whose basename is python-importable. Return None if a pkgpath can not be determined. --- a/doc/index.txt +++ b/doc/index.txt @@ -1,29 +1,37 @@ -py lib: tested useful cross-Python abstractions -=============================================================== +.. pylib documentation master file, created by + sphinx-quickstart on Thu Oct 21 08:30:10 2010. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. -The ``py`` lib has several namespaces: +Welcome to pylib's documentation! +================================= -`py.path`_: use path objects to transparently access local and svn filesystems. +:ref:`2.0.0 release announcement ` and :ref:`CHANGELOG ` -`py.code`_: generate code and use advanced introspection/traceback support. +Contents: -`py.io`_: Helper Classes for Capturing of Input/Output on FD/sys.std* level +.. toctree:: -`py.xml`_ for generating in-memory xml/html object trees + install + path + code + io + log + xml + misc -`py.log`_: an alpha document about the ad-hoc logging facilities + :maxdepth: 2 -`miscellaneous features`_ describes some small but nice py lib features. +.. toctree:: + :hidden: -.. _`PyPI project page`: http://pypi.python.org/pypi/py/ + announce/release-2.0.0 + changelog -For the latest Release, see `PyPI project page`_ +Indices and tables +================== -.. _`py-dev at codespeak net`: http://codespeak.net/mailman/listinfo/py-dev -.. _`py.log`: log.html -.. _`py.io`: io.html -.. _`py.path`: path.html -.. _`py.code`: code.html -.. _`py.xml`: xml.html -.. _`miscellaneous features`: misc.html +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` --- a/testing/path/common.py +++ b/testing/path/common.py @@ -102,7 +102,8 @@ class CommonFSTests(object): def test_fnmatch_file(self, path1): assert path1.join("samplefile").check(fnmatch='s*e') - assert path1.join("samplefile").check(notfnmatch='s*x') + assert path1.join("samplefile").fnmatch('s*e') + assert not path1.join("samplefile").fnmatch('s*x') assert not path1.join("samplefile").check(fnmatch='s*x') #def test_fnmatch_dir(self, path1): --- a/setup.py +++ b/setup.py @@ -9,8 +9,6 @@ pylib: cross-python development utils py.path.local: local path objects py.path.svnwc: local subversion WC paths -py.iniconfig: parse for .ini / .cfg files -py.apipkg: package API export control + lazy import py.io: io-capturing on filedescriptor or sys.* level Platforms: Linux, Win32, OSX --- a/doc/xml.txt +++ b/doc/xml.txt @@ -1,8 +1,7 @@ ==================================================== -py.xml: Lightweight and flexible xml/html generation +py.xml: simple pythonic xml/html file generation ==================================================== - Motivation ========== @@ -11,9 +10,6 @@ xml and html trees. However, many of th steep learning curve and are often hard to debug. Not to speak of the fact that they are frameworks to begin with. -The py lib strives to offer enough functionality to represent -itself and especially its API in html or xml. - .. _xist: http://www.livinglogic.de/Python/xist/index.html a pythonic object model , please --- a/.hgignore +++ b/.hgignore @@ -15,6 +15,7 @@ syntax:glob *.orig *~ +doc/_build build/ dist/ *.egg-info --- a/doc/confrest.py +++ /dev/null @@ -1,289 +0,0 @@ -import py - -from pytest.plugin.restdoc import convert_rest_html, strip_html_header - -html = py.xml.html - -class css: - #pagetitle = "pagetitle" - contentspace = "contentspace" - menubar = "menubar" - navspace = "navspace" - versioninfo = "versioninfo" - -class Page(object): - doctype = ('\n') - googlefragment = """ - - -""" - - def __init__(self, project, title, targetpath, stylesheeturl=None, - type="text/html", encoding="ISO-8859-1"): - self.project = project - self.title = project.prefix_title + title - self.targetpath = targetpath - self.stylesheeturl = stylesheeturl - self.type = type - self.encoding = encoding - - self.body = html.body() - self.head = html.head() - self._root = html.html(self.head, self.body) - self.fill() - - def a_href(self, name, url, **kwargs): - return html.a(name, class_="menu", href=url, **kwargs) - - def a_docref(self, name, relhtmlpath): - docpath = self.project.docpath - return html.div(html.a(name, class_="menu", - href=relpath(self.targetpath.strpath, - docpath.join(relhtmlpath).strpath))) - - def a_apigenref(self, name, relhtmlpath): - apipath = self.project.apigenpath - return html.a(name, class_="menu", - href=relpath(self.targetpath.strpath, - apipath.join(relhtmlpath).strpath)) - - def fill_menubar(self): - items = [ - self.a_docref("INSTALL", "install.html"), - self.a_docref("CONTACT", "contact.html"), - self.a_docref("CHANGELOG", "changelog.html"), - self.a_docref("FAQ", "faq.html"), - html.div( - html.h3("py.test:"), - self.a_docref("Index", "test/index.html"), - self.a_docref("Quickstart", "test/quickstart.html"), - self.a_docref("Features", "test/features.html"), - self.a_docref("Funcargs", "test/funcargs.html"), - self.a_docref("Plugins", "test/plugin/index.html"), - self.a_docref("Customize", "test/customize.html"), - self.a_docref("Tutorials", "test/talks.html"), - self.a_href("hudson-tests", "http://hudson.testrun.org") - ), - html.div( - html.h3("supporting APIs:"), - self.a_docref("Index", "index.html"), - self.a_docref("py.path", "path.html"), - self.a_docref("py.code", "code.html"), - ) - #self.a_docref("py.code", "code.html"), - #self.a_apigenref("api", "api/index.html"), - #self.a_apigenref("source", "source/index.html"), - #self.a_href("source", "http://bitbucket.org/hpk42/py-trunk/src/"), - ] - self.menubar = html.div(id=css.menubar, *[ - html.div(item) for item in items]) - version = py.version - announcelink = self.a_docref("%s ANN" % version, - "announce/release-%s.html" %(version,)) - self.menubar.insert(0, - html.div(announcelink)) - #self.a_href("%s-%s" % (self.title, py.version), - # "http://pypi.python.org/pypi/py/%s" % version, - #id="versioninfo", - - def fill(self): - content_type = "%s;charset=%s" %(self.type, self.encoding) - self.head.append(html.title(self.title)) - self.head.append(html.meta(name="Content-Type", content=content_type)) - if self.stylesheeturl: - self.head.append( - html.link(href=self.stylesheeturl, - media="screen", rel="stylesheet", - type="text/css")) - self.fill_menubar() - - self.body.append(html.div( - self.project.logo, - self.menubar, - id=css.navspace, - )) - - #self.body.append(html.div(self.title, id=css.pagetitle)) - self.contentspace = html.div(id=css.contentspace) - self.body.append(self.contentspace) - - def unicode(self, doctype=True): - page = self._root.unicode() - page = page.replace("", self.googlefragment + "") - if doctype: - return self.doctype + page - else: - return page - -class PyPage(Page): - def get_menubar(self): - menubar = super(PyPage, self).get_menubar() - # base layout - menubar.append( - html.a("issue", href="https://codespeak.net/issue/py-dev/", - class_="menu"), - ) - return menubar - - -def getrealname(username): - try: - import uconf - except ImportError: - return username - try: - user = uconf.system.User(username) - except KeyboardInterrupt: - raise - try: - return user.realname or username - except KeyError: - return username - - -class Project: - mydir = py.path.local(__file__).dirpath() - title = "py lib" - prefix_title = "" # we have a logo already containing "py lib" - encoding = 'latin1' - logo = html.div( - html.a( - html.img(alt="py lib", id='pyimg', height=114/2, width=154/2, - src="http://codespeak.net/img/pylib.png"), - href="http://pylib.org")) - Page = PyPage - - def __init__(self, sourcepath=None): - if sourcepath is None: - sourcepath = self.mydir - self.setpath(sourcepath) - - def setpath(self, sourcepath, docpath=None, - apigenpath=None, stylesheet=None): - self.sourcepath = sourcepath - if docpath is None: - docpath = sourcepath - self.docpath = docpath - if apigenpath is None: - apigenpath = docpath - self.apigenpath = apigenpath - if stylesheet is None: - p = sourcepath.join("style.css") - if p.check(): - self.stylesheet = p - else: - self.stylesheet = None - else: - p = sourcepath.join(stylesheet) - if p.check(): - stylesheet = p - self.stylesheet = stylesheet - #assert self.stylesheet - self.apigen_relpath = relpath( - self.docpath.strpath + '/', self.apigenpath.strpath + '/') - - def get_content(self, txtpath, encoding): - return unicode(txtpath.read(), encoding) - - def get_htmloutputpath(self, txtpath): - reloutputpath = txtpath.new(ext='.html').relto(self.sourcepath) - return self.docpath.join(reloutputpath) - - def process(self, txtpath): - encoding = self.encoding - content = self.get_content(txtpath, encoding) - outputpath = self.get_htmloutputpath(txtpath) - - stylesheet = self.stylesheet - if isinstance(stylesheet, py.path.local): - if not self.docpath.join(stylesheet.basename).check(): - docpath.ensure(dir=True) - stylesheet.copy(docpath) - stylesheet = relpath(outputpath.strpath, - self.docpath.join(stylesheet.basename).strpath) - - content = convert_rest_html(content, txtpath, - stylesheet=stylesheet, encoding=encoding) - content = strip_html_header(content, encoding=encoding) - - title = txtpath.purebasename - if txtpath.dirpath().basename == "test": - title = "py.test " + title - # title = "[%s] %s" % (txtpath.purebasename, py.version) - page = self.Page(self, title, - outputpath, stylesheeturl=stylesheet) - - try: - modified = py.process.cmdexec( - "hg tip --template 'modified {date|shortdate}'" - ) - except py.process.cmdexec.Error: - modified = " " - - #page.body.append(html.div(modified, id="docinfoline")) - - page.contentspace.append(py.xml.raw(content)) - outputpath.ensure().write(page.unicode().encode(encoding)) - -# XXX this function comes from apigen/linker.py, put it -# somewhere in py lib -import os -def relpath(p1, p2, sep=os.path.sep, back='..', normalize=True): - """ create a relative path from p1 to p2 - - sep is the seperator used for input and (depending - on the setting of 'normalize', see below) output - - back is the string used to indicate the parent directory - - when 'normalize' is True, any backslashes (\) in the path - will be replaced with forward slashes, resulting in a consistent - output on Windows and the rest of the world - - paths to directories must end on a / (URL style) - """ - if normalize: - p1 = p1.replace(sep, '/') - p2 = p2.replace(sep, '/') - sep = '/' - # XXX would be cool to be able to do long filename - # expansion and drive - # letter fixes here, and such... iow: windows sucks :( - if (p1.startswith(sep) ^ p2.startswith(sep)): - raise ValueError("mixed absolute relative path: %r -> %r" %(p1, p2)) - fromlist = p1.split(sep) - tolist = p2.split(sep) - - # AA - # AA BB -> AA/BB - # - # AA BB - # AA CC -> CC - # - # AA BB - # AA -> ../AA - - diffindex = 0 - for x1, x2 in zip(fromlist, tolist): - if x1 != x2: - break - diffindex += 1 - commonindex = diffindex - 1 - - fromlist_diff = fromlist[diffindex:] - tolist_diff = tolist[diffindex:] - - if not fromlist_diff: - return sep.join(tolist[commonindex:]) - backcount = len(fromlist_diff) - if tolist_diff: - return sep.join([back,]*(backcount-1) + tolist_diff) - return sep.join([back,]*(backcount) + tolist[commonindex:]) --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -1,2 +1,3 @@ +.. _`changelog`: .. include:: ../CHANGELOG --- a/doc/contact.txt +++ /dev/null @@ -1,30 +0,0 @@ -Contact and Communication points -=================================== - -- `py-dev developers list`_ announcements and discussions. - -- #pylib on irc.freenode.net IRC channel for random questions. - -- `tetamap`_: Holger Krekel's blog, main author of pylib code. - -- `commit mailing list`_ or `@pylibcommit`_ to follow development commits, - -- `bitbucket issue tracker`_ use this bitbucket issue tracker to report - bugs or request features. - -.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/pylib/issues/ - -.. _`get an account`: - -.. _tetamap: http://tetamap.wordpress.com - -.. _`@pylibcommit`: http://twitter.com/pylibcommit - - -.. _codespeak: http://codespeak.net/ -.. _`py-dev`: -.. _`development mailing list`: -.. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev -.. _`py-svn`: -.. _`commit mailing list`: http://codespeak.net/mailman/listinfo/py-svn - --- /dev/null +++ b/doc/announce/release-2.0.0.txt @@ -0,0 +1,23 @@ + +.. _`release-2.0.0`: + +pylib 2.0.0: cross-platform library for path, code, io, ... manipulations +=========================================================================== + +"pylib" is a library comprising APIs for filesystem and svn path manipulations, +dynamic code construction, IO capturing and a Python2/Python3 compatibility +namespace. It runs unmodified on all Python interpreters compatible to +Python2.4 up until Python 3.2. "pylib" functionality used to be contained in a +PyPI distribution named "py" and then contained "py.test" and other +command line tools. This is now history. "pytest" is its own distribution +and "pylib" can be and is used completely separately from py.test. +The other "py.*" command line tools are installed with the new +separate "pycmd" distribution. + +The general idea for "pylib" is to place high value on providing +some basic APIs that are continously tested against many Python +interpreters and thus also to help transition. + +cheers, +holger + From commits-noreply at bitbucket.org Sun Nov 7 17:33:24 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 10:33:24 -0600 (CST) Subject: [py-svn] pylib commit 64c4901145ff: fix recursion handling (failed on windows) Message-ID: <20101107163324.99D6A6C1325@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289147700 -3600 # Node ID 64c4901145ffb29356f00cbdd4aea35fce9474c2 # Parent 50db633de97b3626c0a07fbe8911856e2c14988b fix recursion handling (failed on windows) --- a/py/_path/common.py +++ b/py/_path/common.py @@ -327,13 +327,13 @@ class Visitor: def __init__(self, fil, rec, ignore, bf, sort): if isinstance(fil, str): fil = FNMatcher(fil) - if rec: - if isinstance(rec, str): - rec = fnmatch(fil) - else: - assert hasattr(rec, '__call__') + if isinstance(rec, str): + self.rec = fnmatch(fil) + elif not hasattr(rec, '__call__') and rec: + self.rec = lambda path: True + else: + self.rec = rec self.fil = fil - self.rec = rec self.ignore = ignore self.breadthfirst = bf self.optsort = sort and sorted or (lambda x: x) From commits-noreply at bitbucket.org Sun Nov 7 17:36:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 10:36:09 -0600 (CST) Subject: [py-svn] pytest commit 360498cc71f6: avoid parsing of path objects when pytest.main(path) is called. Message-ID: <20101107163609.993816C131E@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289147860 -3600 # Node ID 360498cc71f600f48b0a23e1b130c7117fd7b861 # Parent 2203d078dd485640a5be1d6e8c8782e15e33065f avoid parsing of path objects when pytest.main(path) is called. --- a/pytest/main.py +++ b/pytest/main.py @@ -410,9 +410,9 @@ _preinit = [PluginManager(load=True)] # def main(args=None, plugins=None): if args is None: args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] elif not isinstance(args, (tuple, list)): - if isinstance(args, py.path.local): - args = str(args) if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = py.std.shlex.split(args) From commits-noreply at bitbucket.org Mon Nov 8 09:20:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 02:20:47 -0600 (CST) Subject: [py-svn] pytest commit 3bfe3a961247: install dependency from pytest distribution, not prior. Message-ID: <20101108082047.2132D24108B@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289204534 -3600 # Node ID 3bfe3a9612475789b02d7ac6496cc3def1319760 # Parent 360498cc71f600f48b0a23e1b130c7117fd7b861 install dependency from pytest distribution, not prior. --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ indexserver= changedir=testing commands= py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - pylib pypi pexpect pypi nose From commits-noreply at bitbucket.org Mon Nov 8 17:42:48 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 10:42:48 -0600 (CST) Subject: [py-svn] pylib commit 646d50096ca9: adapt to new tox indexserver syntax Message-ID: <20101108164248.DE7FE6C1311@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289234669 -3600 # Node ID 646d50096ca96073a81e02e085f3e5891a73192c # Parent 64c4901145ffb29356f00cbdd4aea35fce9474c2 adapt to new tox indexserver syntax --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist=py26,py27,py31,py27-xdist,py25,py24 -indexserver= - default http://pypi.testrun.org + +[indexserver] +url = http://pypi.testrun.org [testenv] changedir=testing From commits-noreply at bitbucket.org Mon Nov 8 17:43:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 10:43:00 -0600 (CST) Subject: [py-svn] pytest commit 6da16e7e805f: adapt to new tox indexserver syntax Message-ID: <20101108164300.6E2F9240FC9@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289234205 -3600 # Node ID 6da16e7e805f4a339749e969ee537545018cc8ba # Parent 3bfe3a9612475789b02d7ac6496cc3def1319760 adapt to new tox indexserver syntax --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,19 @@ [tox] distshare={homedir}/.tox/distshare envlist=py26,py27,py31,py32,py27-xdist,py25,py24 -indexserver= - default http://pypi.testrun.org - pypi http://pypi.python.org/simple + +[indexserver:default] +url = http://pypi.testrun.org + +[indexserver:pypi] +url = http://pypi.python.org/simple [testenv] changedir=testing commands= py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - pypi pexpect - pypi nose + :pypi:pexpect + :pypi:nose [testenv:genscript] changedir=. @@ -27,7 +30,7 @@ commands= [testenv:doc] basepython=python changedir=doc -deps=pypi sphinx +deps=:pypi:sphinx pytest commands= From commits-noreply at bitbucket.org Mon Nov 8 21:11:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 14:11:58 -0600 (CST) Subject: [py-svn] pylib commit 4ce4019bb0a9: adapt to simpler indexserver def Message-ID: <20101108201158.D118F6C1311@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289247220 -3600 # Node ID 4ce4019bb0a929796cacb251eb1505001cdaacee # Parent 646d50096ca96073a81e02e085f3e5891a73192c adapt to simpler indexserver def --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,6 @@ [tox] envlist=py26,py27,py31,py27-xdist,py25,py24 - -[indexserver] -url = http://pypi.testrun.org +indexserver= default=http://pypi.testrun.org [testenv] changedir=testing From commits-noreply at bitbucket.org Mon Nov 8 21:11:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 14:11:58 -0600 (CST) Subject: [py-svn] pytest commit 0e827d0237a4: adapt to simplified tox indexserver definition Message-ID: <20101108201158.CA0271E107A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289247204 -3600 # Node ID 0e827d0237a44bce95a59df4dea41f82546bb4b0 # Parent 6da16e7e805f4a339749e969ee537545018cc8ba adapt to simplified tox indexserver definition --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,9 @@ [tox] distshare={homedir}/.tox/distshare envlist=py26,py27,py31,py32,py27-xdist,py25,py24 - -[indexserver:default] -url = http://pypi.testrun.org - -[indexserver:pypi] -url = http://pypi.python.org/simple +indexserver= + default = http://pypi.testrun.org + pypi = http://pypi.python.org/simple [testenv] changedir=testing From commits-noreply at bitbucket.org Tue Nov 9 00:24:35 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 17:24:35 -0600 (CST) Subject: [py-svn] pytest commit 64e7dded2022: add coding for py3 Message-ID: <20101108232435.3F626241238@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289256495 21600 # Node ID 64e7dded2022a6f8cd9078539ebec395857e266b # Parent 0e827d0237a44bce95a59df4dea41f82546bb4b0 add coding for py3 --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -222,6 +222,7 @@ class TestPython: def test_unicode(self, testdir): value = 'hx\xc4\x85\xc4\x87\n' testdir.makepyfile(""" + # coding: utf-8 def test_hello(): print (%r) assert 0 From commits-noreply at bitbucket.org Tue Nov 9 00:24:35 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 17:24:35 -0600 (CST) Subject: [py-svn] pytest commit f52a7803cac8: run subprocess py.test scripts with the python version we're testing on Message-ID: <20101108232435.4CA85241420@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289258702 21600 # Node ID f52a7803cac89ab3cbcceeca38b8f218ad58ffbc # Parent 64e7dded2022a6f8cd9078539ebec395857e266b run subprocess py.test scripts with the python version we're testing on --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -451,7 +451,7 @@ class TmpTestdir: if not self.request.config.getvalue("notoolsonpath"): script = py.path.local.sysfind(scriptname) assert script, "script %r not found" % scriptname - return (script,) + return (py.std.sys.executable, script,) else: py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) From commits-noreply at bitbucket.org Sat Nov 13 09:09:12 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 02:09:12 -0600 (CST) Subject: [py-svn] pylib commit 1371dfb06957: use new apipkg with aliasmodule support Message-ID: <20101113080912.54FEE2419E2@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289635306 -3600 # Node ID 1371dfb06957338aef2066da0af04888f7e2af55 # Parent 4ce4019bb0a929796cacb251eb1505001cdaacee use new apipkg with aliasmodule support --- a/py/_apipkg.py +++ b/py/_apipkg.py @@ -9,11 +9,11 @@ import os import sys from types import ModuleType -__version__ = "1.1" +__version__ = '1.2.dev5' def initpkg(pkgname, exportdefs, attr=dict()): """ initialize given package from the export definitions. """ - oldmod = sys.modules[pkgname] + oldmod = sys.modules.get(pkgname) d = {} f = getattr(oldmod, '__file__', None) if f: @@ -25,10 +25,11 @@ def initpkg(pkgname, exportdefs, attr=di d['__loader__'] = oldmod.__loader__ if hasattr(oldmod, '__path__'): d['__path__'] = [os.path.abspath(p) for p in oldmod.__path__] - if hasattr(oldmod, '__doc__'): + if '__doc__' not in exportdefs and getattr(oldmod, '__doc__', None): d['__doc__'] = oldmod.__doc__ d.update(attr) - oldmod.__dict__.update(d) + if hasattr(oldmod, "__dict__"): + oldmod.__dict__.update(d) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod @@ -44,6 +45,16 @@ def importobj(modpath, attrname): return retval class ApiModule(ModuleType): + def __docget(self): + try: + return self.__doc + except AttributeError: + if '__doc__' in self.__map__: + return self.__makeattr('__doc__') + def __docset(self, value): + self.__doc = value + __doc__ = property(__docget, __docset) + def __init__(self, name, importspec, implprefix=None, attr=None): self.__name__ = name self.__all__ = [x for x in importspec if x != '__onfirstaccess__'] @@ -65,8 +76,13 @@ class ApiModule(ModuleType): attrname = parts and parts[0] or "" if modpath[0] == '.': modpath = implprefix + modpath - if name == '__doc__': - self.__doc__ = importobj(modpath, attrname) + + if not attrname: + subname = '%s.%s'%(self.__name__, name) + apimod = AliasModule(subname, modpath) + sys.modules[subname] = apimod + if '.' not in name: + setattr(self, name, apimod) else: self.__map__[name] = (modpath, attrname) @@ -118,3 +134,28 @@ class ApiModule(ModuleType): pass return dict __dict__ = property(__dict__) + + +def AliasModule(modname, modpath): + mod = [] + + def getmod(): + if not mod: + mod.append(importobj(modpath, None)) + return mod[0] + + class AliasModule(ModuleType): + + def __repr__(self): + return '' % (modname, modpath) + + def __getattribute__(self, name): + return getattr(getmod(), name) + + def __setattr__(self, name, value): + setattr(getmod(), name, value) + + def __delattr__(self, name): + delattr(getmod(), name) + + return AliasModule(modname) --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): long_description = long_description, install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev6', + version='2.0.0.dev7', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_apipkg.py +++ b/testing/test_apipkg.py @@ -1,7 +1,7 @@ import types import sys import py -from py import apipkg +import py._apipkg as apipkg import subprocess # # test support for importing modules @@ -16,7 +16,7 @@ class TestRealModule: tfile = pkgdir.join('__init__.py') tfile.write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, { 'x': { 'module': { @@ -89,7 +89,7 @@ class TestScenarios: def test_relative_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("mymodule") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ '__doc__': '.submod:maindoc', 'x': '.submod:x', @@ -109,32 +109,45 @@ class TestScenarios: def test_recursive_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("recmodule") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ 'some': '.submod:someclass', }) """)) pkgdir.join('submod.py').write(py.code.Source(""" - import recmodule + import recmodule class someclass: pass print (recmodule.__dict__) """)) monkeypatch.syspath_prepend(tmpdir) - import recmodule + import recmodule assert isinstance(recmodule, apipkg.ApiModule) assert recmodule.some.__name__ == "someclass" def test_module_alias_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("aliasimport") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ 'some': 'os.path', }) """)) monkeypatch.syspath_prepend(tmpdir) import aliasimport - assert aliasimport.some is py.std.os.path + for k, v in py.std.os.path.__dict__.items(): + assert getattr(aliasimport.some, k) == v + + def test_from_module_alias_import(self, monkeypatch, tmpdir): + pkgdir = tmpdir.mkdir("fromaliasimport") + pkgdir.join('__init__.py').write(py.code.Source(""" + import py._apipkg as apipkg + apipkg.initpkg(__name__, exportdefs={ + 'some': 'os.path', + }) + """)) + monkeypatch.syspath_prepend(tmpdir) + from fromaliasimport.some import join + assert join is py.std.os.path.join def xtest_nested_absolute_imports(): import email @@ -211,14 +224,22 @@ def test_initpkg_transfers_attrs(monkeyp assert newmod.__loader__ == mod.__loader__ assert newmod.__doc__ == mod.__doc__ -def test_initpkg_not_overwrite_exportdefs(monkeypatch): +def test_initpkg_nodoc(monkeypatch): mod = type(sys)('hello') - mod.__doc__ = "this is the documentation" + mod.__file__ = "hello.py" monkeypatch.setitem(sys.modules, 'hello', mod) + apipkg.initpkg('hello', {}) + newmod = sys.modules['hello'] + assert not newmod.__doc__ + +def test_initpkg_overwrite_doc(monkeypatch): + hello = type(sys)('hello') + hello.__doc__ = "this is the documentation" + monkeypatch.setitem(sys.modules, 'hello', hello) apipkg.initpkg('hello', {"__doc__": "sys:__doc__"}) - newmod = sys.modules['hello'] - assert newmod != mod - assert newmod.__doc__ == sys.__doc__ + newhello = sys.modules['hello'] + assert newhello != hello + assert newhello.__doc__ == sys.__doc__ def test_initpkg_not_transfers_not_existing_attrs(monkeypatch): mod = type(sys)('hello') @@ -249,7 +270,7 @@ def test_name_attribute(): def test_error_loading_one_element(monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("errorloading1") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ 'x': '.notexists:x', 'y': '.submod:y' @@ -267,7 +288,7 @@ def test_error_loading_one_element(monke def test_onfirstaccess(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("firstaccess") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', 'l': '.submod:l', @@ -276,7 +297,7 @@ def test_onfirstaccess(tmpdir, monkeypat """)) pkgdir.join('submod.py').write(py.code.Source(""" l = [] - def init(): + def init(): l.append(1) """)) monkeypatch.syspath_prepend(tmpdir) @@ -288,19 +309,19 @@ def test_onfirstaccess(tmpdir, monkeypat @py.test.mark.multi(mode=['attr', 'dict', 'onfirst']) def test_onfirstaccess_setsnewattr(tmpdir, monkeypatch, mode): - pkgname = 'mode_' + mode + pkgname = tmpdir.basename.replace("-", "") pkgdir = tmpdir.mkdir(pkgname) pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', }, ) """)) pkgdir.join('submod.py').write(py.code.Source(""" - def init(): + def init(): import %s as pkg - pkg.newattr = 42 + pkg.newattr = 42 """ % pkgname)) monkeypatch.syspath_prepend(tmpdir) mod = __import__(pkgname) @@ -329,12 +350,11 @@ def test_bpython_getattr_override(tmpdir def test_chdir_with_relative_imports_shouldnt_break_lazy_loading(tmpdir): - from py import _apipkg # cause py.apipkg is a apimodule - tmpdir.join('apipkg.py').write(py.code.Source(_apipkg)) + tmpdir.join('apipkg.py').write(py.code.Source(apipkg)) pkg = tmpdir.mkdir('pkg') messy = tmpdir.mkdir('messy') pkg.join('__init__.py').write(py.code.Source(""" - import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, { 'test': '.sub:test', }) @@ -365,7 +385,7 @@ def test_chdir_with_relative_imports_sho def test_dotted_name_lookup(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("dotted_name_lookup") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, dict(abs='os:path.abspath')) """)) monkeypatch.syspath_prepend(tmpdir) @@ -375,9 +395,76 @@ def test_dotted_name_lookup(tmpdir, monk def test_extra_attributes(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("extra_attributes") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, dict(abs='os:path.abspath'), dict(foo='bar')) """)) monkeypatch.syspath_prepend(tmpdir) import extra_attributes assert extra_attributes.foo == 'bar' + +def test_aliasmodule_repr(): + am = apipkg.AliasModule("mymod", "sys") + r = repr(am) + assert "" == r + am.version + assert repr(am) == r + +def test_aliasmodule_proxy_methods(tmpdir, monkeypatch): + pkgdir = tmpdir + pkgdir.join('aliasmodule_proxy.py').write(py.code.Source(""" + def doit(): + return 42 + """)) + + pkgdir.join('my_aliasmodule_proxy.py').write(py.code.Source(""" + import py._apipkg as apipkg + apipkg.initpkg(__name__, dict(proxy='aliasmodule_proxy')) + + def doit(): + return 42 + """)) + + monkeypatch.syspath_prepend(tmpdir) + import aliasmodule_proxy as orig + from my_aliasmodule_proxy import proxy + + doit = proxy.doit + assert doit is orig.doit + + del proxy.doit + py.test.raises(AttributeError, "orig.doit") + + proxy.doit = doit + assert orig.doit is doit + +def test_aliasmodule_nested_import_with_from(tmpdir, monkeypatch): + import os + pkgdir = tmpdir.mkdir("api1") + pkgdir.ensure("__init__.py").write(py.std.textwrap.dedent(""" + import py._apipkg as apipkg + apipkg.initpkg(__name__, { + 'os2': 'api2', + 'os2.path': 'api2.path2', + }) + """)) + tmpdir.join("api2.py").write(py.std.textwrap.dedent(""" + import os, sys + from os import path + sys.modules['api2.path2'] = path + x = 3 + """)) + monkeypatch.syspath_prepend(tmpdir) + from api1 import os2 + from api1.os2.path import abspath + assert abspath == os.path.abspath + # check that api1.os2 mirrors os.* + assert os2.x == 3 + import api1 + assert 'os2.path' not in api1.__dict__ + + +def test_initpkg_without_old_module(): + apipkg.initpkg("initpkg_without_old_module", + dict(modules="sys:modules")) + from initpkg_without_old_module import modules + assert modules is sys.modules --- a/py/__init__.py +++ b/py/__init__.py @@ -8,40 +8,44 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev6' +__version__ = '2.0.0.dev7' from py import _apipkg -_apipkg.initpkg(__name__, attr={'_apipkg': _apipkg}, exportdefs=dict( +_apipkg.initpkg(__name__, attr={'_apipkg': _apipkg}, exportdefs={ # access to all standard lib modules - std = '._std:std', + 'std': '._std:std', # access to all posix errno's as classes - error = '._error:error', + 'error': '._error:error', - _pydir = '.__metainfo:pydir', - version = 'py:__version__', # backward compatibility + '_pydir' : '.__metainfo:pydir', + 'version': 'py:__version__', # backward compatibility - test = 'pytest', # defer to pytest package + # pytest-2.0 has a flat namespace, we use alias modules + # to keep old references compatible + 'test' : 'pytest', + 'test.collect' : 'pytest', + 'test.cmdline' : 'pytest', # hook into the top-level standard library - process = { + 'process' : { '__doc__' : '._process:__doc__', 'cmdexec' : '._process.cmdexec:cmdexec', 'kill' : '._process.killproc:kill', 'ForkedFunc' : '._process.forkedfunc:ForkedFunc', }, - apipkg = { + 'apipkg' : { 'initpkg' : '._apipkg:initpkg', 'ApiModule' : '._apipkg:ApiModule', }, - iniconfig = { + 'iniconfig' : { 'IniConfig' : '._iniconfig:IniConfig', 'ParseError' : '._iniconfig:ParseError', }, - path = { + 'path' : { '__doc__' : '._path:__doc__', 'svnwc' : '._path.svnwc:SvnWCCommandPath', 'svnurl' : '._path.svnurl:SvnCommandPath', @@ -50,7 +54,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # python inspection/code-generation API - code = { + 'code' : { '__doc__' : '._code:__doc__', 'compile' : '._code.source:compile_', 'Source' : '._code.source:Source', @@ -69,7 +73,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # backports and additions of builtins - builtin = { + 'builtin' : { '__doc__' : '._builtin:__doc__', 'enumerate' : '._builtin:enumerate', 'reversed' : '._builtin:reversed', @@ -98,7 +102,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # input-output helping - io = { + 'io' : { '__doc__' : '._io:__doc__', 'dupfile' : '._io.capture:dupfile', 'TextIO' : '._io.capture:TextIO', @@ -113,7 +117,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # small and mean xml/html generation - xml = { + 'xml' : { '__doc__' : '._xmlgen:__doc__', 'html' : '._xmlgen:html', 'Tag' : '._xmlgen:Tag', @@ -122,7 +126,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg 'escape' : '._xmlgen:escape', }, - log = { + 'log' : { # logging API ('producers' and 'consumers' connected via keywords) '__doc__' : '._log:__doc__', '_apiwarn' : '._log.warning:_apiwarn', @@ -137,11 +141,11 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # compatibility modules (deprecated) - compat = { + 'compat' : { '__doc__' : '._compat:__doc__', 'doctest' : '._compat.dep_doctest:doctest', 'optparse' : '._compat.dep_optparse:optparse', 'textwrap' : '._compat.dep_textwrap:textwrap', 'subprocess' : '._compat.dep_subprocess:subprocess', }, -)) +}) From commits-noreply at bitbucket.org Sat Nov 13 09:13:41 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 02:13:41 -0600 (CST) Subject: [py-svn] pytest commit d3c75ce89f7d: internally use pytest.* instead of ``py.test.*`` in many places. Message-ID: <20101113081341.514156C112C@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289635511 -3600 # Node ID d3c75ce89f7d8c69468c3f96a3bd1473600895f6 # Parent f52a7803cac89ab3cbcceeca38b8f218ad58ffbc internally use pytest.* instead of ``py.test.*`` in many places. make sub namespace names 'collect' and 'cmdline' available on pytest directly --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,9 +5,9 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev24' +__version__ = '2.0.0.dev25' -__all__ = ['config', 'cmdline'] +__all__ = ['cmdline', 'collect', 'main'] from pytest import main as cmdline UsageError = cmdline.UsageError --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -140,7 +140,7 @@ def pytest_runtest_teardown(item): def pytest_runtest_makereport(item, call): """ return a :py:class:`pytest.plugin.runner.TestReport` object - for the given :py:class:`pytest.collect.Item` and + for the given :py:class:`pytest.Item` and :py:class:`pytest.plugin.runner.CallInfo`. """ pytest_runtest_makereport.firstresult = True --- a/doc/example/nonpython/conftest.py +++ b/doc/example/nonpython/conftest.py @@ -6,14 +6,14 @@ def pytest_collect_file(path, parent): if path.ext == ".yml" and path.basename.startswith("test"): return YamlFile(path, parent) -class YamlFile(py.test.collect.File): +class YamlFile(pytest.File): def collect(self): import yaml # we need a yaml parser, e.g. PyYAML raw = yaml.load(self.fspath.open()) for name, spec in raw.items(): yield YamlItem(name, self, spec) -class YamlItem(py.test.collect.Item): +class YamlItem(pytest.Item): def __init__(self, name, parent, spec): super(YamlItem, self).__init__(name, parent) self.spec = spec --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -1,6 +1,6 @@ """ discover and run doctests in modules and test files.""" -import py +import pytest, py from py._code.code import TerminalRepr, ReprFileLocation def pytest_addoption(parser): @@ -31,7 +31,7 @@ class ReprFailDoctest(TerminalRepr): tw.line(line) self.reprlocation.toterminal(tw) -class DoctestItem(py.test.collect.Item): +class DoctestItem(pytest.Item): def __init__(self, path, parent): name = self.__class__.__name__ + ":" + path.basename super(DoctestItem, self).__init__(name=name, parent=parent) --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -141,8 +141,8 @@ class BaseFunctionalTests: def test_custom_failure_repr(self, testdir): testdir.makepyfile(conftest=""" - import py - class Function(py.test.collect.Function): + import pytest + class Function(pytest.Function): def repr_failure(self, excinfo): return "hello" """) @@ -162,13 +162,12 @@ class BaseFunctionalTests: def test_failure_in_setup_function_ignores_custom_repr(self, testdir): testdir.makepyfile(conftest=""" - import py - class Function(py.test.collect.Function): + import pytest + class Function(pytest.Function): def repr_failure(self, excinfo): assert 0 """) reports = testdir.runitem(""" - import py def setup_function(func): raise ValueError(42) def test_func(): @@ -200,9 +199,9 @@ class BaseFunctionalTests: def test_exit_propagates(self, testdir): try: testdir.runitem(""" - import py + import pytest def test_func(): - raise py.test.exit.Exception() + raise pytest.exit.Exception() """) except py.test.exit.Exception: pass @@ -267,8 +266,8 @@ class TestSessionReports: def test_skip_at_module_scope(self, testdir): col = testdir.getmodulecol(""" - import py - py.test.skip("hello") + import pytest + pytest.skip("hello") def test_func(): pass """) --- a/pytest/plugin/mark.py +++ b/pytest/plugin/mark.py @@ -1,5 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ -import py +import pytest, py def pytest_namespace(): return {'mark': MarkGenerator()} @@ -156,13 +156,13 @@ class MarkInfo: self._name, self.args, self.kwargs) def pytest_itemcollected(item): - if not isinstance(item, py.test.collect.Function): + if not isinstance(item, pytest.Function): return try: func = item.obj.__func__ except AttributeError: func = getattr(item.obj, 'im_func', item.obj) - pyclasses = (py.test.collect.Class, py.test.collect.Module) + pyclasses = (pytest.Class, pytest.Module) for node in item.listchain(): if isinstance(node, pyclasses): marker = getattr(node.obj, 'pytestmark', None) --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -237,11 +237,11 @@ class TestPython: class TestNonPython: def test_summing_simple(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_collect_file(path, parent): if path.ext == ".xyz": return MyItem(path, parent) - class MyItem(py.test.collect.Item): + class MyItem(pytest.Item): def __init__(self, path, parent): super(MyItem, self).__init__(path.basename, parent) self.fspath = path --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -234,7 +234,7 @@ initialisation, command line and configu generic "runtest" hooks ------------------------------ -All all runtest related hooks receive a :py:class:`pytest.collect.Item` object. +All all runtest related hooks receive a :py:class:`pytest.Item` object. .. autofunction:: pytest_runtest_protocol .. autofunction:: pytest_runtest_setup --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -52,7 +52,7 @@ class TestConftestValueAccessGlobal: def test_default_has_lower_prio(self, basedir): conftest = ConftestWithSetinitial(basedir.join("adir")) assert conftest.rget('Directory') == 3 - #assert conftest.lget('Directory') == py.test.collect.Directory + #assert conftest.lget('Directory') == pytest.Directory def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev24', + version='2.0.0.dev25', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_main.py +++ b/testing/test_main.py @@ -352,8 +352,8 @@ class TestPytestPluginInteractions: def test_namespace_early_from_import(self, testdir): p = testdir.makepyfile(""" - from py.test.collect import Item - from pytest.collect import Item as Item2 + from pytest import Item + from pytest import Item as Item2 assert Item is Item2 """) result = testdir.runpython(p) --- a/pytest/plugin/nose.py +++ b/pytest/plugin/nose.py @@ -1,6 +1,6 @@ """run test suites written for nose. """ -import py +import pytest, py import inspect import sys @@ -14,12 +14,12 @@ def pytest_runtest_makereport(__multical def pytest_runtest_setup(item): - if isinstance(item, (py.test.collect.Function)): - if isinstance(item.parent, py.test.collect.Generator): + if isinstance(item, (pytest.Function)): + if isinstance(item.parent, pytest.Generator): gen = item.parent if not hasattr(gen, '_nosegensetup'): call_optional(gen.obj, 'setup') - if isinstance(gen.parent, py.test.collect.Instance): + if isinstance(gen.parent, pytest.Instance): call_optional(gen.parent.obj, 'setup') gen._nosegensetup = True if not call_optional(item.obj, 'setup'): @@ -27,7 +27,7 @@ def pytest_runtest_setup(item): call_optional(item.parent.obj, 'setup') def pytest_runtest_teardown(item): - if isinstance(item, py.test.collect.Function): + if isinstance(item, pytest.Function): if not call_optional(item.obj, 'teardown'): call_optional(item.parent.obj, 'teardown') #if hasattr(item.parent, '_nosegensetup'): @@ -35,7 +35,7 @@ def pytest_runtest_teardown(item): # del item.parent._nosegensetup def pytest_make_collect_report(collector): - if isinstance(collector, py.test.collect.Generator): + if isinstance(collector, pytest.Generator): call_optional(collector.obj, 'setup') def call_optional(obj, name): --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -122,9 +122,9 @@ class HookProxy: def compatproperty(name): def fget(self): #print "retrieving %r property from %s" %(name, self.fspath) - py.log._apiwarn("2.0", "use py.test.collect.%s for " - "Session classes" % name) - return getattr(pytest.collect, name) + py.log._apiwarn("2.0", "use pytest.%s for " + "test collection and item classes" % name) + return getattr(pytest, name) return property(fget) class Node(object): @@ -479,10 +479,10 @@ class Session(FSCollector): nextnames = names[1:] resultnodes = [] for node in matching: - if isinstance(node, pytest.collect.Item): + if isinstance(node, pytest.Item): resultnodes.append(node) continue - assert isinstance(node, pytest.collect.Collector) + assert isinstance(node, pytest.Collector) node.ihook.pytest_collectstart(collector=node) rep = node.ihook.pytest_make_collect_report(collector=node) if rep.passed: @@ -494,11 +494,11 @@ class Session(FSCollector): def genitems(self, node): self.trace("genitems", node) - if isinstance(node, pytest.collect.Item): + if isinstance(node, pytest.Item): node.ihook.pytest_itemcollected(item=node) yield node else: - assert isinstance(node, pytest.collect.Collector) + assert isinstance(node, pytest.Collector) node.ihook.pytest_collectstart(collector=node) rep = node.ihook.pytest_make_collect_report(collector=node) if rep.passed: --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -1,5 +1,5 @@ """ discovery and running of std-library "unittest" style tests. """ -import py +import pytest, py import sys def pytest_pycollect_makeitem(collector, name, obj): @@ -16,7 +16,7 @@ def pytest_pycollect_makeitem(collector, if isunit: return UnitTestCase(name, parent=collector) -class UnitTestCase(py.test.collect.Class): +class UnitTestCase(pytest.Class): def collect(self): loader = py.std.unittest.TestLoader() for name in loader.getTestCaseNames(self.obj): @@ -32,7 +32,7 @@ class UnitTestCase(py.test.collect.Class if meth is not None: meth() -class TestCaseFunction(py.test.collect.Function): +class TestCaseFunction(pytest.Function): def setup(self): pass def teardown(self): --- a/pytest/plugin/skipping.py +++ b/pytest/plugin/skipping.py @@ -65,7 +65,7 @@ class MarkEvaluator: def pytest_runtest_setup(item): - if not isinstance(item, py.test.collect.Function): + if not isinstance(item, pytest.Function): return evalskip = MarkEvaluator(item, 'skipif') if evalskip.istrue(): @@ -84,7 +84,7 @@ def check_xfail_no_run(item): py.test.xfail("[NOTRUN] " + evalxfail.getexplanation()) def pytest_runtest_makereport(__multicall__, item, call): - if not isinstance(item, py.test.collect.Function): + if not isinstance(item, pytest.Function): return if not (call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception)): --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -59,11 +59,11 @@ class TestGenerator: colitems = modcol.collect() assert len(colitems) == 1 gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == '[0]' assert gencolitems[0].obj.__name__ == 'func1' @@ -77,11 +77,11 @@ class TestGenerator: yield func1, 42, 6*7 """) gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == '[0]' assert gencolitems[0].obj.__name__ == 'func1' @@ -97,11 +97,11 @@ class TestGenerator: colitems = modcol.collect() assert len(colitems) == 1 gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == "['seventeen']" assert gencolitems[0].obj.__name__ == 'func1' assert gencolitems[1].name == "['fortytwo']" @@ -118,7 +118,7 @@ class TestGenerator: colitems = modcol.collect() assert len(colitems) == 1 gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) py.test.raises(ValueError, "gencol.collect()") def test_generative_methods_with_explicit_names(self, testdir): @@ -131,11 +131,11 @@ class TestGenerator: yield "m2", func1, 42, 6*7 """) gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == "['m1']" assert gencolitems[0].obj.__name__ == 'func1' assert gencolitems[1].name == "['m2']" @@ -199,20 +199,20 @@ class TestGenerator: class TestFunction: def test_getmodulecollector(self, testdir): item = testdir.getitem("def test_func(): pass") - modcol = item.getparent(py.test.collect.Module) - assert isinstance(modcol, py.test.collect.Module) + modcol = item.getparent(pytest.Module) + assert isinstance(modcol, pytest.Module) assert hasattr(modcol.obj, 'test_func') def test_function_equality(self, testdir, tmpdir): config = testdir.reparseconfig() session = testdir.Session(config) - f1 = py.test.collect.Function(name="name", config=config, + f1 = pytest.Function(name="name", config=config, args=(1,), callobj=isinstance, session=session) - f2 = py.test.collect.Function(name="name",config=config, + f2 = pytest.Function(name="name",config=config, args=(1,), callobj=py.builtin.callable, session=session) assert not f1 == f2 assert f1 != f2 - f3 = py.test.collect.Function(name="name", config=config, + f3 = pytest.Function(name="name", config=config, args=(1,2), callobj=py.builtin.callable, session=session) assert not f3 == f2 assert f3 != f2 @@ -220,7 +220,7 @@ class TestFunction: assert not f3 == f1 assert f3 != f1 - f1_b = py.test.collect.Function(name="name", config=config, + f1_b = pytest.Function(name="name", config=config, args=(1,), callobj=isinstance, session=session) assert f1 == f1_b assert not f1 != f1_b @@ -236,9 +236,9 @@ class TestFunction: funcargs = {} id = "world" session = testdir.Session(config) - f5 = py.test.collect.Function(name="name", config=config, + f5 = pytest.Function(name="name", config=config, callspec=callspec1, callobj=isinstance, session=session) - f5b = py.test.collect.Function(name="name", config=config, + f5b = pytest.Function(name="name", config=config, callspec=callspec2, callobj=isinstance, session=session) assert f5 != f5b assert not (f5 == f5b) @@ -263,9 +263,9 @@ class TestSorting: def test_fail(): assert 0 """) fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) + assert isinstance(fn1, pytest.Function) fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) + assert isinstance(fn2, pytest.Function) assert fn1 == fn2 assert fn1 != modcol @@ -274,7 +274,7 @@ class TestSorting: assert hash(fn1) == hash(fn2) fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) + assert isinstance(fn3, pytest.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -309,8 +309,8 @@ class TestSorting: class TestConftestCustomization: def test_pytest_pycollect_module(self, testdir): testdir.makeconftest(""" - import py - class MyModule(py.test.collect.Module): + import pytest + class MyModule(pytest.Module): pass def pytest_pycollect_makemodule(path, parent): if path.basename == "test_xyz.py": @@ -326,8 +326,8 @@ class TestConftestCustomization: def test_pytest_pycollect_makeitem(self, testdir): testdir.makeconftest(""" - import py - class MyFunction(py.test.collect.Function): + import pytest + class MyFunction(pytest.Function): pass def pytest_pycollect_makeitem(collector, name, obj): if name == "some": @@ -342,7 +342,7 @@ class TestConftestCustomization: def test_makeitem_non_underscore(self, testdir, monkeypatch): modcol = testdir.getmodulecol("def _hello(): pass") l = [] - monkeypatch.setattr(py.test.collect.Module, 'makeitem', + monkeypatch.setattr(pytest.Module, 'makeitem', lambda self, name, obj: l.append(name)) l = modcol.collect() assert '_hello' not in l @@ -545,7 +545,7 @@ class TestFillFuncArgs: item.config.pluginmanager.register(Provider()) if hasattr(item, '_args'): del item._args - py.test.collect._fillfuncargs(item) + pytest._fillfuncargs(item) assert len(item.funcargs) == 1 class TestRequest: @@ -634,7 +634,7 @@ class TestRequest: req.config._setupstate.prepare(item) # XXX req._fillfuncargs() # successively check finalization calls - teardownlist = item.getparent(py.test.collect.Module).obj.teardownlist + teardownlist = item.getparent(pytest.Module).obj.teardownlist ss = item.config._setupstate assert not teardownlist ss.teardown_exact(item) @@ -1009,11 +1009,11 @@ def test_conftest_funcargs_only_availabl def test_funcarg_non_pycollectobj(testdir): # rough jstests usage testdir.makeconftest(""" - import py + import pytest def pytest_pycollect_makeitem(collector, name, obj): if name == "MyClass": return MyCollector(name, parent=collector) - class MyCollector(py.test.collect.Collector): + class MyCollector(pytest.Collector): def reportinfo(self): return self.fspath, 3, "xyz" """) @@ -1048,8 +1048,8 @@ def test_funcarg_lookup_error(testdir): class TestReportInfo: def test_itemreport_reportinfo(self, testdir, linecomp): testdir.makeconftest(""" - import py - class MyFunction(py.test.collect.Function): + import pytest + class MyFunction(pytest.Function): def reportinfo(self): return "ABCDE", 42, "custom" def pytest_pycollect_makeitem(collector, name, obj): --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -24,7 +24,6 @@ class TestGeneralUsage: parser.addoption("--xyz", dest="xyz", action="store") """) testdir.makepyfile(test_one=""" - import py def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) @@ -37,7 +36,6 @@ class TestGeneralUsage: def test_basetemp(self, testdir): mytemp = testdir.tmpdir.mkdir("mytemp") p = testdir.makepyfile(""" - import py def test_1(pytestconfig): pytestconfig.getbasetemp().ensure("hello") """) @@ -86,9 +84,9 @@ class TestGeneralUsage: def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest(""" - import py + import pytest def pytest_collect_directory(): - py.test.skip("early") + pytest.skip("early") """) result = testdir.runpytest() assert result.ret == 0 @@ -98,8 +96,8 @@ class TestGeneralUsage: def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" - import py - class MyFile(py.test.collect.File): + import pytest + class MyFile(pytest.File): def collect(self): return def pytest_collect_file(path, parent): @@ -163,9 +161,9 @@ class TestGeneralUsage: def test_directory_skipped(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_ignore_collect(): - py.test.skip("intentional") + pytest.skip("intentional") """) testdir.makepyfile("def test_hello(): pass") result = testdir.runpytest() @@ -176,11 +174,11 @@ class TestGeneralUsage: def test_multiple_items_per_collector_byid(self, testdir): c = testdir.makeconftest(""" - import py - class MyItem(py.test.collect.Item): + import pytest + class MyItem(pytest.Item): def runtest(self): pass - class MyCollector(py.test.collect.File): + class MyCollector(pytest.File): def collect(self): return [MyItem(name="xyz", parent=self)] def pytest_collect_file(path, parent): @@ -195,13 +193,13 @@ class TestGeneralUsage: def test_skip_on_generated_funcarg_id(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_generate_tests(metafunc): metafunc.addcall({'x': 3}, id='hello-123') def pytest_runtest_setup(item): print (item.keywords) if 'hello-123' in item.keywords: - py.test.skip("hello") + pytest.skip("hello") assert 0 """) p = testdir.makepyfile("""def test_func(x): pass""") @@ -233,23 +231,37 @@ class TestGeneralUsage: class TestInvocationVariants: def test_earlyinit(self, testdir): p = testdir.makepyfile(""" - import py - assert hasattr(py.test, 'mark') + import pytest + assert hasattr(pytest, 'mark') """) result = testdir.runpython(p) assert result.ret == 0 def test_pydoc(self, testdir): - result = testdir.runpython_c("import py;help(py.test)") + for name in ('py.test', 'pytest'): + result = testdir.runpython_c("import %s;help(%s)" % (name,name)) + assert result.ret == 0 + s = result.stdout.str() + assert 'MarkGenerator' in s + + @pytest.mark.multi(source=['py.test', 'pytest']) + def test_import_star(self, testdir, source): + p = testdir.makepyfile(""" + from %s import * + collect + cmdline + main + skip + xfail + """ % source) + result = testdir.runpython(p) assert result.ret == 0 - s = result.stdout.str() - assert 'MarkGenerator' in s def test_double_pytestcmdline(self, testdir): p = testdir.makepyfile(run=""" - import py - py.test.cmdline.main() - py.test.cmdline.main() + import py, pytest + pytest.main() + pytest.main() """) testdir.makepyfile(""" def test_hello(): @@ -343,7 +355,6 @@ class TestInvocationVariants: def test_noclass_discovery_if_not_testcase(self, testdir): testpath = testdir.makepyfile(""" import unittest - import py class TestHello(object): def test_hello(self): assert self.attr --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -1,4 +1,4 @@ -import py +import pytest, py class SessionTests: def test_basic_testitem_events(self, testdir): @@ -26,7 +26,7 @@ class SessionTests: colstarted = reprec.getcalls("pytest_collectstart") assert len(colstarted) == 1 + 1 col = colstarted[1].collector - assert isinstance(col, py.test.collect.Module) + assert isinstance(col, pytest.Module) def test_nested_import_error(self, testdir): tfile = testdir.makepyfile(""" @@ -94,7 +94,7 @@ class SessionTests: def test_broken_repr(self, testdir): p = testdir.makepyfile(""" - import py + import pytest class BrokenRepr1: foo=0 def __repr__(self): @@ -103,7 +103,7 @@ class SessionTests: class TestBrokenClass: def test_explicit_bad_repr(self): t = BrokenRepr1() - py.test.raises(Exception, 'repr(t)') + pytest.raises(Exception, 'repr(t)') def test_implicit_bad_repr1(self): t = BrokenRepr1() --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -142,7 +142,7 @@ class PyobjMixin(object): modpath = self.getmodpath() return fspath, lineno, modpath -class PyCollectorMixin(PyobjMixin, pytest.collect.Collector): +class PyCollectorMixin(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): return name.startswith('test') @@ -203,7 +203,7 @@ class PyCollectorMixin(PyobjMixin, pytes l.append(function) return l -class Module(pytest.collect.File, PyCollectorMixin): +class Module(pytest.File, PyCollectorMixin): def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) @@ -249,7 +249,7 @@ class Module(pytest.collect.File, PyColl else: self.obj.teardown_module() -class Class(PyCollectorMixin, pytest.collect.Collector): +class Class(PyCollectorMixin, pytest.Collector): def collect(self): return [Instance(name="()", parent=self)] @@ -266,7 +266,7 @@ class Class(PyCollectorMixin, pytest.col teardown_class = getattr(teardown_class, 'im_func', teardown_class) teardown_class(self.obj) -class Instance(PyCollectorMixin, pytest.collect.Collector): +class Instance(PyCollectorMixin, pytest.Collector): def _getobj(self): return self.parent.obj() @@ -348,7 +348,7 @@ class FuncargLookupErrorRepr(TerminalRep tw.line() tw.line("%s:%d" % (self.filename, self.firstlineno+1)) -class Generator(FunctionMixin, PyCollectorMixin, pytest.collect.Collector): +class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): def collect(self): # test generators are seen as collectors but they also # invoke setup/teardown on popular request @@ -388,7 +388,7 @@ class Generator(FunctionMixin, PyCollect # Test Items # _dummy = object() -class Function(FunctionMixin, pytest.collect.Item): +class Function(FunctionMixin, pytest.Item): """ a Function Item is responsible for setting up and executing a Python callable test object. """ @@ -480,10 +480,10 @@ def fillfuncargs(function): def getplugins(node, withpy=False): # might by any node plugins = node.config._getmatchingplugins(node.fspath) if withpy: - mod = node.getparent(pytest.collect.Module) + mod = node.getparent(pytest.Module) if mod is not None: plugins.append(mod.obj) - inst = node.getparent(pytest.collect.Instance) + inst = node.getparent(pytest.Instance) if inst is not None: plugins.append(inst.obj) return plugins @@ -573,12 +573,12 @@ class FuncargRequest: @property def module(self): """ module where the test function was collected. """ - return self._pyfuncitem.getparent(pytest.collect.Module).obj + return self._pyfuncitem.getparent(pytest.Module).obj @property def cls(self): """ class (can be None) where the test function was collected. """ - clscol = self._pyfuncitem.getparent(pytest.collect.Class) + clscol = self._pyfuncitem.getparent(pytest.Class) if clscol: return clscol.obj @property @@ -679,7 +679,7 @@ class FuncargRequest: if scope == "function": return self._pyfuncitem elif scope == "module": - return self._pyfuncitem.getparent(py.test.collect.Module) + return self._pyfuncitem.getparent(pytest.Module) elif scope == "session": return None raise ValueError("unknown finalization scope %r" %(scope,)) --- a/example/assertion/global_testmodule_config/conftest.py +++ b/example/assertion/global_testmodule_config/conftest.py @@ -2,9 +2,9 @@ import py mydir = py.path.local(__file__).dirpath() def pytest_runtest_setup(item): - if isinstance(item, py.test.collect.Function): + if isinstance(item, pytest.Function): if not item.fspath.relto(mydir): return - mod = item.getparent(py.test.collect.Module).obj + mod = item.getparent(pytest.Module).obj if hasattr(mod, 'hello'): py.builtin.print_("mod.hello", mod.hello) --- a/doc/test/attic.txt +++ b/doc/test/attic.txt @@ -60,7 +60,7 @@ modules to determine collectors and item Customizing execution of Items and Functions ---------------------------------------------------- -- ``py.test.collect.Function`` test items control execution +- ``pytest.Function`` test items control execution of a test function through its ``function.runtest()`` method. This method is responsible for performing setup and teardown ("Test Fixtures") for a test Function. --- a/pytest/main.py +++ b/pytest/main.py @@ -6,6 +6,7 @@ All else is in pytest/plugin. import sys, os import inspect import py +import pytest from pytest import hookspec # the extension point definitions assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " @@ -207,7 +208,7 @@ class PluginManager(object): def pytest_plugin_registered(self, plugin): dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} if dic: - self._setns(py.test, dic) + self._setns(pytest, dic) if hasattr(self, '_config'): self.call_plugin(plugin, "pytest_addoption", {'parser': self._config._parser}) @@ -219,16 +220,20 @@ class PluginManager(object): if isinstance(value, dict): mod = getattr(obj, name, None) if mod is None: - mod = py.std.types.ModuleType(name) - sys.modules['pytest.%s' % name] = mod - sys.modules['py.test.%s' % name] = mod + modname = "pytest.%s" % name + mod = py.std.types.ModuleType(modname) + sys.modules[modname] = mod mod.__all__ = [] setattr(obj, name, mod) + #print "setns", mod, value self._setns(mod, value) else: #print "setting", name, value, "on", obj setattr(obj, name, value) obj.__all__.append(name) + #print "appending", name, "to", obj + #pytest.__all__.append(name) # don't show in help(py.test) + setattr(pytest, name, value) def pytest_terminal_summary(self, terminalreporter): tw = terminalreporter._tw @@ -284,6 +289,7 @@ def canonical_importname(name): return name def importplugin(importspec): + #print "importing", importspec try: return __import__(importspec, None, None, '__doc__') except ImportError: @@ -408,6 +414,9 @@ class HookCaller: _preinit = [PluginManager(load=True)] # triggers default plugin importing def main(args=None, plugins=None): + """ returned exit code integer, after an in-process testing run + with the given command line arguments, preloading an optional list + of passed in plugin objects. """ if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,10 +1,10 @@ -import py +import pytest, py from pytest.plugin.session import Session class TestCollector: def test_collect_versus_item(self): - from pytest.collect import Collector, Item + from pytest import Collector, Item assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) @@ -14,15 +14,15 @@ class TestCollector: def test_fail(): assert 0 """) recwarn.clear() - assert modcol.Module == py.test.collect.Module + assert modcol.Module == pytest.Module recwarn.pop(DeprecationWarning) - assert modcol.Class == py.test.collect.Class + assert modcol.Class == pytest.Class recwarn.pop(DeprecationWarning) - assert modcol.Item == py.test.collect.Item + assert modcol.Item == pytest.Item recwarn.pop(DeprecationWarning) - assert modcol.File == py.test.collect.File + assert modcol.File == pytest.File recwarn.pop(DeprecationWarning) - assert modcol.Function == py.test.collect.Function + assert modcol.Function == pytest.Function recwarn.pop(DeprecationWarning) def test_check_equality(self, testdir): @@ -31,9 +31,9 @@ class TestCollector: def test_fail(): assert 0 """) fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) + assert isinstance(fn1, pytest.Function) fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) + assert isinstance(fn2, pytest.Function) assert fn1 == fn2 assert fn1 != modcol @@ -42,7 +42,7 @@ class TestCollector: assert hash(fn1) == hash(fn2) fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) + assert isinstance(fn3, pytest.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -63,32 +63,32 @@ class TestCollector: fn = testdir.collect_by_name( testdir.collect_by_name(cls, "()"), "test_foo") - parent = fn.getparent(py.test.collect.Module) + parent = fn.getparent(pytest.Module) assert parent is modcol - parent = fn.getparent(py.test.collect.Function) + parent = fn.getparent(pytest.Function) assert parent is fn - parent = fn.getparent(py.test.collect.Class) + parent = fn.getparent(pytest.Class) assert parent is cls def test_getcustomfile_roundtrip(self, testdir): hello = testdir.makefile(".xxx", hello="world") testdir.makepyfile(conftest=""" - import py - class CustomFile(py.test.collect.File): + import pytest + class CustomFile(pytest.File): pass def pytest_collect_file(path, parent): if path.ext == ".xxx": return CustomFile(path, parent=parent) """) node = testdir.getpathnode(hello) - assert isinstance(node, py.test.collect.File) + assert isinstance(node, pytest.File) assert node.name == "hello.xxx" nodes = node.session.perform_collect([node.nodeid], genitems=False) assert len(nodes) == 1 - assert isinstance(nodes[0], py.test.collect.File) + assert isinstance(nodes[0], pytest.File) class TestCollectFS: def test_ignored_certain_directories(self, testdir): @@ -158,18 +158,18 @@ class TestPrunetraceback: import not_exists """) testdir.makeconftest(""" - import py + import pytest def pytest_collect_file(path, parent): return MyFile(path, parent) class MyError(Exception): pass - class MyFile(py.test.collect.File): + class MyFile(pytest.File): def collect(self): raise MyError() def repr_failure(self, excinfo): if excinfo.errisinstance(MyError): return "hello world" - return py.test.collect.File.repr_failure(self, excinfo) + return pytest.File.repr_failure(self, excinfo) """) result = testdir.runpytest(p) @@ -184,7 +184,7 @@ class TestPrunetraceback: import not_exists """) testdir.makeconftest(""" - import py + import pytest def pytest_make_collect_report(__multicall__): rep = __multicall__.execute() rep.headerlines += ["header1"] @@ -246,8 +246,8 @@ class TestCustomConftests: def test_pytest_fs_collect_hooks_are_seen(self, testdir): conf = testdir.makeconftest(""" - import py - class MyModule(py.test.collect.Module): + import pytest + class MyModule(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": @@ -265,8 +265,8 @@ class TestCustomConftests: sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") conf1 = testdir.makeconftest(""" - import py - class MyModule1(py.test.collect.Module): + import pytest + class MyModule1(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": @@ -274,8 +274,8 @@ class TestCustomConftests: """) conf1.move(sub1.join(conf1.basename)) conf2 = testdir.makeconftest(""" - import py - class MyModule2(py.test.collect.Module): + import pytest + class MyModule2(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": @@ -378,11 +378,11 @@ class TestSession: def test_collect_custom_nodes_multi_id(self, testdir): p = testdir.makepyfile("def test_func(): pass") testdir.makeconftest(""" - import py - class SpecialItem(py.test.collect.Item): + import pytest + class SpecialItem(pytest.Item): def runtest(self): return # ok - class SpecialFile(py.test.collect.File): + class SpecialFile(pytest.File): def collect(self): return [SpecialItem(name="check", parent=self)] def pytest_collect_file(path, parent): @@ -481,7 +481,7 @@ class Test_getinitialnodes: x = tmpdir.ensure("x.py") config = testdir.reparseconfig([x]) col = testdir.getnode(config, x) - assert isinstance(col, py.test.collect.Module) + assert isinstance(col, pytest.Module) assert col.name == 'x.py' assert col.parent.name == testdir.tmpdir.basename assert col.parent.parent is None @@ -496,7 +496,7 @@ class Test_getinitialnodes: subdir.ensure("__init__.py") config = testdir.reparseconfig([x]) col = testdir.getnode(config, x) - assert isinstance(col, py.test.collect.Module) + assert isinstance(col, pytest.Module) assert col.name == 'subdir/x.py' assert col.parent.parent is None for col in col.listchain(): From commits-noreply at bitbucket.org Sat Nov 13 11:12:10 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 04:12:10 -0600 (CST) Subject: [py-svn] pytest commit 4e2a0da2c7df: flat is better than nested (cont'd): Message-ID: <20101113101210.AC144241099@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289643045 -3600 # Node ID 4e2a0da2c7df834b6789db857af49440a11f98af # Parent d3c75ce89f7d8c69468c3f96a3bd1473600895f6 flat is better than nested (cont'd): - pytest.py is new module, making "python -m pytest" work always - _pytest/*.py now contains core.py, hookspec and the plugins, no sub packages --- a/pytest/plugin/monkeypatch.py +++ /dev/null @@ -1,103 +0,0 @@ -""" monkeypatching and mocking functionality. """ - -import os, sys - -def pytest_funcarg__monkeypatch(request): - """The returned ``monkeypatch`` funcarg provides these - helper methods to modify objects, dictionaries or os.environ:: - - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=False) - monkeypatch.delenv(name, value, raising=True) - monkeypatch.syspath_prepend(path) - - All modifications will be undone when the requesting - test function finished its execution. The ``raising`` - parameter determines if a KeyError or AttributeError - will be raised if the set/deletion operation has no target. - """ - mpatch = monkeypatch() - request.addfinalizer(mpatch.undo) - return mpatch - -notset = object() - -class monkeypatch: - """ object keeping a record of setattr/item/env/syspath changes. """ - def __init__(self): - self._setattr = [] - self._setitem = [] - - def setattr(self, obj, name, value, raising=True): - """ set attribute ``name`` on ``obj`` to ``value``, by default - raise AttributeEror if the attribute did not exist. """ - oldval = getattr(obj, name, notset) - if raising and oldval is notset: - raise AttributeError("%r has no attribute %r" %(obj, name)) - self._setattr.insert(0, (obj, name, oldval)) - setattr(obj, name, value) - - def delattr(self, obj, name, raising=True): - """ delete attribute ``name`` from ``obj``, by default raise - AttributeError it the attribute did not previously exist. """ - if not hasattr(obj, name): - if raising: - raise AttributeError(name) - else: - self._setattr.insert(0, (obj, name, getattr(obj, name, notset))) - delattr(obj, name) - - def setitem(self, dic, name, value): - """ set dictionary entry ``name`` to value. """ - self._setitem.insert(0, (dic, name, dic.get(name, notset))) - dic[name] = value - - def delitem(self, dic, name, raising=True): - """ delete ``name`` from dict, raise KeyError if it doesn't exist.""" - if name not in dic: - if raising: - raise KeyError(name) - else: - self._setitem.insert(0, (dic, name, dic.get(name, notset))) - del dic[name] - - def setenv(self, name, value, prepend=None): - """ set environment variable ``name`` to ``value``. if ``prepend`` - is a character, read the current environment variable value - and prepend the ``value`` adjoined with the ``prepend`` character.""" - value = str(value) - if prepend and name in os.environ: - value = value + prepend + os.environ[name] - self.setitem(os.environ, name, value) - - def delenv(self, name, raising=True): - """ delete ``name`` from environment, raise KeyError it not exists.""" - self.delitem(os.environ, name, raising=raising) - - def syspath_prepend(self, path): - """ prepend ``path`` to ``sys.path`` list of import locations. """ - if not hasattr(self, '_savesyspath'): - self._savesyspath = sys.path[:] - sys.path.insert(0, str(path)) - - def undo(self): - """ undo previous changes. This call consumes the - undo stack. Calling it a second time has no effect unless - you do more monkeypatching after the undo call.""" - for obj, name, value in self._setattr: - if value is not notset: - setattr(obj, name, value) - else: - delattr(obj, name) - self._setattr[:] = [] - for dictionary, name, value in self._setitem: - if value is notset: - del dictionary[name] - else: - dictionary[name] = value - self._setitem[:] = [] - if hasattr(self, '_savesyspath'): - sys.path[:] = self._savesyspath --- /dev/null +++ b/_pytest/monkeypatch.py @@ -0,0 +1,103 @@ +""" monkeypatching and mocking functionality. """ + +import os, sys + +def pytest_funcarg__monkeypatch(request): + """The returned ``monkeypatch`` funcarg provides these + helper methods to modify objects, dictionaries or os.environ:: + + monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.delattr(obj, name, raising=True) + monkeypatch.setitem(mapping, name, value) + monkeypatch.delitem(obj, name, raising=True) + monkeypatch.setenv(name, value, prepend=False) + monkeypatch.delenv(name, value, raising=True) + monkeypatch.syspath_prepend(path) + + All modifications will be undone when the requesting + test function finished its execution. The ``raising`` + parameter determines if a KeyError or AttributeError + will be raised if the set/deletion operation has no target. + """ + mpatch = monkeypatch() + request.addfinalizer(mpatch.undo) + return mpatch + +notset = object() + +class monkeypatch: + """ object keeping a record of setattr/item/env/syspath changes. """ + def __init__(self): + self._setattr = [] + self._setitem = [] + + def setattr(self, obj, name, value, raising=True): + """ set attribute ``name`` on ``obj`` to ``value``, by default + raise AttributeEror if the attribute did not exist. """ + oldval = getattr(obj, name, notset) + if raising and oldval is notset: + raise AttributeError("%r has no attribute %r" %(obj, name)) + self._setattr.insert(0, (obj, name, oldval)) + setattr(obj, name, value) + + def delattr(self, obj, name, raising=True): + """ delete attribute ``name`` from ``obj``, by default raise + AttributeError it the attribute did not previously exist. """ + if not hasattr(obj, name): + if raising: + raise AttributeError(name) + else: + self._setattr.insert(0, (obj, name, getattr(obj, name, notset))) + delattr(obj, name) + + def setitem(self, dic, name, value): + """ set dictionary entry ``name`` to value. """ + self._setitem.insert(0, (dic, name, dic.get(name, notset))) + dic[name] = value + + def delitem(self, dic, name, raising=True): + """ delete ``name`` from dict, raise KeyError if it doesn't exist.""" + if name not in dic: + if raising: + raise KeyError(name) + else: + self._setitem.insert(0, (dic, name, dic.get(name, notset))) + del dic[name] + + def setenv(self, name, value, prepend=None): + """ set environment variable ``name`` to ``value``. if ``prepend`` + is a character, read the current environment variable value + and prepend the ``value`` adjoined with the ``prepend`` character.""" + value = str(value) + if prepend and name in os.environ: + value = value + prepend + os.environ[name] + self.setitem(os.environ, name, value) + + def delenv(self, name, raising=True): + """ delete ``name`` from environment, raise KeyError it not exists.""" + self.delitem(os.environ, name, raising=raising) + + def syspath_prepend(self, path): + """ prepend ``path`` to ``sys.path`` list of import locations. """ + if not hasattr(self, '_savesyspath'): + self._savesyspath = sys.path[:] + sys.path.insert(0, str(path)) + + def undo(self): + """ undo previous changes. This call consumes the + undo stack. Calling it a second time has no effect unless + you do more monkeypatching after the undo call.""" + for obj, name, value in self._setattr: + if value is not notset: + setattr(obj, name, value) + else: + delattr(obj, name) + self._setattr[:] = [] + for dictionary, name, value in self._setitem: + if value is notset: + del dictionary[name] + else: + dictionary[name] = value + self._setitem[:] = [] + if hasattr(self, '_savesyspath'): + sys.path[:] = self._savesyspath --- /dev/null +++ b/_pytest/pastebin.py @@ -0,0 +1,63 @@ +""" submit failure or test session information to a pastebin service. """ +import py, sys + +class url: + base = "http://paste.pocoo.org" + xmlrpc = base + "/xmlrpc/" + show = base + "/show/" + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group._addoption('--pastebin', metavar="mode", + action='store', dest="pastebin", default=None, + type="choice", choices=['failed', 'all'], + help="send failed|all info to Pocoo pastebin service.") + +def pytest_configure(__multicall__, config): + import tempfile + __multicall__.execute() + if config.option.pastebin == "all": + config._pastebinfile = tempfile.TemporaryFile('w+') + tr = config.pluginmanager.getplugin('terminalreporter') + oldwrite = tr._tw.write + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) + config._pastebinfile.write(str(s)) + tr._tw.write = tee_write + +def pytest_unconfigure(config): + if hasattr(config, '_pastebinfile'): + config._pastebinfile.seek(0) + sessionlog = config._pastebinfile.read() + config._pastebinfile.close() + del config._pastebinfile + proxyid = getproxy().newPaste("python", sessionlog) + pastebinurl = "%s%s" % (url.show, proxyid) + sys.stderr.write("pastebin session-log: %s\n" % pastebinurl) + tr = config.pluginmanager.getplugin('terminalreporter') + del tr._tw.__dict__['write'] + +def getproxy(): + return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes + +def pytest_terminal_summary(terminalreporter): + if terminalreporter.config.option.pastebin != "failed": + return + tr = terminalreporter + if 'failed' in tr.stats: + terminalreporter.write_sep("=", "Sending information to Paste Service") + if tr.config.option.debug: + terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) + serverproxy = getproxy() + for rep in terminalreporter.stats.get('failed'): + try: + msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc + except AttributeError: + msg = tr._getfailureheadline(rep) + tw = py.io.TerminalWriter(stringio=True) + rep.toterminal(tw) + s = tw.stringio.getvalue() + assert len(s) + proxyid = serverproxy.newPaste("python", s) + pastebinurl = "%s%s" % (url.show, proxyid) + tr.write_line("%s --> %s" %(msg, pastebinurl)) --- a/pytest/plugin/capture.py +++ /dev/null @@ -1,216 +0,0 @@ -""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ - -import py -import os - -def pytest_addoption(parser): - group = parser.getgroup("general") - group._addoption('--capture', action="store", default=None, - metavar="method", type="choice", choices=['fd', 'sys', 'no'], - help="per-test capturing method: one of fd (default)|sys|no.") - group._addoption('-s', action="store_const", const="no", dest="capture", - help="shortcut for --capture=no.") - -def addouterr(rep, outerr): - repr = getattr(rep, 'longrepr', None) - if not hasattr(repr, 'addsection'): - return - for secname, content in zip(["out", "err"], outerr): - if content: - repr.addsection("Captured std%s" % secname, content.rstrip()) - -def pytest_configure(config): - config.pluginmanager.register(CaptureManager(), 'capturemanager') - -def pytest_unconfigure(config): - capman = config.pluginmanager.getplugin('capturemanager') - while capman._method2capture: - name, cap = capman._method2capture.popitem() - cap.reset() - -class NoCapture: - def startall(self): - pass - def resume(self): - pass - def reset(self): - pass - def suspend(self): - return "", "" - -class CaptureManager: - def __init__(self): - self._method2capture = {} - - def _maketempfile(self): - f = py.std.tempfile.TemporaryFile() - newf = py.io.dupfile(f, encoding="UTF-8") - f.close() - return newf - - def _makestringio(self): - return py.io.TextIO() - - def _getcapture(self, method): - if method == "fd": - return py.io.StdCaptureFD(now=False, - out=self._maketempfile(), err=self._maketempfile() - ) - elif method == "sys": - return py.io.StdCapture(now=False, - out=self._makestringio(), err=self._makestringio() - ) - elif method == "no": - return NoCapture() - else: - raise ValueError("unknown capturing method: %r" % method) - - def _getmethod(self, config, fspath): - if config.option.capture: - method = config.option.capture - else: - try: - method = config._conftest.rget("option_capture", path=fspath) - except KeyError: - method = "fd" - if method == "fd" and not hasattr(os, 'dup'): # e.g. jython - method = "sys" - return method - - def resumecapture_item(self, item): - method = self._getmethod(item.config, item.fspath) - if not hasattr(item, 'outerr'): - item.outerr = ('', '') # we accumulate outerr on the item - return self.resumecapture(method) - - def resumecapture(self, method): - if hasattr(self, '_capturing'): - raise ValueError("cannot resume, already capturing with %r" % - (self._capturing,)) - cap = self._method2capture.get(method) - self._capturing = method - if cap is None: - self._method2capture[method] = cap = self._getcapture(method) - cap.startall() - else: - cap.resume() - - def suspendcapture(self, item=None): - self.deactivate_funcargs() - if hasattr(self, '_capturing'): - method = self._capturing - cap = self._method2capture.get(method) - if cap is not None: - outerr = cap.suspend() - del self._capturing - if item: - outerr = (item.outerr[0] + outerr[0], - item.outerr[1] + outerr[1]) - return outerr - if hasattr(item, 'outerr'): - return item.outerr - return "", "" - - def activate_funcargs(self, pyfuncitem): - if not hasattr(pyfuncitem, 'funcargs'): - return - assert not hasattr(self, '_capturing_funcargs') - self._capturing_funcargs = capturing_funcargs = [] - for name, capfuncarg in pyfuncitem.funcargs.items(): - if name in ('capsys', 'capfd'): - capturing_funcargs.append(capfuncarg) - capfuncarg._start() - - def deactivate_funcargs(self): - capturing_funcargs = getattr(self, '_capturing_funcargs', None) - if capturing_funcargs is not None: - while capturing_funcargs: - capfuncarg = capturing_funcargs.pop() - capfuncarg._finalize() - del self._capturing_funcargs - - def pytest_make_collect_report(self, __multicall__, collector): - method = self._getmethod(collector.config, collector.fspath) - try: - self.resumecapture(method) - except ValueError: - return # recursive collect, XXX refactor capturing - # to allow for more lightweight recursive capturing - try: - rep = __multicall__.execute() - finally: - outerr = self.suspendcapture() - addouterr(rep, outerr) - return rep - - def pytest_runtest_setup(self, item): - self.resumecapture_item(item) - - def pytest_runtest_call(self, item): - self.resumecapture_item(item) - self.activate_funcargs(item) - - def pytest_runtest_teardown(self, item): - self.resumecapture_item(item) - - def pytest__teardown_final(self, __multicall__, session): - method = self._getmethod(session.config, None) - self.resumecapture(method) - try: - rep = __multicall__.execute() - finally: - outerr = self.suspendcapture() - if rep: - addouterr(rep, outerr) - return rep - - def pytest_keyboard_interrupt(self, excinfo): - if hasattr(self, '_capturing'): - self.suspendcapture() - - def pytest_runtest_makereport(self, __multicall__, item, call): - self.deactivate_funcargs() - rep = __multicall__.execute() - outerr = self.suspendcapture(item) - if not rep.passed: - addouterr(rep, outerr) - if not rep.passed or rep.when == "teardown": - outerr = ('', '') - item.outerr = outerr - return rep - -def pytest_funcarg__capsys(request): - """captures writes to sys.stdout/sys.stderr and makes - them available successively via a ``capsys.readouterr()`` method - which returns a ``(out, err)`` tuple of captured snapshot strings. - """ - return CaptureFuncarg(py.io.StdCapture) - -def pytest_funcarg__capfd(request): - """captures writes to file descriptors 1 and 2 and makes - snapshotted ``(out, err)`` string tuples available - via the ``capsys.readouterr()`` method. If the underlying - platform does not have ``os.dup`` (e.g. Jython) tests using - this funcarg will automatically skip. - """ - if not hasattr(os, 'dup'): - py.test.skip("capfd funcarg needs os.dup") - return CaptureFuncarg(py.io.StdCaptureFD) - -class CaptureFuncarg: - def __init__(self, captureclass): - self.capture = captureclass(now=False) - - def _start(self): - self.capture.startall() - - def _finalize(self): - if hasattr(self, 'capture'): - self.capture.reset() - del self.capture - - def readouterr(self): - return self.capture.readouterr() - - def close(self): - self._finalize() --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -1,5 +1,5 @@ import py, sys -from pytest.plugin import runner +from _pytest import runner from py._code.code import ReprExceptionInfo class TestSetupState: --- a/doc/nose.txt +++ b/doc/nose.txt @@ -4,7 +4,7 @@ Running test written for nose .. include:: links.inc py.test has basic support for running tests written for nose_. -This is implemented in :pymod:`pytest.plugin.nose`. +This is implemented in :pymod:`_pytest.nose`. Usage ------------- --- /dev/null +++ b/_pytest/resultlog.py @@ -0,0 +1,92 @@ +""" (disabled by default) create result information in a plain text file. """ + +import py +from py.builtin import print_ + +def pytest_addoption(parser): + group = parser.getgroup("resultlog", "resultlog plugin options") + group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None, + help="path for machine-readable result log.") + +def pytest_configure(config): + resultlog = config.option.resultlog + # prevent opening resultlog on slave nodes (xdist) + if resultlog and not hasattr(config, 'slaveinput'): + logfile = open(resultlog, 'w', 1) # line buffered + config._resultlog = ResultLog(config, logfile) + config.pluginmanager.register(config._resultlog) + +def pytest_unconfigure(config): + resultlog = getattr(config, '_resultlog', None) + if resultlog: + resultlog.logfile.close() + del config._resultlog + config.pluginmanager.unregister(resultlog) + +def generic_path(item): + chain = item.listchain() + gpath = [chain[0].name] + fspath = chain[0].fspath + fspart = False + for node in chain[1:]: + newfspath = node.fspath + if newfspath == fspath: + if fspart: + gpath.append(':') + fspart = False + else: + gpath.append('.') + else: + gpath.append('/') + fspart = True + name = node.name + if name[0] in '([': + gpath.pop() + gpath.append(name) + fspath = newfspath + return ''.join(gpath) + +class ResultLog(object): + def __init__(self, config, logfile): + self.config = config + self.logfile = logfile # preferably line buffered + + def write_log_entry(self, testpath, lettercode, longrepr): + print_("%s %s" % (lettercode, testpath), file=self.logfile) + for line in longrepr.splitlines(): + print_(" %s" % line, file=self.logfile) + + def log_outcome(self, report, lettercode, longrepr): + testpath = getattr(report, 'nodeid', None) + if testpath is None: + testpath = report.fspath + self.write_log_entry(testpath, lettercode, longrepr) + + def pytest_runtest_logreport(self, report): + res = self.config.hook.pytest_report_teststatus(report=report) + code = res[1] + if code == 'x': + longrepr = str(report.longrepr) + elif code == 'X': + longrepr = '' + elif report.passed: + longrepr = "" + elif report.failed: + longrepr = str(report.longrepr) + elif report.skipped: + longrepr = str(report.longrepr.reprcrash.message) + self.log_outcome(report, code, longrepr) + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + code = "F" + else: + assert report.skipped + code = "S" + longrepr = str(report.longrepr.reprcrash) + self.log_outcome(report, code, longrepr) + + def pytest_internalerror(self, excrepr): + path = excrepr.reprcrash.path + self.write_log_entry(path, '!', str(excrepr)) --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -4,9 +4,9 @@ terminal reporting of the full testing p import pytest,py import sys -from pytest.plugin.terminal import TerminalReporter, \ +from _pytest.terminal import TerminalReporter, \ CollectonlyReporter, repr_pythonversion, getreportopt -from pytest.plugin import runner +from _pytest import runner def basic_run_report(item): runner.call_and_report(item, "setup", log=False) --- a/testing/plugin/test_tmpdir.py +++ b/testing/plugin/test_tmpdir.py @@ -1,7 +1,7 @@ import py -from pytest.plugin.tmpdir import pytest_funcarg__tmpdir -from pytest.plugin.python import FuncargRequest +from _pytest.tmpdir import pytest_funcarg__tmpdir +from _pytest.python import FuncargRequest def test_funcarg(testdir): item = testdir.getitem(""" --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,5 +1,5 @@ import py -from pytest.plugin import config as parseopt +from _pytest import config as parseopt from textwrap import dedent class TestParser: --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev25', + version='2.0.0.dev26', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -41,7 +41,8 @@ def main(): 'Topic :: Utilities', 'Programming Language :: Python', 'Programming Language :: Python :: 3'], - packages=['pytest', 'pytest.plugin', ], + packages=['_pytest', ], + py_modules=['pytest'], zip_safe=False, ) --- /dev/null +++ b/_pytest/genscript.py @@ -0,0 +1,73 @@ +""" generate a single-file self-contained version of py.test """ +import py +import pickle +import zlib +import base64 + +def find_toplevel(name): + for syspath in py.std.sys.path: + base = py.path.local(syspath) + lib = base/name + if lib.check(dir=1): + return lib + mod = base.join("%s.py" % name) + if mod.check(file=1): + return mod + raise LookupError(name) + +def pkgname(toplevel, rootpath, path): + parts = path.parts()[len(rootpath.parts()):] + return '.'.join([toplevel] + [x.purebasename for x in parts]) + +def pkg_to_mapping(name): + toplevel = find_toplevel(name) + name2src = {} + if toplevel.check(file=1): # module + name2src[toplevel.purebasename] = toplevel.read() + else: # package + for pyfile in toplevel.visit('*.py'): + pkg = pkgname(name, toplevel, pyfile) + name2src[pkg] = pyfile.read() + return name2src + +def compress_mapping(mapping): + data = pickle.dumps(mapping, 2) + data = zlib.compress(data, 9) + data = base64.encodestring(data) + data = data.decode('ascii') + return data + + +def compress_packages(names): + mapping = {} + for name in names: + mapping.update(pkg_to_mapping(name)) + return compress_mapping(mapping) + + +def generate_script(entry, packages): + data = compress_packages(packages) + tmpl = py.path.local(__file__).dirpath().join('standalonetemplate.py') + exe = tmpl.read() + exe = exe.replace('@SOURCES@', data) + exe = exe.replace('@ENTRY@', entry) + return exe + + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group.addoption("--genscript", action="store", default=None, + dest="genscript", metavar="path", + help="create standalone py.test script at given target path.") + +def pytest_cmdline_main(config): + genscript = config.getvalue("genscript") + if genscript: + script = generate_script( + 'import py; raise SystemExit(py.test.cmdline.main())', + ['py', '_pytest', 'pytest'], + ) + + genscript = py.path.local(genscript) + genscript.write(script) + return 0 --- /dev/null +++ b/_pytest/tmpdir.py @@ -0,0 +1,26 @@ +""" support for providing temporary directories to test functions. """ +import pytest, py + +def pytest_configure(config): + def ensuretemp(string, dir=1): + """ (deprecated) return temporary directory path with + the given string as the trailing part. It is usually + better to use the 'tmpdir' function argument which will + take care to provide empty unique directories for each + test call even if the test is called multiple times. + """ + #py.log._apiwarn(">1.1", "use tmpdir function argument") + return config.ensuretemp(string, dir=dir) + pytest.ensuretemp = ensuretemp + +def pytest_funcarg__tmpdir(request): + """return a temporary directory path object + unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a `py.path.local`_ + path object. + """ + name = request._pyfuncitem.name + name = py.std.re.sub("[\W]", "_", name) + x = request.config.mktemp(name, numbered=True) + return x.realpath() --- a/pytest/plugin/standalonetemplate.py +++ /dev/null @@ -1,63 +0,0 @@ -#! /usr/bin/env python - -sources = """ - at SOURCES@""" - -import sys -import base64 -import zlib -import imp - -class DictImporter(object): - def __init__(self, sources): - self.sources = sources - - def find_module(self, fullname, path=None): - if fullname in self.sources: - return self - if fullname+'.__init__' in self.sources: - return self - return None - - def load_module(self, fullname): - # print "load_module:", fullname - from types import ModuleType - try: - s = self.sources[fullname] - is_pkg = False - except KeyError: - s = self.sources[fullname+'.__init__'] - is_pkg = True - - co = compile(s, fullname, 'exec') - module = sys.modules.setdefault(fullname, ModuleType(fullname)) - module.__file__ = "%s/%s" % (__file__, fullname) - module.__loader__ = self - if is_pkg: - module.__path__ = [fullname] - - do_exec(co, module.__dict__) - return sys.modules[fullname] - - def get_source(self, name): - res = self.sources.get(name) - if res is None: - res = self.sources.get(name+'.__init__') - return res - -if __name__ == "__main__": - if sys.version_info >= (3,0): - exec("def do_exec(co, loc): exec(co, loc)\n") - import pickle - sources = sources.encode("ascii") # ensure bytes - sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) - else: - import cPickle as pickle - exec("def do_exec(co, loc): exec co in loc\n") - sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) - - importer = DictImporter(sources) - sys.meta_path.append(importer) - - entry = "@ENTRY@" - do_exec(entry, locals()) --- a/doc/usage.txt +++ b/doc/usage.txt @@ -7,6 +7,19 @@ Usage and Invocations .. _cmdline: +calling pytest through ``python -m pytest`` +----------------------------------------------------- + +.. versionadded: 2.0 + +If you use Python-2.5 or above you can invoke testing through the +Python interpreter from the command line:: + + python -m pytest [...] + +This is equivalent to invoking the command line script ``py.test [...]`` +directly. + Getting help on version, option names, environment vars ----------------------------------------------------------- @@ -38,23 +51,6 @@ Import 'pkg' and use its filesystem loca py.test --pyargs pkg # run all tests found below directory of pypkg -calling pytest through ``python -m pytest`` ------------------------------------------------------ - -.. versionadded: 2.0 - -You can invoke testing through the Python interpreter from the command line:: - - python -m pytest.main [...] - -Python2.7 and Python3 introduced specifying packages to "-m" so there -you can also type:: - - python -m pytest [...] - -All of these invocations are equivalent to the ``py.test [...]`` command line invocation. - - Modifying Python traceback printing ---------------------------------------------- --- /dev/null +++ b/_pytest/junitxml.py @@ -0,0 +1,174 @@ +""" report test results in JUnit-XML format, for use with Hudson and build integration servers. + +Based on initial code from Ross Lawley. +""" + +import py +import os +import time + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group.addoption('--junitxml', action="store", dest="xmlpath", + metavar="path", default=None, + help="create junit-xml style report file at given path.") + group.addoption('--junitprefix', action="store", dest="junitprefix", + metavar="str", default=None, + help="prepend prefix to classnames in junit-xml output") + +def pytest_configure(config): + xmlpath = config.option.xmlpath + if xmlpath: + config._xml = LogXML(xmlpath, config.option.junitprefix) + config.pluginmanager.register(config._xml) + +def pytest_unconfigure(config): + xml = getattr(config, '_xml', None) + if xml: + del config._xml + config.pluginmanager.unregister(xml) + +class LogXML(object): + def __init__(self, logfile, prefix): + self.logfile = logfile + self.prefix = prefix + self.test_logs = [] + self.passed = self.skipped = 0 + self.failed = self.errors = 0 + self._durations = {} + + def _opentestcase(self, report): + names = report.nodeid.split("::") + names[0] = names[0].replace("/", '.') + names = tuple(names) + d = {'time': self._durations.pop(names, "0")} + names = [x.replace(".py", "") for x in names if x != "()"] + classnames = names[:-1] + if self.prefix: + classnames.insert(0, self.prefix) + d['classname'] = ".".join(classnames) + d['name'] = py.xml.escape(names[-1]) + attrs = ['%s="%s"' % item for item in sorted(d.items())] + self.test_logs.append("\n" % " ".join(attrs)) + + def _closetestcase(self): + self.test_logs.append("") + + def appendlog(self, fmt, *args): + args = tuple([py.xml.escape(arg) for arg in args]) + self.test_logs.append(fmt % args) + + def append_pass(self, report): + self.passed += 1 + self._opentestcase(report) + self._closetestcase() + + def append_failure(self, report): + self._opentestcase(report) + #msg = str(report.longrepr.reprtraceback.extraline) + if "xfail" in report.keywords: + self.appendlog( + '') + self.skipped += 1 + else: + self.appendlog('%s', + report.longrepr) + self.failed += 1 + self._closetestcase() + + def append_collect_failure(self, report): + self._opentestcase(report) + #msg = str(report.longrepr.reprtraceback.extraline) + self.appendlog('%s', + report.longrepr) + self._closetestcase() + self.errors += 1 + + def append_collect_skipped(self, report): + self._opentestcase(report) + #msg = str(report.longrepr.reprtraceback.extraline) + self.appendlog('%s', + report.longrepr) + self._closetestcase() + self.skipped += 1 + + def append_error(self, report): + self._opentestcase(report) + self.appendlog('%s', + report.longrepr) + self._closetestcase() + self.errors += 1 + + def append_skipped(self, report): + self._opentestcase(report) + if "xfail" in report.keywords: + self.appendlog( + '%s', + report.keywords['xfail']) + else: + self.appendlog("") + self._closetestcase() + self.skipped += 1 + + def pytest_runtest_logreport(self, report): + if report.passed: + self.append_pass(report) + elif report.failed: + if report.when != "call": + self.append_error(report) + else: + self.append_failure(report) + elif report.skipped: + self.append_skipped(report) + + def pytest_runtest_call(self, item, __multicall__): + names = tuple(item.listnames()) + start = time.time() + try: + return __multicall__.execute() + finally: + self._durations[names] = time.time() - start + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + self.append_collect_failure(report) + else: + self.append_collect_skipped(report) + + def pytest_internalerror(self, excrepr): + self.errors += 1 + data = py.xml.escape(excrepr) + self.test_logs.append( + '\n' + ' ' + '%s' % data) + + def pytest_sessionstart(self, session): + self.suite_start_time = time.time() + + def pytest_sessionfinish(self, session, exitstatus, __multicall__): + if py.std.sys.version_info[0] < 3: + logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') + else: + logfile = open(self.logfile, 'w', encoding='utf-8') + + suite_stop_time = time.time() + suite_time_delta = suite_stop_time - self.suite_start_time + numtests = self.passed + self.failed + logfile.write('') + logfile.write('') + logfile.writelines(self.test_logs) + logfile.write('') + logfile.close() + + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile)) --- a/pytest/plugin/runner.py +++ /dev/null @@ -1,373 +0,0 @@ -""" basic collect and runtest protocol implementations """ - -import py, sys -from py._code.code import TerminalRepr - -def pytest_namespace(): - return { - 'fail' : fail, - 'skip' : skip, - 'importorskip' : importorskip, - 'exit' : exit, - } - -# -# pytest plugin hooks - -# XXX move to pytest_sessionstart and fix py.test owns tests -def pytest_configure(config): - config._setupstate = SetupState() - -def pytest_sessionfinish(session, exitstatus): - if hasattr(session.config, '_setupstate'): - hook = session.config.hook - rep = hook.pytest__teardown_final(session=session) - if rep: - hook.pytest__teardown_final_logerror(session=session, report=rep) - session.exitstatus = 1 - -class NodeInfo: - def __init__(self, location): - self.location = location - -def pytest_runtest_protocol(item): - item.ihook.pytest_runtest_logstart( - nodeid=item.nodeid, location=item.location, - ) - runtestprotocol(item) - return True - -def runtestprotocol(item, log=True): - rep = call_and_report(item, "setup", log) - reports = [rep] - if rep.passed: - reports.append(call_and_report(item, "call", log)) - reports.append(call_and_report(item, "teardown", log)) - return reports - -def pytest_runtest_setup(item): - item.config._setupstate.prepare(item) - -def pytest_runtest_call(item): - item.runtest() - -def pytest_runtest_teardown(item): - item.config._setupstate.teardown_exact(item) - -def pytest__teardown_final(session): - call = CallInfo(session.config._setupstate.teardown_all, when="teardown") - if call.excinfo: - ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) - call.excinfo.traceback = ntraceback.filter() - longrepr = call.excinfo.getrepr(funcargs=True) - return TeardownErrorReport(longrepr) - -def pytest_report_teststatus(report): - if report.when in ("setup", "teardown"): - if report.failed: - # category, shortletter, verbose-word - return "error", "E", "ERROR" - elif report.skipped: - return "skipped", "s", "SKIPPED" - else: - return "", "", "" - - -# -# Implementation - -def call_and_report(item, when, log=True): - call = call_runtest_hook(item, when) - hook = item.ihook - report = hook.pytest_runtest_makereport(item=item, call=call) - if log and (when == "call" or not report.passed): - hook.pytest_runtest_logreport(report=report) - return report - -def call_runtest_hook(item, when): - hookname = "pytest_runtest_" + when - ihook = getattr(item.ihook, hookname) - return CallInfo(lambda: ihook(item=item), when=when) - -class CallInfo: - """ Result/Exception info a function invocation. """ - #: None or ExceptionInfo object. - excinfo = None - def __init__(self, func, when): - #: context of invocation: one of "setup", "call", - #: "teardown", "memocollect" - self.when = when - try: - self.result = func() - except KeyboardInterrupt: - raise - except: - self.excinfo = py.code.ExceptionInfo() - - def __repr__(self): - if self.excinfo: - status = "exception: %s" % str(self.excinfo.value) - else: - status = "result: %r" % (self.result,) - return "" % (self.when, status) - -class BaseReport(object): - def toterminal(self, out): - longrepr = self.longrepr - if hasattr(longrepr, 'toterminal'): - longrepr.toterminal(out) - else: - out.line(str(longrepr)) - - passed = property(lambda x: x.outcome == "passed") - failed = property(lambda x: x.outcome == "failed") - skipped = property(lambda x: x.outcome == "skipped") - - @property - def fspath(self): - return self.nodeid.split("::")[0] - -def pytest_runtest_makereport(item, call): - when = call.when - keywords = dict([(x,1) for x in item.keywords]) - if not call.excinfo: - outcome = "passed" - longrepr = None - else: - excinfo = call.excinfo - if not isinstance(excinfo, py.code.ExceptionInfo): - outcome = "failed" - longrepr = excinfo - elif excinfo.errisinstance(py.test.skip.Exception): - outcome = "skipped" - longrepr = item._repr_failure_py(excinfo) - else: - outcome = "failed" - if call.when == "call": - longrepr = item.repr_failure(excinfo) - else: # exception in setup or teardown - longrepr = item._repr_failure_py(excinfo) - return TestReport(item.nodeid, item.location, - keywords, outcome, longrepr, when) - -class TestReport(BaseReport): - """ Basic test report object (also used for setup and teardown calls if - they fail). - """ - def __init__(self, nodeid, location, - keywords, outcome, longrepr, when): - #: normalized collection node id - self.nodeid = nodeid - - #: a (filesystempath, lineno, domaininfo) tuple indicating the - #: actual location of a test item - it might be different from the - #: collected one e.g. if a method is inherited from a different module. - self.location = location - - #: a name -> value dictionary containing all keywords and - #: markers associated with a test invocation. - self.keywords = keywords - - #: test outcome, always one of "passed", "failed", "skipped". - self.outcome = outcome - - #: None or a failure representation. - self.longrepr = longrepr - - #: one of 'setup', 'call', 'teardown' to indicate runtest phase. - self.when = when - - def __repr__(self): - return "" % ( - self.nodeid, self.when, self.outcome) - -class TeardownErrorReport(BaseReport): - outcome = "failed" - when = "teardown" - def __init__(self, longrepr): - self.longrepr = longrepr - -def pytest_make_collect_report(collector): - call = CallInfo(collector._memocollect, "memocollect") - reason = longrepr = None - if not call.excinfo: - outcome = "passed" - else: - if call.excinfo.errisinstance(py.test.skip.Exception): - outcome = "skipped" - reason = str(call.excinfo.value) - longrepr = collector._repr_failure_py(call.excinfo, "line") - else: - outcome = "failed" - errorinfo = collector.repr_failure(call.excinfo) - if not hasattr(errorinfo, "toterminal"): - errorinfo = CollectErrorRepr(errorinfo) - longrepr = errorinfo - return CollectReport(collector.nodeid, outcome, longrepr, - getattr(call, 'result', None), reason) - -class CollectReport(BaseReport): - def __init__(self, nodeid, outcome, longrepr, result, reason): - self.nodeid = nodeid - self.outcome = outcome - self.longrepr = longrepr - self.result = result or [] - self.reason = reason - - @property - def location(self): - return (self.fspath, None, self.fspath) - - def __repr__(self): - return "" % ( - self.nodeid, len(self.result), self.outcome) - -class CollectErrorRepr(TerminalRepr): - def __init__(self, msg): - self.longrepr = msg - def toterminal(self, out): - out.line(str(self.longrepr), red=True) - -class SetupState(object): - """ shared state for setting up/tearing down test items or collectors. """ - def __init__(self): - self.stack = [] - self._finalizers = {} - - def addfinalizer(self, finalizer, colitem): - """ attach a finalizer to the given colitem. - if colitem is None, this will add a finalizer that - is called at the end of teardown_all(). - """ - assert hasattr(finalizer, '__call__') - #assert colitem in self.stack - self._finalizers.setdefault(colitem, []).append(finalizer) - - def _pop_and_teardown(self): - colitem = self.stack.pop() - self._teardown_with_finalization(colitem) - - def _callfinalizers(self, colitem): - finalizers = self._finalizers.pop(colitem, None) - while finalizers: - fin = finalizers.pop() - fin() - - def _teardown_with_finalization(self, colitem): - self._callfinalizers(colitem) - if colitem: - colitem.teardown() - for colitem in self._finalizers: - assert colitem is None or colitem in self.stack - - def teardown_all(self): - while self.stack: - self._pop_and_teardown() - self._teardown_with_finalization(None) - assert not self._finalizers - - def teardown_exact(self, item): - if self.stack and item == self.stack[-1]: - self._pop_and_teardown() - else: - self._callfinalizers(item) - - def prepare(self, colitem): - """ setup objects along the collector chain to the test-method - and teardown previously setup objects.""" - needed_collectors = colitem.listchain() - while self.stack: - if self.stack == needed_collectors[:len(self.stack)]: - break - self._pop_and_teardown() - # check if the last collection node has raised an error - for col in self.stack: - if hasattr(col, '_prepare_exc'): - py.builtin._reraise(*col._prepare_exc) - for col in needed_collectors[len(self.stack):]: - self.stack.append(col) - try: - col.setup() - except Exception: - col._prepare_exc = sys.exc_info() - raise - -# ============================================================= -# Test OutcomeExceptions and helpers for creating them. - - -class OutcomeException(Exception): - """ OutcomeException and its subclass instances indicate and - contain info about test and collection outcomes. - """ - def __init__(self, msg=None): - self.msg = msg - - def __repr__(self): - if self.msg: - return str(self.msg) - return "<%s instance>" %(self.__class__.__name__,) - __str__ = __repr__ - -class Skipped(OutcomeException): - # XXX hackish: on 3k we fake to live in the builtins - # in order to have Skipped exception printing shorter/nicer - __module__ = 'builtins' - -class Failed(OutcomeException): - """ raised from an explicit call to py.test.fail() """ - __module__ = 'builtins' - -class Exit(KeyboardInterrupt): - """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason"): - self.msg = msg - KeyboardInterrupt.__init__(self, msg) - -# exposed helper methods - -def exit(msg): - """ exit testing process as if KeyboardInterrupt was triggered. """ - __tracebackhide__ = True - raise Exit(msg) - -exit.Exception = Exit - -def skip(msg=""): - """ skip an executing test with the given message. Note: it's usually - better to use the py.test.mark.skipif marker to declare a test to be - skipped under certain conditions like mismatching platforms or - dependencies. See the pytest_skipping plugin for details. - """ - __tracebackhide__ = True - raise Skipped(msg=msg) -skip.Exception = Skipped - -def fail(msg=""): - """ explicitely fail an currently-executing test with the given Message. """ - __tracebackhide__ = True - raise Failed(msg=msg) -fail.Exception = Failed - - -def importorskip(modname, minversion=None): - """ return imported module if it has a higher __version__ than the - optionally specified 'minversion' - otherwise call py.test.skip() - with a message detailing the mismatch. - """ - compile(modname, '', 'eval') # to catch syntaxerrors - try: - mod = __import__(modname, None, None, ['__doc__']) - except ImportError: - py.test.skip("could not import %r" %(modname,)) - if minversion is None: - return mod - verattr = getattr(mod, '__version__', None) - if isinstance(minversion, str): - minver = minversion.split(".") - else: - minver = list(minversion) - if verattr is None or verattr.split(".") < minver: - py.test.skip("module %r has __version__ %r, required is: %r" %( - modname, verattr, minversion)) - return mod --- a/testing/plugin/test_doctest.py +++ b/testing/plugin/test_doctest.py @@ -1,4 +1,4 @@ -from pytest.plugin.doctest import DoctestModule, DoctestTextfile +from _pytest.doctest import DoctestModule, DoctestTextfile import py pytest_plugins = ["pytest_doctest"] --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -244,16 +244,30 @@ class TestInvocationVariants: s = result.stdout.str() assert 'MarkGenerator' in s - @pytest.mark.multi(source=['py.test', 'pytest']) - def test_import_star(self, testdir, source): + def test_import_star_py_dot_test(self, testdir): p = testdir.makepyfile(""" - from %s import * - collect - cmdline + from py.test import * + #collect + #cmdline + #Item + #assert collect.Item is Item + #assert collect.Collector is Collector main skip xfail - """ % source) + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_import_star_pytest(self, testdir): + p = testdir.makepyfile(""" + from pytest import * + #Item + #File + main + skip + xfail + """) result = testdir.runpython(p) assert result.ret == 0 @@ -286,13 +300,6 @@ class TestInvocationVariants: assert res.ret == 1 @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_pytest_main(self, testdir): - p1 = testdir.makepyfile("def test_pass(): pass") - res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) - assert res.ret == 0 - res.stdout.fnmatch_lines(["*1 passed*"]) - - @py.test.mark.skipif("sys.version_info < (2,7)") def test_python_pytest_package(self, testdir): p1 = testdir.makepyfile("def test_pass(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) --- a/pytest/hookspec.py +++ /dev/null @@ -1,223 +0,0 @@ -""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ - -# ------------------------------------------------------------------------- -# Initialization -# ------------------------------------------------------------------------- - -def pytest_addhooks(pluginmanager): - """called at plugin load time to allow adding new hooks via a call to - pluginmanager.registerhooks(module).""" - - -def pytest_namespace(): - """return dict of name->object to be made globally available in - the py.test/pytest namespace. This hook is called before command - line options are parsed. - """ - -def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ -pytest_cmdline_parse.firstresult = True - -def pytest_addoption(parser): - """add optparse-style options and ini-style config values via calls - to ``parser.addoption`` and ``parser.addini(...)``. - """ - -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_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 -# ------------------------------------------------------------------------- - -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 - the items in-place.""" - -def pytest_collection_finish(session): - """ called after collection has been performed and modified. """ - -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 - -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 - needs to have the specified ``parent`` as a parent.""" - -# logging hooks for collection -def pytest_collectstart(collector): - """ collector starts collecting. """ - -def pytest_itemcollected(item): - """ we just collected a test item. """ - -def pytest_collectreport(report): - """ collector finished collecting. """ - -def pytest_deselected(items): - """ called for test items deselected by keyword. """ - -def pytest_make_collect_report(collector): - """ perform ``collector.collect()`` and return a CollectReport. """ -pytest_make_collect_report.firstresult = True - -# ------------------------------------------------------------------------- -# Python test function related hooks -# ------------------------------------------------------------------------- - -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 - -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 - -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.""" - -# ------------------------------------------------------------------------- -# generic runtest related hooks -# ------------------------------------------------------------------------- -def pytest_itemstart(item, node=None): - """ (deprecated, use pytest_runtest_logstart). """ - -def pytest_runtest_protocol(item): - """ implements the standard runtest_setup/call/teardown protocol including - capturing exceptions and calling reporting hooks on the results accordingly. - - :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 a test run. """ - -def pytest_runtest_setup(item): - """ called before ``pytest_runtest_call(item)``. """ - -def pytest_runtest_call(item): - """ called to execute the test ``item``. """ - -def pytest_runtest_teardown(item): - """ called after ``pytest_runtest_call``. """ - -def pytest_runtest_makereport(item, call): - """ return a :py:class:`pytest.plugin.runner.TestReport` object - for the given :py:class:`pytest.Item` and - :py:class:`pytest.plugin.runner.CallInfo`. - """ -pytest_runtest_makereport.firstresult = True - -def pytest_runtest_logreport(report): - """ process item test report. """ - -# special handling for final teardown - somewhat internal for now -def pytest__teardown_final(session): - """ called before test session finishes. """ -pytest__teardown_final.firstresult = True - -def pytest__teardown_final_logerror(report, session): - """ called if runtest_teardown_final failed. """ - -# ------------------------------------------------------------------------- -# test session related hooks -# ------------------------------------------------------------------------- - -def pytest_sessionstart(session): - """ before session.main() is called. """ - -def pytest_sessionfinish(session, exitstatus): - """ whole test run finishes. """ - - -# ------------------------------------------------------------------------- -# hooks for customising the assert methods -# ------------------------------------------------------------------------- - -def pytest_assertrepr_compare(config, op, left, right): - """return explanation for comparisons in failing assert expressions. - - Return None for no custom explanation, otherwise return a list - of strings. The strings will be joined by newlines but any newlines - *in* a string will be escaped. Note that all but the first line will - be indented sligthly, the intention is for the first line to be a summary. - """ - -# ------------------------------------------------------------------------- -# hooks for influencing reporting (invoked from pytest_terminal) -# ------------------------------------------------------------------------- - -def pytest_report_header(config): - """ return a string to be displayed as header info for terminal reporting.""" - -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. """ - -# ------------------------------------------------------------------------- -# doctest hooks -# ------------------------------------------------------------------------- - -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 py lib plugin got registered. """ - -def pytest_plugin_unregistered(plugin): - """ a py lib plugin got unregistered. """ - -def pytest_internalerror(excrepr): - """ called for internal errors. """ - -def pytest_keyboard_interrupt(excinfo): - """ called for keyboard interrupt. """ - -def pytest_trace(category, msg): - """ called for debug info. """ --- a/testing/plugin/test_monkeypatch.py +++ b/testing/plugin/test_monkeypatch.py @@ -1,6 +1,6 @@ import os, sys import py -from pytest.plugin.monkeypatch import monkeypatch as MonkeyPatch +from _pytest.monkeypatch import monkeypatch as MonkeyPatch def test_setattr(): class A: --- a/testing/plugin/test_recwarn.py +++ b/testing/plugin/test_recwarn.py @@ -1,5 +1,5 @@ import py -from pytest.plugin.recwarn import WarningsRecorder +from _pytest.recwarn import WarningsRecorder def test_WarningRecorder(recwarn): showwarning = py.std.warnings.showwarning --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import pytest, py -from pytest.plugin.session import Session +from _pytest.session import Session class TestCollector: def test_collect_versus_item(self): --- a/pytest/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -import pytest - -if __name__ == '__main__': - raise SystemExit(pytest.main()) --- /dev/null +++ b/_pytest/__init__.py @@ -0,0 +1,1 @@ +# --- a/pytest/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -unit and functional testing with Python. - -see http://pytest.org for documentation and details - -(c) Holger Krekel and others, 2004-2010 -""" -__version__ = '2.0.0.dev25' - -__all__ = ['cmdline', 'collect', 'main'] - -from pytest import main as cmdline -UsageError = cmdline.UsageError -main = cmdline.main --- a/doc/index.txt +++ b/doc/index.txt @@ -5,8 +5,8 @@ py.test: no-boilerplate testing with Pyt .. note:: version 2.0 introduces ``pytest`` as the main Python import name - but for historic reasons ``py.test`` remains fully valid and - represents the same package. + but for compatibility reasons you may continue to use ``py.test`` + in your test code. Welcome to ``py.test`` documentation: --- a/pytest/plugin/pastebin.py +++ /dev/null @@ -1,63 +0,0 @@ -""" submit failure or test session information to a pastebin service. """ -import py, sys - -class url: - base = "http://paste.pocoo.org" - xmlrpc = base + "/xmlrpc/" - show = base + "/show/" - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group._addoption('--pastebin', metavar="mode", - action='store', dest="pastebin", default=None, - type="choice", choices=['failed', 'all'], - help="send failed|all info to Pocoo pastebin service.") - -def pytest_configure(__multicall__, config): - import tempfile - __multicall__.execute() - if config.option.pastebin == "all": - config._pastebinfile = tempfile.TemporaryFile('w+') - tr = config.pluginmanager.getplugin('terminalreporter') - oldwrite = tr._tw.write - def tee_write(s, **kwargs): - oldwrite(s, **kwargs) - config._pastebinfile.write(str(s)) - tr._tw.write = tee_write - -def pytest_unconfigure(config): - if hasattr(config, '_pastebinfile'): - config._pastebinfile.seek(0) - sessionlog = config._pastebinfile.read() - config._pastebinfile.close() - del config._pastebinfile - proxyid = getproxy().newPaste("python", sessionlog) - pastebinurl = "%s%s" % (url.show, proxyid) - sys.stderr.write("pastebin session-log: %s\n" % pastebinurl) - tr = config.pluginmanager.getplugin('terminalreporter') - del tr._tw.__dict__['write'] - -def getproxy(): - return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes - -def pytest_terminal_summary(terminalreporter): - if terminalreporter.config.option.pastebin != "failed": - return - tr = terminalreporter - if 'failed' in tr.stats: - terminalreporter.write_sep("=", "Sending information to Paste Service") - if tr.config.option.debug: - terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) - serverproxy = getproxy() - for rep in terminalreporter.stats.get('failed'): - try: - msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc - except AttributeError: - msg = tr._getfailureheadline(rep) - tw = py.io.TerminalWriter(stringio=True) - rep.toterminal(tw) - s = tw.stringio.getvalue() - assert len(s) - proxyid = serverproxy.newPaste("python", s) - pastebinurl = "%s%s" % (url.show, proxyid) - tr.write_line("%s --> %s" %(msg, pastebinurl)) --- /dev/null +++ b/_pytest/runner.py @@ -0,0 +1,373 @@ +""" basic collect and runtest protocol implementations """ + +import py, sys +from py._code.code import TerminalRepr + +def pytest_namespace(): + return { + 'fail' : fail, + 'skip' : skip, + 'importorskip' : importorskip, + 'exit' : exit, + } + +# +# pytest plugin hooks + +# XXX move to pytest_sessionstart and fix py.test owns tests +def pytest_configure(config): + config._setupstate = SetupState() + +def pytest_sessionfinish(session, exitstatus): + if hasattr(session.config, '_setupstate'): + hook = session.config.hook + rep = hook.pytest__teardown_final(session=session) + if rep: + hook.pytest__teardown_final_logerror(session=session, report=rep) + session.exitstatus = 1 + +class NodeInfo: + def __init__(self, location): + self.location = location + +def pytest_runtest_protocol(item): + item.ihook.pytest_runtest_logstart( + nodeid=item.nodeid, location=item.location, + ) + runtestprotocol(item) + return True + +def runtestprotocol(item, log=True): + rep = call_and_report(item, "setup", log) + reports = [rep] + if rep.passed: + reports.append(call_and_report(item, "call", log)) + reports.append(call_and_report(item, "teardown", log)) + return reports + +def pytest_runtest_setup(item): + item.config._setupstate.prepare(item) + +def pytest_runtest_call(item): + item.runtest() + +def pytest_runtest_teardown(item): + item.config._setupstate.teardown_exact(item) + +def pytest__teardown_final(session): + call = CallInfo(session.config._setupstate.teardown_all, when="teardown") + if call.excinfo: + ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) + call.excinfo.traceback = ntraceback.filter() + longrepr = call.excinfo.getrepr(funcargs=True) + return TeardownErrorReport(longrepr) + +def pytest_report_teststatus(report): + if report.when in ("setup", "teardown"): + if report.failed: + # category, shortletter, verbose-word + return "error", "E", "ERROR" + elif report.skipped: + return "skipped", "s", "SKIPPED" + else: + return "", "", "" + + +# +# Implementation + +def call_and_report(item, when, log=True): + call = call_runtest_hook(item, when) + hook = item.ihook + report = hook.pytest_runtest_makereport(item=item, call=call) + if log and (when == "call" or not report.passed): + hook.pytest_runtest_logreport(report=report) + return report + +def call_runtest_hook(item, when): + hookname = "pytest_runtest_" + when + ihook = getattr(item.ihook, hookname) + return CallInfo(lambda: ihook(item=item), when=when) + +class CallInfo: + """ Result/Exception info a function invocation. """ + #: None or ExceptionInfo object. + excinfo = None + def __init__(self, func, when): + #: context of invocation: one of "setup", "call", + #: "teardown", "memocollect" + self.when = when + try: + self.result = func() + except KeyboardInterrupt: + raise + except: + self.excinfo = py.code.ExceptionInfo() + + def __repr__(self): + if self.excinfo: + status = "exception: %s" % str(self.excinfo.value) + else: + status = "result: %r" % (self.result,) + return "" % (self.when, status) + +class BaseReport(object): + def toterminal(self, out): + longrepr = self.longrepr + if hasattr(longrepr, 'toterminal'): + longrepr.toterminal(out) + else: + out.line(str(longrepr)) + + passed = property(lambda x: x.outcome == "passed") + failed = property(lambda x: x.outcome == "failed") + skipped = property(lambda x: x.outcome == "skipped") + + @property + def fspath(self): + return self.nodeid.split("::")[0] + +def pytest_runtest_makereport(item, call): + when = call.when + keywords = dict([(x,1) for x in item.keywords]) + if not call.excinfo: + outcome = "passed" + longrepr = None + else: + excinfo = call.excinfo + if not isinstance(excinfo, py.code.ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + longrepr = item._repr_failure_py(excinfo) + else: + outcome = "failed" + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: # exception in setup or teardown + longrepr = item._repr_failure_py(excinfo) + return TestReport(item.nodeid, item.location, + keywords, outcome, longrepr, when) + +class TestReport(BaseReport): + """ Basic test report object (also used for setup and teardown calls if + they fail). + """ + def __init__(self, nodeid, location, + keywords, outcome, longrepr, when): + #: normalized collection node id + self.nodeid = nodeid + + #: a (filesystempath, lineno, domaininfo) tuple indicating the + #: actual location of a test item - it might be different from the + #: collected one e.g. if a method is inherited from a different module. + self.location = location + + #: a name -> value dictionary containing all keywords and + #: markers associated with a test invocation. + self.keywords = keywords + + #: test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: one of 'setup', 'call', 'teardown' to indicate runtest phase. + self.when = when + + def __repr__(self): + return "" % ( + self.nodeid, self.when, self.outcome) + +class TeardownErrorReport(BaseReport): + outcome = "failed" + when = "teardown" + def __init__(self, longrepr): + self.longrepr = longrepr + +def pytest_make_collect_report(collector): + call = CallInfo(collector._memocollect, "memocollect") + reason = longrepr = None + if not call.excinfo: + outcome = "passed" + else: + if call.excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + reason = str(call.excinfo.value) + longrepr = collector._repr_failure_py(call.excinfo, "line") + else: + outcome = "failed" + errorinfo = collector.repr_failure(call.excinfo) + if not hasattr(errorinfo, "toterminal"): + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + return CollectReport(collector.nodeid, outcome, longrepr, + getattr(call, 'result', None), reason) + +class CollectReport(BaseReport): + def __init__(self, nodeid, outcome, longrepr, result, reason): + self.nodeid = nodeid + self.outcome = outcome + self.longrepr = longrepr + self.result = result or [] + self.reason = reason + + @property + def location(self): + return (self.fspath, None, self.fspath) + + def __repr__(self): + return "" % ( + self.nodeid, len(self.result), self.outcome) + +class CollectErrorRepr(TerminalRepr): + def __init__(self, msg): + self.longrepr = msg + def toterminal(self, out): + out.line(str(self.longrepr), red=True) + +class SetupState(object): + """ shared state for setting up/tearing down test items or collectors. """ + def __init__(self): + self.stack = [] + self._finalizers = {} + + def addfinalizer(self, finalizer, colitem): + """ attach a finalizer to the given colitem. + if colitem is None, this will add a finalizer that + is called at the end of teardown_all(). + """ + assert hasattr(finalizer, '__call__') + #assert colitem in self.stack + self._finalizers.setdefault(colitem, []).append(finalizer) + + def _pop_and_teardown(self): + colitem = self.stack.pop() + self._teardown_with_finalization(colitem) + + def _callfinalizers(self, colitem): + finalizers = self._finalizers.pop(colitem, None) + while finalizers: + fin = finalizers.pop() + fin() + + def _teardown_with_finalization(self, colitem): + self._callfinalizers(colitem) + if colitem: + colitem.teardown() + for colitem in self._finalizers: + assert colitem is None or colitem in self.stack + + def teardown_all(self): + while self.stack: + self._pop_and_teardown() + self._teardown_with_finalization(None) + assert not self._finalizers + + def teardown_exact(self, item): + if self.stack and item == self.stack[-1]: + self._pop_and_teardown() + else: + self._callfinalizers(item) + + def prepare(self, colitem): + """ setup objects along the collector chain to the test-method + and teardown previously setup objects.""" + needed_collectors = colitem.listchain() + while self.stack: + if self.stack == needed_collectors[:len(self.stack)]: + break + self._pop_and_teardown() + # check if the last collection node has raised an error + for col in self.stack: + if hasattr(col, '_prepare_exc'): + py.builtin._reraise(*col._prepare_exc) + for col in needed_collectors[len(self.stack):]: + self.stack.append(col) + try: + col.setup() + except Exception: + col._prepare_exc = sys.exc_info() + raise + +# ============================================================= +# Test OutcomeExceptions and helpers for creating them. + + +class OutcomeException(Exception): + """ OutcomeException and its subclass instances indicate and + contain info about test and collection outcomes. + """ + def __init__(self, msg=None): + self.msg = msg + + def __repr__(self): + if self.msg: + return str(self.msg) + return "<%s instance>" %(self.__class__.__name__,) + __str__ = __repr__ + +class Skipped(OutcomeException): + # XXX hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = 'builtins' + +class Failed(OutcomeException): + """ raised from an explicit call to py.test.fail() """ + __module__ = 'builtins' + +class Exit(KeyboardInterrupt): + """ raised for immediate program exits (no tracebacks/summaries)""" + def __init__(self, msg="unknown reason"): + self.msg = msg + KeyboardInterrupt.__init__(self, msg) + +# exposed helper methods + +def exit(msg): + """ exit testing process as if KeyboardInterrupt was triggered. """ + __tracebackhide__ = True + raise Exit(msg) + +exit.Exception = Exit + +def skip(msg=""): + """ skip an executing test with the given message. Note: it's usually + better to use the py.test.mark.skipif marker to declare a test to be + skipped under certain conditions like mismatching platforms or + dependencies. See the pytest_skipping plugin for details. + """ + __tracebackhide__ = True + raise Skipped(msg=msg) +skip.Exception = Skipped + +def fail(msg=""): + """ explicitely fail an currently-executing test with the given Message. """ + __tracebackhide__ = True + raise Failed(msg=msg) +fail.Exception = Failed + + +def importorskip(modname, minversion=None): + """ return imported module if it has a higher __version__ than the + optionally specified 'minversion' - otherwise call py.test.skip() + with a message detailing the mismatch. + """ + compile(modname, '', 'eval') # to catch syntaxerrors + try: + mod = __import__(modname, None, None, ['__doc__']) + except ImportError: + py.test.skip("could not import %r" %(modname,)) + if minversion is None: + return mod + verattr = getattr(mod, '__version__', None) + if isinstance(minversion, str): + minver = minversion.split(".") + else: + minver = list(minversion) + if verattr is None or verattr.split(".") < minver: + py.test.skip("module %r has __version__ %r, required is: %r" %( + modname, verattr, minversion)) + return mod --- /dev/null +++ b/testing/test_core.py @@ -0,0 +1,634 @@ +import py, os +from _pytest.core import PluginManager, canonical_importname +from _pytest.core import MultiCall, HookRelay, varnames + + +class TestBootstrapping: + def test_consider_env_fails_to_import(self, monkeypatch): + pluginmanager = PluginManager() + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + py.test.raises(ImportError, "pluginmanager.consider_env()") + + def test_preparse_args(self): + pluginmanager = PluginManager() + py.test.raises(ImportError, """ + pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) + """) + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(pytest_skipping1=""" + import py + py.test.skip("hello") + """) + p.copy(p.dirpath("pytest_skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*hint*skipping2*hello*", + "*hint*skipping1*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): + pluginmanager = PluginManager() + testdir.syspathinsert() + testdir.makepyfile(pytest_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('pytest_xy123') + pluginmanager.consider_env() + l3 = len(pluginmanager.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + 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 + plugin2 = pluginmanager.getplugin("pytest_mytestplugin") + assert plugin2 == plugin + + 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): + x500 = testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import py + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('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() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') + + reset = testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pluginmanager.import_plugin("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("hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir): + pluginmanager = PluginManager() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') + + reset = 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_plug1="#") + testdir.makepyfile(pytest_plug2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + pluginmanager.consider_module(mod) + assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" + assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" + + 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 = PluginManager() + reprec = testdir.getreportrecorder(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() + py.test.raises(ImportError, "pp.consider_conftest(mod)") + + def test_pm(self): + pp = PluginManager() + 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() + assert a1 in l + assert a2 in l + assert pp.getplugin('hello') == a2 + pp.unregister(a1) + assert not pp.isregistered(a1) + pp.unregister(name="hello") + assert not pp.isregistered(a2) + + 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 + py.test.raises(AssertionError, "pp.register(mod)") + mod2 = py.std.types.ModuleType("pytest_hello") + #pp.register(mod2) # double pm + py.test.raises(AssertionError, "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('xyz') + assert pp.getplugin('xyz') == mod + assert pp.getplugin('pytest_xyz') == mod + assert pp.isregistered(mod) + + def test_register_mismatch_method(self): + pp = PluginManager(load=True) + class hello: + def pytest_gurgel(self): + pass + py.test.raises(Exception, "pp.register(hello())") + + def test_register_mismatch_arg(self): + pp = PluginManager(load=True) + class hello: + def pytest_configure(self, asd): + pass + excinfo = py.test.raises(Exception, "pp.register(hello())") + + def test_canonical_importname(self): + for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': + impname = canonical_importname(name) + + def test_notify_exception(self, capfd): + pp = PluginManager() + excinfo = py.test.raises(ValueError, "raise ValueError(1)") + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert "ValueError" in err + class A: + def pytest_internalerror(self, excrepr): + return True + pp.register(A()) + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert not err + + def test_register(self): + pm = PluginManager(load=False) + class MyPlugin: + pass + my = MyPlugin() + pm.register(my) + assert pm.getplugins() + my2 = MyPlugin() + pm.register(my2) + assert pm.getplugins()[1:] == [my, my2] + + assert pm.isregistered(my) + assert pm.isregistered(my2) + pm.unregister(my) + assert not pm.isregistered(my) + assert pm.getplugins()[1:] == [my2] + + def test_listattr(self): + plugins = PluginManager() + 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_hook_tracing(self): + pm = PluginManager() + saveindent = [] + class api1: + x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pm.trace.root.indent) + raise ValueError(42) + l = [] + pm.trace.root.setwriter(l.append) + indent = pm.trace.root.indent + p = api1() + pm.register(p) + + assert pm.trace.root.indent == indent + assert len(l) == 1 + assert 'pytest_plugin_registered' in l[0] + py.test.raises(ValueError, lambda: pm.register(api1())) + assert pm.trace.root.indent == indent + assert saveindent[0] > indent + +class TestPytestPluginInteractions: + + def test_addhooks_conftestplugin(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + def pytest_myhook(xyz): + "new hook" + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + def pytest_myhook(xyz): + return xyz + 1 + """) + config = testdir.Config() + config._conftest.importconftest(conf) + print(config.pluginmanager.getplugins()) + res = config.hook.pytest_myhook(xyz=10) + assert res == [11] + + def test_addhooks_docstring_error(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + class A: # no pytest_ prefix + pass + def pytest_myhook(xyz): + pass + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*docstring*pytest_myhook*newhooks*" + ]) + + def test_addhooks_nohooks(self, testdir): + conf = testdir.makeconftest(""" + import sys + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(sys) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*did not find*sys*" + ]) + + def test_do_option_conftestplugin(self, testdir): + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true") + """) + config = testdir.Config() + config._conftest.importconftest(p) + print(config.pluginmanager.getplugins()) + config.parse([]) + assert not config.option.test123 + + def test_namespace_early_from_import(self, testdir): + p = testdir.makepyfile(""" + from pytest import Item + from pytest import Item as Item2 + assert Item is Item2 + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_do_ext_namespace(self, testdir): + testdir.makeconftest(""" + def pytest_namespace(): + return {'hello': 'world'} + """) + p = testdir.makepyfile(""" + from py.test import hello + import py + def test_hello(): + assert hello == "world" + assert 'hello' in py.test.__all__ + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_do_option_postinitialize(self, testdir): + config = testdir.Config() + config.parse([]) + config.pluginmanager.do_configure(config=config) + assert not hasattr(config.option, 'test123') + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true", + default=True) + """) + config._conftest.importconftest(p) + assert config.option.test123 + + def test_configure(self, testdir): + config = testdir.parseconfig() + l = [] + class A: + def pytest_configure(self, config): + l.append(self) + + config.pluginmanager.register(A()) + assert len(l) == 0 + config.pluginmanager.do_configure(config=config) + assert len(l) == 1 + config.pluginmanager.register(A()) # this should lead to a configured() plugin + assert len(l) == 2 + assert l[0] != l[1] + + config.pluginmanager.do_unconfigure(config=config) + config.pluginmanager.register(A()) + assert len(l) == 2 + + # lower level API + + def test_listattr(self): + pluginmanager = PluginManager() + class My2: + x = 42 + pluginmanager.register(My2()) + assert not pluginmanager.listattr("hello") + assert pluginmanager.listattr("x") == [42] + +def test_namespace_has_default_and_env_plugins(testdir): + p = testdir.makepyfile(""" + import py + py.test.mark + """) + result = testdir.runpython(p) + assert result.ret == 0 + +def test_varnames(): + def f(x): + i = 3 + class A: + def f(self, y): + pass + class B(object): + def __call__(self, z): + pass + assert varnames(f) == ("x",) + assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) + +class TestMultiCall: + def test_uses_copy_of_methods(self): + l = [lambda: 42] + mc = MultiCall(l, {}) + repr(mc) + l[:] = [] + res = mc.execute() + return res == 42 + + def test_call_passing(self): + class P1: + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.methods + return 17 + + class P2: + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.methods + return 23 + + p1 = P1() + p2 = P2() + multicall = MultiCall([p1.m, p2.m], {'x': 23}) + assert "23" in repr(multicall) + reslist = multicall.execute() + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + def test_keyword_args(self): + def f(x): + return x + 1 + class A: + def f(self, x, y): + return x + y + multicall = MultiCall([f, A().f], dict(x=23, y=24)) + assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) + reslist = multicall.execute() + assert reslist == [24+23, 24] + assert "2 results" in repr(multicall) + + def test_keyword_args_with_defaultargs(self): + def f(x, z=1): + return x + z + reslist = MultiCall([f], dict(x=23, y=24)).execute() + assert reslist == [24] + reslist = MultiCall([f], dict(x=23, z=2)).execute() + assert reslist == [25] + + def test_tags_call_error(self): + multicall = MultiCall([lambda x: x], {}) + py.test.raises(TypeError, "multicall.execute()") + + def test_call_subexecute(self): + def m(__multicall__): + subresult = __multicall__.execute() + return subresult + 1 + + def n(): + return 1 + + call = MultiCall([n, m], {}, firstresult=True) + res = call.execute() + assert res == 2 + + def test_call_none_is_no_result(self): + def m1(): + return 1 + def m2(): + return None + res = MultiCall([m1, m2], {}, firstresult=True).execute() + assert res == 1 + res = MultiCall([m1, m2], {}).execute() + assert res == [1] + +class TestHookRelay: + def test_happypath(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + assert hasattr(mcm, 'hello') + assert repr(mcm.hello).find("hello") != -1 + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + l = mcm.hello(arg=3) + assert l == [4] + assert not hasattr(mcm, 'world') + + def test_only_kwargs(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + py.test.raises(TypeError, "mcm.hello(3)") + + def test_firstresult_definition(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + hello.firstresult = True + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + res = mcm.hello(arg=3) + assert res == 4 + +class TestTracer: + def test_simple(self): + from _pytest.core import TagTracer + rootlogger = TagTracer("[my] ") + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "[my] world\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "[my] hello\n" + + def test_indent(self): + from _pytest.core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + l = [] + log.root.setwriter(lambda arg: l.append(arg)) + log("hello") + log.root.indent += 1 + log("line1") + log("line2") + log.root.indent += 1 + log("line3") + log("line4") + log.root.indent -= 1 + log("line5") + log.root.indent -= 1 + log("last") + assert len(l) == 7 + names = [x.rstrip()[len(rootlogger.prefix):] for x in l] + assert names == ['hello', ' line1', ' line2', + ' line3', ' line4', ' line5', 'last'] + + def test_setprocessor(self): + from _pytest.core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + + def test_setmyprocessor(self): + from _pytest.core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) --- /dev/null +++ b/_pytest/recwarn.py @@ -0,0 +1,96 @@ +""" recording warnings during test function execution. """ + +import py +import sys, os + +def pytest_funcarg__recwarn(request): + """Return a WarningsRecorder instance that provides these methods: + + * ``pop(category=None)``: return last warning matching the category. + * ``clear()``: clear list of warnings + """ + if sys.version_info >= (2,7): + import warnings + oldfilters = warnings.filters[:] + warnings.simplefilter('default') + def reset_filters(): + warnings.filters[:] = oldfilters + request.addfinalizer(reset_filters) + wrec = WarningsRecorder() + request.addfinalizer(wrec.finalize) + return wrec + +def pytest_namespace(): + return {'deprecated_call': deprecated_call} + +def deprecated_call(func, *args, **kwargs): + """ assert that calling func(*args, **kwargs) + triggers a DeprecationWarning. + """ + warningmodule = py.std.warnings + l = [] + oldwarn_explicit = getattr(warningmodule, 'warn_explicit') + def warn_explicit(*args, **kwargs): + l.append(args) + oldwarn_explicit(*args, **kwargs) + oldwarn = getattr(warningmodule, 'warn') + def warn(*args, **kwargs): + l.append(args) + oldwarn(*args, **kwargs) + + warningmodule.warn_explicit = warn_explicit + warningmodule.warn = warn + try: + ret = func(*args, **kwargs) + finally: + warningmodule.warn_explicit = warn_explicit + warningmodule.warn = warn + if not l: + #print warningmodule + __tracebackhide__ = True + raise AssertionError("%r did not produce DeprecationWarning" %(func,)) + return ret + + +class RecordedWarning: + def __init__(self, message, category, filename, lineno, line): + self.message = message + self.category = category + self.filename = filename + self.lineno = lineno + self.line = line + +class WarningsRecorder: + def __init__(self): + warningmodule = py.std.warnings + self.list = [] + def showwarning(message, category, filename, lineno, line=0): + self.list.append(RecordedWarning( + message, category, filename, lineno, line)) + try: + self.old_showwarning(message, category, + filename, lineno, line=line) + except TypeError: + # < python2.6 + self.old_showwarning(message, category, filename, lineno) + self.old_showwarning = warningmodule.showwarning + warningmodule.showwarning = showwarning + + def pop(self, cls=Warning): + """ pop the first recorded warning, raise exception if not exists.""" + for i, w in enumerate(self.list): + if issubclass(w.category, cls): + return self.list.pop(i) + __tracebackhide__ = True + assert 0, "%r not found in %r" %(cls, self.list) + + #def resetregistry(self): + # import warnings + # warnings.onceregistry.clear() + # warnings.__warningregistry__.clear() + + def clear(self): + self.list[:] = [] + + def finalize(self): + py.std.warnings.showwarning = self.old_showwarning --- /dev/null +++ b/_pytest/session.py @@ -0,0 +1,511 @@ +""" core implementation of testing process: init, session, runtest loop. """ + +import py +import pytest, _pytest +import os, sys +tracebackcutdir = py.path.local(_pytest.__file__).dirpath() + +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 + +def pytest_addoption(parser): + parser.addini("norecursedirs", "directory patterns to avoid for recursion", + type="args", default=('.*', 'CVS', '_darcs', '{arch}')) + #parser.addini("dirpatterns", + # "patterns specifying possible locations of test files", + # type="linelist", default=["**/test_*.txt", + # "**/test_*.py", "**/*_test.py"] + #) + group = parser.getgroup("general", "running and selection options") + group._addoption('-x', '--exitfirst', action="store_true", default=False, + dest="exitfirst", + help="exit instantly on first error or failed test."), + group._addoption('--maxfail', metavar="num", + action="store", type="int", dest="maxfail", default=0, + help="exit after first num failures or errors.") + + group = parser.getgroup("collect", "collection") + group.addoption('--collectonly', + action="store_true", dest="collectonly", + help="only collect tests, don't execute them."), + group.addoption('--pyargs', action="store_true", + help="try to interpret all arguments as python packages.") + group.addoption("--ignore", action="append", metavar="path", + help="ignore path during collection (multi-allowed).") + group.addoption('--confcutdir', dest="confcutdir", default=None, + metavar="dir", + help="only load conftest.py's relative to specified dir.") + + group = parser.getgroup("debugconfig", + "test process debugging and configuration") + group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", + help="base temporary directory for this test run.") + + +def pytest_namespace(): + return dict(collect=dict(Item=Item, Collector=Collector, File=File)) + +def pytest_configure(config): + py.test.config = config # compatibiltiy + if config.option.exitfirst: + config.option.maxfail = 1 + +def pytest_cmdline_main(config): + """ default command line protocol for initialization, session, + running tests and reporting. """ + session = Session(config) + session.exitstatus = EXIT_OK + try: + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=session) + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + except pytest.UsageError: + raise + except KeyboardInterrupt: + excinfo = py.code.ExceptionInfo() + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = EXIT_INTERRUPTED + except: + excinfo = py.code.ExceptionInfo() + config.pluginmanager.notify_exception(excinfo) + session.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + if not session.exitstatus and session._testsfailed: + session.exitstatus = EXIT_TESTSFAILED + config.hook.pytest_sessionfinish(session=session, + exitstatus=session.exitstatus) + config.pluginmanager.do_unconfigure(config) + return session.exitstatus + +def pytest_collection(session): + session.perform_collect() + hook = session.config.hook + hook.pytest_collection_modifyitems(session=session, + config=session.config, items=session.items) + hook.pytest_collection_finish(session=session) + return True + +def pytest_runtestloop(session): + if session.config.option.collectonly: + return True + for item in session.session.items: + item.config.hook.pytest_runtest_protocol(item=item) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True + +def pytest_ignore_collect(path, config): + p = path.dirpath() + ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) + ignore_paths = ignore_paths or [] + excludeopt = config.getvalue("ignore") + if excludeopt: + ignore_paths.extend([py.path.local(x) for x in excludeopt]) + return path in ignore_paths + +class HookProxy: + def __init__(self, fspath, config): + self.fspath = fspath + self.config = config + def __getattr__(self, name): + hookmethod = getattr(self.config.hook, name) + def call_matching_hooks(**kwargs): + plugins = self.config._getmatchingplugins(self.fspath) + return hookmethod.pcall(plugins, **kwargs) + return call_matching_hooks + +def compatproperty(name): + def fget(self): + #print "retrieving %r property from %s" %(name, self.fspath) + py.log._apiwarn("2.0", "use pytest.%s for " + "test collection and item classes" % name) + return getattr(pytest, name) + return property(fget, None, None, + "deprecated attribute %r, use pytest.%s" % (name,name)) + +class Node(object): + """ base class for all Nodes in the collection tree. + Collector subclasses have children, Items are terminal nodes.""" + + def __init__(self, name, parent=None, config=None, session=None): + #: a unique name with the scope of the parent + self.name = name + + #: the parent collector node. + self.parent = parent + + #: the test config object + self.config = config or parent.config + + #: the collection this node is part of + self.session = session or parent.session + + #: filesystem path where this node was collected from + self.fspath = getattr(parent, 'fspath', None) + self.ihook = self.session.gethookproxy(self.fspath) + self.keywords = {self.name: True} + + Module = compatproperty("Module") + Class = compatproperty("Class") + Function = compatproperty("Function") + File = compatproperty("File") + Item = compatproperty("Item") + + def __repr__(self): + return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) + + # methods for ordering nodes + @property + def nodeid(self): + try: + return self._nodeid + except AttributeError: + self._nodeid = x = self._makeid() + return x + + def _makeid(self): + return self.parent.nodeid + "::" + self.name + + def __eq__(self, other): + if not isinstance(other, Node): + return False + return self.__class__ == other.__class__ and \ + self.name == other.name and self.parent == other.parent + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.name, self.parent)) + + def setup(self): + pass + + def teardown(self): + pass + + def _memoizedcall(self, attrname, function): + exattrname = "_ex_" + attrname + failure = getattr(self, exattrname, None) + if failure is not None: + py.builtin._reraise(failure[0], failure[1], failure[2]) + if hasattr(self, attrname): + return getattr(self, attrname) + try: + res = function() + except py.builtin._sysex: + raise + except: + failure = py.std.sys.exc_info() + setattr(self, exattrname, failure) + raise + setattr(self, attrname, res) + return res + + def listchain(self): + """ return list of all parent collectors up to self, + starting from root of collection tree. """ + l = [self] + while 1: + x = l[0] + if x.parent is not None: # and x.parent.parent is not None: + l.insert(0, x.parent) + else: + return l + + def listnames(self): + return [x.name for x in self.listchain()] + + def getparent(self, cls): + current = self + while current and not isinstance(current, cls): + current = current.parent + return current + + def _prunetraceback(self, excinfo): + pass + + def _repr_failure_py(self, excinfo, style=None): + if self.config.option.fulltrace: + style="long" + else: + self._prunetraceback(excinfo) + # XXX should excinfo.getrepr record all data and toterminal() + # process it? + if style is None: + if self.config.option.tbstyle == "short": + style = "short" + else: + style = "long" + return excinfo.getrepr(funcargs=True, + showlocals=self.config.option.showlocals, + style=style) + + repr_failure = _repr_failure_py + +class Collector(Node): + """ Collector instances create children through collect() + and thus iteratively build a tree. + """ + class CollectError(Exception): + """ an error during collection, contains a custom message. """ + + def collect(self): + """ returns a list of children (items and collectors) + for this collection node. + """ + raise NotImplementedError("abstract") + + def repr_failure(self, excinfo): + """ represent a collection failure. """ + if excinfo.errisinstance(self.CollectError): + exc = excinfo.value + return str(exc.args[0]) + return self._repr_failure_py(excinfo, style="short") + + def _memocollect(self): + """ internal helper method to cache results of calling collect(). """ + return self._memoizedcall('_collected', lambda: list(self.collect())) + + def _prunetraceback(self, excinfo): + if hasattr(self, 'fspath'): + path = self.fspath + traceback = excinfo.traceback + ntraceback = traceback.cut(path=self.fspath) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + excinfo.traceback = ntraceback.filter() + +class FSCollector(Collector): + def __init__(self, fspath, parent=None, config=None, session=None): + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + name = fspath.basename + if parent is not None: + rel = fspath.relto(parent.fspath) + if rel: + name = rel + name = name.replace(os.sep, "/") + super(FSCollector, self).__init__(name, parent, config, session) + self.fspath = fspath + + def _makeid(self): + if self == self.session: + return "." + relpath = self.session.fspath.bestrelpath(self.fspath) + if os.sep != "/": + relpath = relpath.replace(os.sep, "/") + return relpath + +class File(FSCollector): + """ base class for collecting tests from a file. """ + +class Item(Node): + """ a basic test invocation item. Note that for a single function + there might be multiple test invocation items. + """ + def reportinfo(self): + return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + location = (str(location[0]), location[1], str(location[2])) + self._location = location + return location + +class NoMatch(Exception): + """ raised if matching cannot locate a matching names. """ + +class Session(FSCollector): + class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + __module__ = 'builtins' # for py3 + + def __init__(self, config): + super(Session, self).__init__(py.path.local(), parent=None, + config=config, session=self) + self.config.pluginmanager.register(self, name="session", prepend=True) + self._testsfailed = 0 + self.shouldstop = False + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") + + def pytest_collectstart(self): + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + def pytest_runtest_logreport(self, report): + if report.failed and 'xfail' not in getattr(report, 'keywords', []): + self._testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self._testsfailed >= maxfail: + self.shouldstop = "stopping after %d failures" % ( + self._testsfailed) + pytest_collectreport = pytest_runtest_logreport + + def isinitpath(self, path): + return path in self._initialpaths + + def gethookproxy(self, fspath): + return HookProxy(fspath, self.config) + + def perform_collect(self, args=None, genitems=True): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + self._initialpaths = set() + self._initialparts = [] + for arg in args: + parts = self._parsearg(arg) + self._initialparts.append(parts) + self._initialpaths.add(parts[0]) + self.ihook.pytest_collectstart(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + for arg, exc in self._notfound: + line = "(no name %r in any of %r)" % (arg, exc.args[0]) + raise pytest.UsageError("not found: %s\n%s" %(arg, line)) + if not genitems: + return rep.result + else: + self.items = items = [] + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for parts in self._initialparts: + arg = "::".join(map(str, parts)) + self.trace("processing argument", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + self.trace.root.indent -= 1 + break + self.trace.root.indent -= 1 + + def _collect(self, arg): + names = self._parsearg(arg) + path = names.pop(0) + if path.check(dir=1): + assert not names, "invalid arg %r" %(arg,) + for path in path.visit(rec=self._recurse, bf=True, sort=True): + for x in self._collectfile(path): + yield x + else: + assert path.check(file=1) + for x in self.matchnodes(self._collectfile(path), names): + yield x + + def _collectfile(self, path): + ihook = self.gethookproxy(path) + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return + for pat in self._norecursepatterns: + if path.check(fnmatch=pat): + return False + ihook.pytest_collect_directory(path=path, parent=self) + return True + + def _tryconvertpyarg(self, x): + try: + mod = __import__(x, None, None, ['__doc__']) + except (ValueError, ImportError): + return x + p = py.path.local(mod.__file__) + if p.purebasename == "__init__": + p = p.dirpath() + else: + p = p.new(basename=p.purebasename+".py") + return p + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + arg = str(arg) + if self.config.option.pyargs: + arg = self._tryconvertpyarg(arg) + parts = str(arg).split("::") + relpath = parts[0].replace("/", os.sep) + path = self.fspath.join(relpath, abs=True) + if not path.check(): + if self.config.option.pyargs: + msg = "file or package not found: " + else: + msg = "file not found: " + raise pytest.UsageError(msg + arg) + parts[0] = path + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, pytest.Item): + resultnodes.append(node) + continue + assert isinstance(node, pytest.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for x in rep.result: + if x.name == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + self.trace("genitems", node) + if isinstance(node, pytest.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, pytest.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for subnode in rep.result: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) + +Session = Session --- /dev/null +++ b/pytest.py @@ -0,0 +1,17 @@ +""" +unit and functional testing with Python. + +see http://pytest.org for documentation and details + +(c) Holger Krekel and others, 2004-2010 +""" +__version__ = '2.0.0.dev26' +__all__ = ['main'] + +from _pytest.core import main, UsageError, _preloadplugins +from _pytest import core as cmdline + +if __name__ == '__main__': # if run as a script or by 'python -m pytest' + raise SystemExit(main()) +else: + _preloadplugins() # to populate pytest.* namespace so help(pytest) works --- a/testing/plugin/test_capture.py +++ b/testing/plugin/test_capture.py @@ -1,5 +1,5 @@ import py, os, sys -from pytest.plugin.capture import CaptureManager +from _pytest.capture import CaptureManager needsosdup = py.test.mark.xfail("not hasattr(os, 'dup')") --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -26,14 +26,16 @@ New Features - new invocations through Python interpreter and from Python:: - python -m pytest # on all pythons >= 2.7 - python -m pytest.main # on all pythons >= 2.5 + python -m pytest # on all pythons >= 2.5 + + or from a python program:: + import pytest ; pytest.main(args, plugins) see http://pytest.org/2.0.0/usage.html for details. - new and better reporting information in assert expressions - which compare lists, sequences or strings. + if comparing lists, sequences or strings. see http://pytest.org/2.0.0/assert.html for details. @@ -42,7 +44,7 @@ New Features [pytest] norecursedirs = .hg data* # don't ever recurse in such dirs - addopts = -x --pyargs # add these options by default + addopts = -x --pyargs # add these command line options by default see http://pytest.org/2.0.0/customize.html @@ -51,9 +53,10 @@ New Features py.test --pyargs unittest -- add a new "-q" option which decreases verbosity and prints a more +- new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. + Fixes ----------------------- --- a/testing/plugin/test_assertion.py +++ b/testing/plugin/test_assertion.py @@ -1,7 +1,7 @@ import sys import py -import pytest.plugin.assertion as plugin +import _pytest.assertion as plugin needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") --- /dev/null +++ b/_pytest/core.py @@ -0,0 +1,451 @@ +""" +pytest PluginManager, basic initialization and tracing. +All else is in pytest/plugin. +(c) Holger Krekel 2004-2010 +""" +import sys, os +import inspect +import py +from _pytest import hookspec # the extension point definitions + +assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " + "%s is too old, remove or upgrade 'py'" % (py.__version__)) + +default_plugins = ( + "config session terminal runner python pdb capture unittest mark skipping " + "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " + "junitxml doctest").split() + +IMPORTPREFIX = "pytest_" + +class TagTracer: + def __init__(self, prefix="[pytest] "): + self._tag2proc = {} + self.writer = None + self.indent = 0 + self.prefix = prefix + + def get(self, name): + return TagTracerSub(self, (name,)) + + def processmessage(self, tags, args): + if self.writer is not None: + if args: + indent = " " * self.indent + content = " ".join(map(str, args)) + self.writer("%s%s%s\n" %(self.prefix, indent, content)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + 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,)) + +class PluginManager(object): + def __init__(self, load=False): + self._name2plugin = {} + self._plugins = [] + self._hints = [] + self.trace = TagTracer().get("pluginmanage") + if os.environ.get('PYTEST_DEBUG'): + err = sys.stderr + encoding = getattr(err, 'encoding', 'utf8') + try: + err = py.io.dupfile(err, encoding=encoding) + except Exception: + pass + self.trace.root.setwriter(err.write) + self.hook = HookRelay([hookspec], pm=self) + self.register(self) + if load: + for spec in default_plugins: + self.import_plugin(spec) + + def _getpluginname(self, plugin, name): + if name is None: + if hasattr(plugin, '__name__'): + name = plugin.__name__.split(".")[-1] + else: + name = id(plugin) + return name + + def register(self, plugin, name=None, prepend=False): + assert not self.isregistered(plugin), plugin + assert not self.isregistered(plugin), plugin + name = self._getpluginname(plugin, name) + if name in self._name2plugin: + return False + self._name2plugin[name] = plugin + self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) + self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) + return True + + def unregister(self, plugin=None, name=None): + if plugin is None: + plugin = self.getplugin(name=name) + self._plugins.remove(plugin) + self.hook.pytest_plugin_unregistered(plugin=plugin) + for name, value in list(self._name2plugin.items()): + if value == plugin: + del self._name2plugin[name] + + def isregistered(self, plugin, name=None): + if self._getpluginname(plugin, name) in self._name2plugin: + return True + for val in self._name2plugin.values(): + if plugin == val: + return True + + def addhooks(self, spec): + self.hook._addhooks(spec, prefix="pytest_") + + def getplugins(self): + return list(self._plugins) + + def skipifmissing(self, name): + if not self.hasplugin(name): + py.test.skip("plugin %r is missing" % name) + + def hasplugin(self, name): + try: + self.getplugin(name) + return True + except KeyError: + return False + + def getplugin(self, name): + try: + return self._name2plugin[name] + except KeyError: + impname = canonical_importname(name) + return self._name2plugin[impname] + + # API for bootstrapping + # + def _envlist(self, varname): + val = py.std.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 + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = canonical_importname(ep.name) + if name in self._name2plugin: + continue + plugin = ep.load() + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.import_plugin(opt2) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__): + 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, spec): + assert isinstance(spec, str) + modname = canonical_importname(spec) + if modname in self._name2plugin: + return + try: + mod = importplugin(modname) + except KeyboardInterrupt: + raise + except: + e = py.std.sys.exc_info()[1] + if not hasattr(py.test, 'skip'): + raise + elif not isinstance(e, py.test.skip.Exception): + raise + self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + self.register(mod, modname) + self.consider_module(mod) + + def pytest_plugin_registered(self, plugin): + import pytest + dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + self._setns(pytest, dic) + if hasattr(self, '_config'): + self.call_plugin(plugin, "pytest_addoption", + {'parser': self._config._parser}) + self.call_plugin(plugin, "pytest_configure", + {'config': self._config}) + + def _setns(self, obj, dic): + import pytest + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + modname = "pytest.%s" % name + mod = py.std.types.ModuleType(modname) + sys.modules[modname] = mod + mod.__all__ = [] + setattr(obj, name, mod) + obj.__all__.append(name) + self._setns(mod, value) + else: + setattr(obj, name, value) + obj.__all__.append(name) + #if obj != pytest: + # pytest.__all__.append(name) + setattr(pytest, name, value) + + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + if terminalreporter.config.option.traceconfig: + for hint in self._hints: + tw.line("hint: %s" % hint) + + def do_addoption(self, parser): + mname = "pytest_addoption" + methods = reversed(self.listattr(mname)) + MultiCall(methods, {'parser': parser}).execute() + + def do_configure(self, config): + assert not hasattr(self, '_config') + self._config = config + config.hook.pytest_configure(config=self._config) + + def do_unconfigure(self, config): + config = self._config + del self._config + config.hook.pytest_unconfigure(config=config) + config.pluginmanager.unregister(self) + + def notify_exception(self, excinfo): + excrepr = excinfo.getrepr(funcargs=True, showlocals=True) + res = self.hook.pytest_internalerror(excrepr=excrepr) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + + def listattr(self, attrname, plugins=None): + if plugins is None: + plugins = self._plugins + l = [] + for plugin in plugins: + try: + l.append(getattr(plugin, attrname)) + except AttributeError: + continue + return l + + def call_plugin(self, plugin, methname, kwargs): + return MultiCall(methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() + +def canonical_importname(name): + if '.' in name: + return name + name = name.lower() + if not name.startswith(IMPORTPREFIX): + name = IMPORTPREFIX + name + return name + +def importplugin(importspec): + #print "importing", importspec + try: + return __import__(importspec, None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(importspec) == -1: + raise + name = importspec + try: + if name.startswith("pytest_"): + name = importspec[7:] + return __import__("_pytest.%s" %(name), None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(name) == -1: + raise + # show the original exception, not the failing internal one + return __import__(importspec, None, None, '__doc__') + + +class MultiCall: + """ execute a call into multiple python functions/methods. """ + def __init__(self, methods, kwargs, firstresult=False): + self.methods = list(methods) + self.kwargs = kwargs + 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): + while self.methods: + method = self.methods.pop() + kwargs = self.getkwargs(method) + res = method(**kwargs) + if res is not None: + self.results.append(res) + if self.firstresult: + return res + if not self.firstresult: + return self.results + + def getkwargs(self, method): + kwargs = {} + for argname in varnames(method): + try: + kwargs[argname] = self.kwargs[argname] + except KeyError: + if argname == "__multicall__": + kwargs[argname] = self + return kwargs + +def varnames(func): + if not inspect.isfunction(func) and not inspect.ismethod(func): + func = getattr(func, '__call__', func) + ismethod = inspect.ismethod(func) + rawcode = py.code.getrawcode(func) + try: + return rawcode.co_varnames[ismethod:rawcode.co_argcount] + except AttributeError: + return () + +class HookRelay: + def __init__(self, hookspecs, pm, prefix="pytest_"): + if not isinstance(hookspecs, list): + hookspecs = [hookspecs] + self._hookspecs = [] + self._pm = pm + self.trace = pm.trace.root.get("hook") + for hookspec in hookspecs: + self._addhooks(hookspec, prefix) + + def _addhooks(self, hookspecs, prefix): + self._hookspecs.append(hookspecs) + added = False + for name, method in vars(hookspecs).items(): + if name.startswith(prefix): + if not method.__doc__: + raise ValueError("docstring required for hook %r, in %r" + % (method, hookspecs)) + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self, name, firstresult=firstresult) + 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, hookspecs,)) + + +class HookCaller: + def __init__(self, hookrelay, name, firstresult): + self.hookrelay = hookrelay + self.name = name + self.firstresult = firstresult + self.trace = self.hookrelay.trace + + def __repr__(self): + return "" %(self.name,) + + def __call__(self, **kwargs): + methods = self.hookrelay._pm.listattr(self.name) + return self._docall(methods, kwargs) + + def pcall(self, plugins, **kwargs): + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) + return self._docall(methods, kwargs) + + def _docall(self, methods, kwargs): + self.trace(self.name, kwargs) + self.trace.root.indent += 1 + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + try: + res = mc.execute() + if res: + self.trace("finish", self.name, "-->", res) + finally: + self.trace.root.indent -= 1 + return res + +_preinit = [] + +def _preloadplugins(): + _preinit.append(PluginManager(load=True)) + +def main(args=None, plugins=None): + """ returned exit code integer, after an in-process testing run + with the given command line arguments, preloading an optional list + of passed in plugin objects. """ + if args is None: + args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] + elif not isinstance(args, (tuple, list)): + if not isinstance(args, str): + raise ValueError("not a string or argument list: %r" % (args,)) + args = py.std.shlex.split(args) + if _preinit: + _pluginmanager = _preinit.pop(0) + else: # subsequent calls to main will create a fresh instance + _pluginmanager = PluginManager(load=True) + hook = _pluginmanager.hook + try: + if plugins: + for plugin in plugins: + _pluginmanager.register(plugin) + config = hook.pytest_cmdline_parse( + pluginmanager=_pluginmanager, args=args) + exitstatus = hook.pytest_cmdline_main(config=config) + except UsageError: + e = sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n" %(e.args[0],)) + exitstatus = 3 + return exitstatus + +class UsageError(Exception): + """ error in py.test usage or invocation""" + --- a/pytest/plugin/recwarn.py +++ /dev/null @@ -1,96 +0,0 @@ -""" recording warnings during test function execution. """ - -import py -import sys, os - -def pytest_funcarg__recwarn(request): - """Return a WarningsRecorder instance that provides these methods: - - * ``pop(category=None)``: return last warning matching the category. - * ``clear()``: clear list of warnings - """ - if sys.version_info >= (2,7): - import warnings - oldfilters = warnings.filters[:] - warnings.simplefilter('default') - def reset_filters(): - warnings.filters[:] = oldfilters - request.addfinalizer(reset_filters) - wrec = WarningsRecorder() - request.addfinalizer(wrec.finalize) - return wrec - -def pytest_namespace(): - return {'deprecated_call': deprecated_call} - -def deprecated_call(func, *args, **kwargs): - """ assert that calling func(*args, **kwargs) - triggers a DeprecationWarning. - """ - warningmodule = py.std.warnings - l = [] - oldwarn_explicit = getattr(warningmodule, 'warn_explicit') - def warn_explicit(*args, **kwargs): - l.append(args) - oldwarn_explicit(*args, **kwargs) - oldwarn = getattr(warningmodule, 'warn') - def warn(*args, **kwargs): - l.append(args) - oldwarn(*args, **kwargs) - - warningmodule.warn_explicit = warn_explicit - warningmodule.warn = warn - try: - ret = func(*args, **kwargs) - finally: - warningmodule.warn_explicit = warn_explicit - warningmodule.warn = warn - if not l: - #print warningmodule - __tracebackhide__ = True - raise AssertionError("%r did not produce DeprecationWarning" %(func,)) - return ret - - -class RecordedWarning: - def __init__(self, message, category, filename, lineno, line): - self.message = message - self.category = category - self.filename = filename - self.lineno = lineno - self.line = line - -class WarningsRecorder: - def __init__(self): - warningmodule = py.std.warnings - self.list = [] - def showwarning(message, category, filename, lineno, line=0): - self.list.append(RecordedWarning( - message, category, filename, lineno, line)) - try: - self.old_showwarning(message, category, - filename, lineno, line=line) - except TypeError: - # < python2.6 - self.old_showwarning(message, category, filename, lineno) - self.old_showwarning = warningmodule.showwarning - warningmodule.showwarning = showwarning - - def pop(self, cls=Warning): - """ pop the first recorded warning, raise exception if not exists.""" - for i, w in enumerate(self.list): - if issubclass(w.category, cls): - return self.list.pop(i) - __tracebackhide__ = True - assert 0, "%r not found in %r" %(cls, self.list) - - #def resetregistry(self): - # import warnings - # warnings.onceregistry.clear() - # warnings.__warningregistry__.clear() - - def clear(self): - self.list[:] = [] - - def finalize(self): - py.std.warnings.showwarning = self.old_showwarning --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -180,28 +180,28 @@ py.test default plugin reference .. autosummary:: - pytest.plugin.assertion - pytest.plugin.capture - pytest.plugin.config - pytest.plugin.doctest - pytest.plugin.genscript - pytest.plugin.helpconfig - pytest.plugin.junitxml - pytest.plugin.mark - pytest.plugin.monkeypatch - pytest.plugin.nose - pytest.plugin.pastebin - pytest.plugin.pdb - pytest.plugin.pytester - pytest.plugin.python - pytest.plugin.recwarn - pytest.plugin.resultlog - pytest.plugin.runner - pytest.plugin.session - pytest.plugin.skipping - pytest.plugin.terminal - pytest.plugin.tmpdir - pytest.plugin.unittest + _pytest.assertion + _pytest.capture + _pytest.config + _pytest.doctest + _pytest.genscript + _pytest.helpconfig + _pytest.junitxml + _pytest.mark + _pytest.monkeypatch + _pytest.nose + _pytest.pastebin + _pytest.pdb + _pytest.pytester + _pytest.python + _pytest.recwarn + _pytest.resultlog + _pytest.runner + _pytest.session + _pytest.skipping + _pytest.terminal + _pytest.tmpdir + _pytest.unittest .. _`well specified hooks`: @@ -222,7 +222,7 @@ hook name itself you get useful errors. initialisation, command line and configuration hooks -------------------------------------------------------------------- -.. currentmodule:: pytest.hookspec +.. currentmodule:: _pytest.hookspec .. autofunction:: pytest_cmdline_parse .. autofunction:: pytest_namespace @@ -243,11 +243,11 @@ All all runtest related hooks receive a .. autofunction:: pytest_runtest_makereport For deeper understanding you may look at the default implementation of -these hooks in :py:mod:`pytest.plugin.runner` and maybe also -in :py:mod:`pytest.plugin.pdb` which intercepts creation +these hooks in :py:mod:`_pytest.runner` and maybe also +in :py:mod:`_pytest.pdb` which intercepts creation of reports in order to drop to interactive debugging. -The :py:mod:`pytest.plugin.terminal` reported specifically uses +The :py:mod:`_pytest.terminal` reported specifically uses the reporting hook to print information about a test run. collection hooks @@ -284,35 +284,35 @@ test execution: Reference of important objects involved in hooks =========================================================== -.. autoclass:: pytest.plugin.config.Config +.. autoclass:: _pytest.config.Config :members: -.. autoclass:: pytest.plugin.config.Parser +.. autoclass:: _pytest.config.Parser :members: -.. autoclass:: pytest.plugin.session.Node(name, parent) +.. autoclass:: _pytest.session.Node(name, parent) :members: .. - .. autoclass:: pytest.plugin.session.File(fspath, parent) + .. autoclass:: _pytest.session.File(fspath, parent) :members: - .. autoclass:: pytest.plugin.session.Item(name, parent) + .. autoclass:: _pytest.session.Item(name, parent) :members: - .. autoclass:: pytest.plugin.python.Module(name, parent) + .. autoclass:: _pytest.python.Module(name, parent) :members: - .. autoclass:: pytest.plugin.python.Class(name, parent) + .. autoclass:: _pytest.python.Class(name, parent) :members: - .. autoclass:: pytest.plugin.python.Function(name, parent) + .. autoclass:: _pytest.python.Function(name, parent) :members: -.. autoclass:: pytest.plugin.runner.CallInfo +.. autoclass:: _pytest.runner.CallInfo :members: -.. autoclass:: pytest.plugin.runner.TestReport +.. autoclass:: _pytest.runner.TestReport :members: --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,5 +1,5 @@ import py -from pytest.plugin.config import Conftest +from _pytest.config import Conftest def pytest_generate_tests(metafunc): if "basedir" in metafunc.funcargnames: @@ -110,7 +110,7 @@ def test_conftest_global_import(testdir) testdir.makeconftest("x=3") p = testdir.makepyfile(""" import py - from pytest.plugin.config import Conftest + from _pytest.config import Conftest conf = Conftest() mod = conf.importconftest(py.path.local("conftest.py")) assert mod.x == 3 --- a/testing/plugin/test_resultlog.py +++ b/testing/plugin/test_resultlog.py @@ -1,11 +1,11 @@ import py import os -from pytest.plugin.resultlog import generic_path, ResultLog, \ +from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure -from pytest.plugin.session import Node, Item, FSCollector +from _pytest.session import Node, Item, FSCollector def test_generic_path(testdir): - from pytest.plugin.session import Session + from _pytest.session import Session config = testdir.parseconfig() session = Session(config) p1 = Node('a', config=config, session=session) --- a/testing/plugin/test_skipping.py +++ b/testing/plugin/test_skipping.py @@ -1,8 +1,8 @@ import py -from pytest.plugin.skipping import MarkEvaluator, folded_skips -from pytest.plugin.skipping import pytest_runtest_setup -from pytest.plugin.runner import runtestprotocol +from _pytest.skipping import MarkEvaluator, folded_skips +from _pytest.skipping import pytest_runtest_setup +from _pytest.runner import runtestprotocol class TestEvaluator: def test_no_marker(self, testdir): --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,6 +1,6 @@ import py -from pytest.plugin.config import getcfg, Config +from _pytest.config import getcfg, Config class TestParseIni: def test_getcfg_and_config(self, tmpdir): --- /dev/null +++ b/_pytest/capture.py @@ -0,0 +1,216 @@ +""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ + +import py +import os + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption('--capture', action="store", default=None, + metavar="method", type="choice", choices=['fd', 'sys', 'no'], + help="per-test capturing method: one of fd (default)|sys|no.") + group._addoption('-s', action="store_const", const="no", dest="capture", + help="shortcut for --capture=no.") + +def addouterr(rep, outerr): + repr = getattr(rep, 'longrepr', None) + if not hasattr(repr, 'addsection'): + return + for secname, content in zip(["out", "err"], outerr): + if content: + repr.addsection("Captured std%s" % secname, content.rstrip()) + +def pytest_configure(config): + config.pluginmanager.register(CaptureManager(), 'capturemanager') + +def pytest_unconfigure(config): + capman = config.pluginmanager.getplugin('capturemanager') + while capman._method2capture: + name, cap = capman._method2capture.popitem() + cap.reset() + +class NoCapture: + def startall(self): + pass + def resume(self): + pass + def reset(self): + pass + def suspend(self): + return "", "" + +class CaptureManager: + def __init__(self): + self._method2capture = {} + + def _maketempfile(self): + f = py.std.tempfile.TemporaryFile() + newf = py.io.dupfile(f, encoding="UTF-8") + f.close() + return newf + + def _makestringio(self): + return py.io.TextIO() + + def _getcapture(self, method): + if method == "fd": + return py.io.StdCaptureFD(now=False, + out=self._maketempfile(), err=self._maketempfile() + ) + elif method == "sys": + return py.io.StdCapture(now=False, + out=self._makestringio(), err=self._makestringio() + ) + elif method == "no": + return NoCapture() + else: + raise ValueError("unknown capturing method: %r" % method) + + def _getmethod(self, config, fspath): + if config.option.capture: + method = config.option.capture + else: + try: + method = config._conftest.rget("option_capture", path=fspath) + except KeyError: + method = "fd" + if method == "fd" and not hasattr(os, 'dup'): # e.g. jython + method = "sys" + return method + + def resumecapture_item(self, item): + method = self._getmethod(item.config, item.fspath) + if not hasattr(item, 'outerr'): + item.outerr = ('', '') # we accumulate outerr on the item + return self.resumecapture(method) + + def resumecapture(self, method): + if hasattr(self, '_capturing'): + raise ValueError("cannot resume, already capturing with %r" % + (self._capturing,)) + cap = self._method2capture.get(method) + self._capturing = method + if cap is None: + self._method2capture[method] = cap = self._getcapture(method) + cap.startall() + else: + cap.resume() + + def suspendcapture(self, item=None): + self.deactivate_funcargs() + if hasattr(self, '_capturing'): + method = self._capturing + cap = self._method2capture.get(method) + if cap is not None: + outerr = cap.suspend() + del self._capturing + if item: + outerr = (item.outerr[0] + outerr[0], + item.outerr[1] + outerr[1]) + return outerr + if hasattr(item, 'outerr'): + return item.outerr + return "", "" + + def activate_funcargs(self, pyfuncitem): + if not hasattr(pyfuncitem, 'funcargs'): + return + assert not hasattr(self, '_capturing_funcargs') + self._capturing_funcargs = capturing_funcargs = [] + for name, capfuncarg in pyfuncitem.funcargs.items(): + if name in ('capsys', 'capfd'): + capturing_funcargs.append(capfuncarg) + capfuncarg._start() + + def deactivate_funcargs(self): + capturing_funcargs = getattr(self, '_capturing_funcargs', None) + if capturing_funcargs is not None: + while capturing_funcargs: + capfuncarg = capturing_funcargs.pop() + capfuncarg._finalize() + del self._capturing_funcargs + + def pytest_make_collect_report(self, __multicall__, collector): + method = self._getmethod(collector.config, collector.fspath) + try: + self.resumecapture(method) + except ValueError: + return # recursive collect, XXX refactor capturing + # to allow for more lightweight recursive capturing + try: + rep = __multicall__.execute() + finally: + outerr = self.suspendcapture() + addouterr(rep, outerr) + return rep + + def pytest_runtest_setup(self, item): + self.resumecapture_item(item) + + def pytest_runtest_call(self, item): + self.resumecapture_item(item) + self.activate_funcargs(item) + + def pytest_runtest_teardown(self, item): + self.resumecapture_item(item) + + def pytest__teardown_final(self, __multicall__, session): + method = self._getmethod(session.config, None) + self.resumecapture(method) + try: + rep = __multicall__.execute() + finally: + outerr = self.suspendcapture() + if rep: + addouterr(rep, outerr) + return rep + + def pytest_keyboard_interrupt(self, excinfo): + if hasattr(self, '_capturing'): + self.suspendcapture() + + def pytest_runtest_makereport(self, __multicall__, item, call): + self.deactivate_funcargs() + rep = __multicall__.execute() + outerr = self.suspendcapture(item) + if not rep.passed: + addouterr(rep, outerr) + if not rep.passed or rep.when == "teardown": + outerr = ('', '') + item.outerr = outerr + return rep + +def pytest_funcarg__capsys(request): + """captures writes to sys.stdout/sys.stderr and makes + them available successively via a ``capsys.readouterr()`` method + which returns a ``(out, err)`` tuple of captured snapshot strings. + """ + return CaptureFuncarg(py.io.StdCapture) + +def pytest_funcarg__capfd(request): + """captures writes to file descriptors 1 and 2 and makes + snapshotted ``(out, err)`` string tuples available + via the ``capsys.readouterr()`` method. If the underlying + platform does not have ``os.dup`` (e.g. Jython) tests using + this funcarg will automatically skip. + """ + if not hasattr(os, 'dup'): + py.test.skip("capfd funcarg needs os.dup") + return CaptureFuncarg(py.io.StdCaptureFD) + +class CaptureFuncarg: + def __init__(self, captureclass): + self.capture = captureclass(now=False) + + def _start(self): + self.capture.startall() + + def _finalize(self): + if hasattr(self, 'capture'): + self.capture.reset() + del self.capture + + def readouterr(self): + return self.capture.readouterr() + + def close(self): + self._finalize() --- a/testing/plugin/test_pytester.py +++ b/testing/plugin/test_pytester.py @@ -1,7 +1,7 @@ import py import os, sys -from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder -from pytest.main import PluginManager +from _pytest.pytester import LineMatcher, LineComp, HookRecorder +from _pytest.core import PluginManager def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -97,7 +97,7 @@ def test_hookrecorder_basic_no_args_hook def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" import py - from pytest.main import HookRelay, PluginManager + from _pytest.core import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): class ApiClass: --- a/pytest/plugin/helpconfig.py +++ /dev/null @@ -1,158 +0,0 @@ -""" version info, help messages, tracing configuration. """ -import py -import pytest -import inspect, sys - -def pytest_addoption(parser): - group = parser.getgroup('debugconfig') - group.addoption('--version', action="store_true", - help="display pytest lib version and import information.") - group._addoption("-h", "--help", action="store_true", dest="help", - help="show help message and configuration info") - group._addoption('-p', action="append", dest="plugins", default = [], - metavar="name", - help="early-load given plugin (multi-allowed).") - group.addoption('--traceconfig', - action="store_true", dest="traceconfig", default=False, - help="trace considerations of conftest.py files."), - group._addoption('--nomagic', - action="store_true", dest="nomagic", default=False, - help="don't reinterpret asserts, no traceback cutting. ") - group.addoption('--debug', - action="store_true", dest="debug", default=False, - help="generate and show internal debugging information.") - - -def pytest_cmdline_main(config): - if config.option.version: - p = py.path.local(pytest.__file__).dirpath() - sys.stderr.write("This is py.test version %s, imported from %s\n" % - (pytest.__version__, p)) - return 0 - elif config.option.help: - config.pluginmanager.do_configure(config) - showhelp(config) - return 0 - -def showhelp(config): - tw = py.io.TerminalWriter() - tw.write(config._parser.optparser.format_help()) - tw.line() - tw.line() - #tw.sep( "=", "config file settings") - tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") - tw.line() - - for name in config._parser._ininames: - help, type, default = config._parser._inidict[name] - if type is None: - type = "string" - spec = "%s (%s)" % (name, type) - line = " %-24s %s" %(spec, help) - tw.line(line[:tw.fullwidth]) - - tw.line() ; tw.line() - #tw.sep("=") - return - - tw.line("conftest.py options:") - tw.line() - conftestitems = sorted(config._parser._conftestdict.items()) - for name, help in conftest_options + conftestitems: - line = " %-15s %s" %(name, help) - tw.line(line[:tw.fullwidth]) - tw.line() - #tw.sep( "=") - -conftest_options = [ - ('pytest_plugins', 'list of plugin names to load'), -] - -def pytest_report_header(config): - lines = [] - if config.option.debug or config.option.traceconfig: - lines.append("using: pytest-%s pylib-%s" % - (pytest.__version__,py.__version__)) - - if config.option.traceconfig: - lines.append("active plugins:") - plugins = [] - items = config.pluginmanager._name2plugin.items() - for name, plugin in items: - lines.append(" %-20s: %s" %(name, repr(plugin))) - return lines - - -# ===================================================== -# validate plugin syntax and hooks -# ===================================================== - -def pytest_plugin_registered(manager, plugin): - methods = collectattr(plugin) - hooks = {} - for hookspec in manager.hook._hookspecs: - hooks.update(collectattr(hookspec)) - - stringio = py.io.TextIO() - def Print(*args): - if args: - stringio.write(" ".join(map(str, args))) - stringio.write("\n") - - fail = False - while methods: - name, method = methods.popitem() - #print "checking", name - if isgenerichook(name): - continue - if name not in hooks: - if not getattr(method, 'optionalhook', False): - Print("found unknown hook:", name) - fail = True - else: - #print "checking", method - method_args = getargs(method) - #print "method_args", method_args - if '__multicall__' in method_args: - method_args.remove('__multicall__') - hook = hooks[name] - hookargs = getargs(hook) - for arg in method_args: - if arg not in hookargs: - Print("argument %r not available" %(arg, )) - Print("actual definition: %s" %(formatdef(method))) - Print("available hook arguments: %s" % - ", ".join(hookargs)) - fail = True - break - #if not fail: - # print "matching hook:", formatdef(method) - if fail: - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue())) - -class PluginValidationError(Exception): - """ plugin failed validation. """ - -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") - -def getargs(func): - args = inspect.getargs(py.code.getrawcode(func))[0] - startindex = inspect.ismethod(func) and 1 or 0 - return args[startindex:] - -def collectattr(obj): - methods = {} - for apiname in dir(obj): - if apiname.startswith("pytest_"): - methods[apiname] = getattr(obj, apiname) - return methods - -def formatdef(func): - return "%s%s" %( - func.__name__, - inspect.formatargspec(*inspect.getargspec(func)) - ) - --- a/pytest/plugin/assertion.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -support for presented detailed information in failing assertions. -""" -import py -import sys -from pytest.plugin.monkeypatch import monkeypatch - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group._addoption('--no-assert', action="store_true", default=False, - dest="noassert", - help="disable python assert expression reinterpretation."), - -def pytest_configure(config): - # The _pytesthook attribute on the AssertionError is used by - # py._code._assertionnew to detect this plugin was loaded and in - # turn call the hooks defined here as part of the - # DebugInterpreter. - config._monkeypatch = m = monkeypatch() - if not config.getvalue("noassert") and not config.getvalue("nomagic"): - warn_about_missing_assertion() - def callbinrepr(op, left, right): - hook_result = config.hook.pytest_assertrepr_compare( - config=config, op=op, left=left, right=right) - for new_expl in hook_result: - if new_expl: - return '\n~'.join(new_expl) - m.setattr(py.builtin.builtins, - 'AssertionError', py.code._AssertionError) - m.setattr(py.code, '_reprcompare', callbinrepr) - -def pytest_unconfigure(config): - config._monkeypatch.undo() - -def warn_about_missing_assertion(): - try: - assert False - except AssertionError: - pass - else: - py.std.warnings.warn("Assertions are turned off!" - " (are you using python -O?)") - - -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - -def pytest_assertrepr_compare(op, left, right): - """return specialised explanations for some operators/operands""" - left_repr = py.io.saferepr(left, maxsize=30) - right_repr = py.io.saferepr(right, maxsize=30) - summary = '%s %s %s' % (left_repr, op, right_repr) - - issequence = lambda x: isinstance(x, (list, tuple)) - istext = lambda x: isinstance(x, basestring) - isdict = lambda x: isinstance(x, dict) - isset = lambda x: isinstance(x, set) - - explanation = None - try: - if op == '==': - if istext(left) and istext(right): - explanation = _diff_text(left, right) - elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right) - elif isdict(left) and isdict(right): - explanation = _diff_text(py.std.pprint.pformat(left), - py.std.pprint.pformat(right)) - except py.builtin._sysex: - raise - except: - excinfo = py.code.ExceptionInfo() - explanation = ['(pytest_assertion plugin: representation of ' - 'details failed. Probably an object has a faulty __repr__.)', - str(excinfo) - ] - - - if not explanation: - return None - - # Don't include pageloads of data, should be configurable - if len(''.join(explanation)) > 80*8: - explanation = ['Detailed information too verbose, truncated'] - - return [summary] + explanation - - -def _diff_text(left, right): - """Return the explanation for the diff between text - - This will skip leading and trailing characters which are - identical to keep the diff minimal. - """ - explanation = [] - i = 0 # just in case left or right has zero length - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - break - if i > 42: - i -= 10 # Provide some context - explanation = ['Skipping %s identical ' - 'leading characters in diff' % i] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += ['Skipping %s identical ' - 'trailing characters in diff' % i] - left = left[:-i] - right = right[:-i] - explanation += [line.strip('\n') - for line in py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] - return explanation - - -def _compare_eq_sequence(left, right): - explanation = [] - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (i, left[i], right[i])] - break - if len(left) > len(right): - explanation += ['Left contains more items, ' - 'first extra item: %s' % py.io.saferepr(left[len(right)],)] - elif len(left) < len(right): - explanation += ['Right contains more items, ' - 'first extra item: %s' % py.io.saferepr(right[len(left)],)] - return explanation # + _diff_text(py.std.pprint.pformat(left), - # py.std.pprint.pformat(right)) - - -def _compare_eq_set(left, right): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append('Extra items in the left set:') - for item in diff_left: - explanation.append(py.io.saferepr(item)) - if diff_right: - explanation.append('Extra items in the right set:') - for item in diff_right: - explanation.append(py.io.saferepr(item)) - return explanation --- /dev/null +++ b/_pytest/standalonetemplate.py @@ -0,0 +1,63 @@ +#! /usr/bin/env python + +sources = """ + at SOURCES@""" + +import sys +import base64 +import zlib +import imp + +class DictImporter(object): + def __init__(self, sources): + self.sources = sources + + def find_module(self, fullname, path=None): + if fullname in self.sources: + return self + if fullname+'.__init__' in self.sources: + return self + return None + + def load_module(self, fullname): + # print "load_module:", fullname + from types import ModuleType + try: + s = self.sources[fullname] + is_pkg = False + except KeyError: + s = self.sources[fullname+'.__init__'] + is_pkg = True + + co = compile(s, fullname, 'exec') + module = sys.modules.setdefault(fullname, ModuleType(fullname)) + module.__file__ = "%s/%s" % (__file__, fullname) + module.__loader__ = self + if is_pkg: + module.__path__ = [fullname] + + do_exec(co, module.__dict__) + return sys.modules[fullname] + + def get_source(self, name): + res = self.sources.get(name) + if res is None: + res = self.sources.get(name+'.__init__') + return res + +if __name__ == "__main__": + if sys.version_info >= (3,0): + exec("def do_exec(co, loc): exec(co, loc)\n") + import pickle + sources = sources.encode("ascii") # ensure bytes + sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) + else: + import cPickle as pickle + exec("def do_exec(co, loc): exec co in loc\n") + sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) + + importer = DictImporter(sources) + sys.meta_path.append(importer) + + entry = "@ENTRY@" + do_exec(entry, locals()) --- a/doc/example/nonpython/conftest.py +++ b/doc/example/nonpython/conftest.py @@ -1,6 +1,6 @@ # content of conftest.py -import py +import pytest def pytest_collect_file(path, parent): if path.ext == ".yml" and path.basename.startswith("test"): --- a/pytest/plugin/doctest.py +++ /dev/null @@ -1,79 +0,0 @@ -""" discover and run doctests in modules and test files.""" - -import pytest, py -from py._code.code import TerminalRepr, ReprFileLocation - -def pytest_addoption(parser): - group = parser.getgroup("collect") - group.addoption("--doctest-modules", - action="store_true", default=False, - help="run doctests in all .py modules", - dest="doctestmodules") - group.addoption("--doctest-glob", - action="store", default="test*.txt", metavar="pat", - help="doctests file matching pattern, default: test*.txt", - dest="doctestglob") - -def pytest_collect_file(path, parent): - config = parent.config - if path.ext == ".py": - if config.option.doctestmodules: - return DoctestModule(path, parent) - elif path.check(fnmatch=config.getvalue("doctestglob")): - return DoctestTextfile(path, parent) - -class ReprFailDoctest(TerminalRepr): - def __init__(self, reprlocation, lines): - self.reprlocation = reprlocation - self.lines = lines - def toterminal(self, tw): - for line in self.lines: - tw.line(line) - self.reprlocation.toterminal(tw) - -class DoctestItem(pytest.Item): - def __init__(self, path, parent): - name = self.__class__.__name__ + ":" + path.basename - super(DoctestItem, self).__init__(name=name, parent=parent) - self.fspath = path - - def repr_failure(self, excinfo): - if excinfo.errisinstance(py.std.doctest.DocTestFailure): - doctestfailure = excinfo.value - example = doctestfailure.example - test = doctestfailure.test - filename = test.filename - lineno = test.lineno + example.lineno + 1 - message = excinfo.type.__name__ - reprlocation = ReprFileLocation(filename, lineno, message) - checker = py.std.doctest.OutputChecker() - REPORT_UDIFF = py.std.doctest.REPORT_UDIFF - filelines = py.path.local(filename).readlines(cr=0) - i = max(test.lineno, max(0, lineno - 10)) # XXX? - lines = [] - for line in filelines[i:lineno]: - lines.append("%03d %s" % (i+1, line)) - i += 1 - lines += checker.output_difference(example, - doctestfailure.got, REPORT_UDIFF).split("\n") - return ReprFailDoctest(reprlocation, lines) - elif excinfo.errisinstance(py.std.doctest.UnexpectedException): - excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) - return super(DoctestItem, self).repr_failure(excinfo) - else: - return super(DoctestItem, self).repr_failure(excinfo) - - def reportinfo(self): - return self.fspath, None, "[doctest]" - -class DoctestTextfile(DoctestItem): - def runtest(self): - failed, tot = py.std.doctest.testfile( - str(self.fspath), module_relative=False, - raise_on_error=True, verbose=0) - -class DoctestModule(DoctestItem): - def runtest(self): - module = self.fspath.pyimport() - failed, tot = py.std.doctest.testmod( - module, raise_on_error=True, verbose=0) --- /dev/null +++ b/_pytest/python.py @@ -0,0 +1,823 @@ +""" Python test discovery, setup and run of test functions. """ +import py +import inspect +import sys +import pytest +from py._code.code import TerminalRepr + +import _pytest +cutdir = py.path.local(_pytest.__file__).dirpath() + +def pytest_addoption(parser): + group = parser.getgroup("general") + group.addoption('--funcargs', + action="store_true", dest="showfuncargs", default=False, + help="show available function arguments, sorted by plugin") + +def pytest_cmdline_main(config): + if config.option.showfuncargs: + showfuncargs(config) + return 0 + +def pytest_namespace(__multicall__): + __multicall__.execute() + raises.Exception = pytest.fail.Exception + return { + 'raises' : raises, + 'collect': { + 'Module': Module, 'Class': Class, 'Instance': Instance, + 'Function': Function, 'Generator': Generator, + '_fillfuncargs': fillfuncargs} + } + +def pytest_funcarg__pytestconfig(request): + """ the pytest config object with access to command line opts.""" + return request.config + +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): + testfunction = pyfuncitem.obj + if pyfuncitem._isyieldedfunction(): + testfunction(*pyfuncitem._args) + else: + funcargs = pyfuncitem.funcargs + testfunction(**funcargs) + +def pytest_collect_file(path, parent): + ext = path.ext + pb = path.purebasename + if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or + parent.session.isinitpath(path)): + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) + +def pytest_pycollect_makemodule(path, parent): + return Module(path, parent) + +def pytest_pycollect_makeitem(__multicall__, collector, name, obj): + res = __multicall__.execute() + if res is not None: + return res + if collector._istestclasscandidate(name, obj): + if hasattr(collector.obj, 'unittest'): + return # we assume it's a mixin class for a TestCase derived one + return Class(name, parent=collector) + elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): + if is_generator(obj): + return Generator(name, parent=collector) + else: + return collector._genfunctions(name, obj) + +def is_generator(func): + try: + return py.code.getrawcode(func).co_flags & 32 # generator function + except AttributeError: # builtin functions have no bytecode + # assume them to not be generators + return False + +class PyobjMixin(object): + def obj(): + def fget(self): + try: + return self._obj + except AttributeError: + self._obj = obj = self._getobj() + return obj + def fset(self, value): + self._obj = value + return property(fget, fset, None, "underlying python object") + obj = obj() + + def _getobj(self): + return getattr(self.parent.obj, self.name) + + def getmodpath(self, stopatmodule=True, includemodule=False): + """ return python path relative to the containing module. """ + chain = self.listchain() + chain.reverse() + parts = [] + for node in chain: + if isinstance(node, Instance): + continue + name = node.name + if isinstance(node, Module): + assert name.endswith(".py") + name = name[:-3] + if stopatmodule: + if includemodule: + parts.append(name) + break + parts.append(name) + parts.reverse() + s = ".".join(parts) + return s.replace(".[", "[") + + def _getfslineno(self): + try: + return self._fslineno + except AttributeError: + pass + obj = self.obj + # xxx let decorators etc specify a sane ordering + if hasattr(obj, 'place_as'): + obj = obj.place_as + + self._fslineno = py.code.getfslineno(obj) + return self._fslineno + + def reportinfo(self): + # XXX caching? + obj = self.obj + if hasattr(obj, 'compat_co_firstlineno'): + # nose compatibility + fspath = sys.modules[obj.__module__].__file__ + if fspath.endswith(".pyc"): + fspath = fspath[:-1] + #assert 0 + #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + lineno = obj.compat_co_firstlineno + modpath = obj.__module__ + else: + fspath, lineno = self._getfslineno() + modpath = self.getmodpath() + return fspath, lineno, modpath + +class PyCollectorMixin(PyobjMixin, pytest.Collector): + + def funcnamefilter(self, name): + return name.startswith('test') + def classnamefilter(self, name): + return name.startswith('Test') + + def collect(self): + # NB. we avoid random getattrs and peek in the __dict__ instead + # (XXX originally introduced from a PyPy need, still true?) + dicts = [getattr(self.obj, '__dict__', {})] + for basecls in inspect.getmro(self.obj.__class__): + dicts.append(basecls.__dict__) + seen = {} + l = [] + for dic in dicts: + for name, obj in dic.items(): + if name in seen: + continue + seen[name] = True + if name[0] != "_": + res = self.makeitem(name, obj) + if res is None: + continue + if not isinstance(res, list): + res = [res] + l.extend(res) + l.sort(key=lambda item: item.reportinfo()[:2]) + return l + + def makeitem(self, name, obj): + return self.ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj) + + def _istestclasscandidate(self, name, obj): + if self.classnamefilter(name) and \ + inspect.isclass(obj): + if hasinit(obj): + # XXX WARN + return False + return True + + def _genfunctions(self, name, funcobj): + module = self.getparent(Module).obj + clscol = self.getparent(Class) + cls = clscol and clscol.obj or None + metafunc = Metafunc(funcobj, config=self.config, + cls=cls, module=module) + gentesthook = self.config.hook.pytest_generate_tests + plugins = getplugins(self, withpy=True) + gentesthook.pcall(plugins, metafunc=metafunc) + if not metafunc._calls: + return Function(name, parent=self) + l = [] + for callspec in metafunc._calls: + subname = "%s[%s]" %(name, callspec.id) + function = Function(name=subname, parent=self, + callspec=callspec, callobj=funcobj, keywords={callspec.id:True}) + l.append(function) + return l + +class Module(pytest.File, PyCollectorMixin): + def _getobj(self): + return self._memoizedcall('_obj', self._importtestmodule) + + def _importtestmodule(self): + # we assume we are only called once per module + try: + mod = self.fspath.pyimport(ensuresyspath=True) + except SyntaxError: + excinfo = py.code.ExceptionInfo() + raise self.CollectError(excinfo.getrepr(style="short")) + except self.fspath.ImportMismatchError: + e = sys.exc_info()[1] + raise self.CollectError( + "import file mismatch:\n" + "imported module %r has this __file__ attribute:\n" + " %s\n" + "which is not the same as the test file we want to collect:\n" + " %s\n" + "HINT: use a unique basename for your test file modules" + % e.args + ) + #print "imported test module", mod + self.config.pluginmanager.consider_module(mod) + return mod + + def setup(self): + if hasattr(self.obj, 'setup_module'): + #XXX: nose compat hack, move to nose plugin + # if it takes a positional arg, its probably a pytest style one + # so we pass the current module object + if inspect.getargspec(self.obj.setup_module)[0]: + self.obj.setup_module(self.obj) + else: + self.obj.setup_module() + + def teardown(self): + if hasattr(self.obj, 'teardown_module'): + #XXX: nose compat hack, move to nose plugin + # if it takes a positional arg, its probably a py.test style one + # so we pass the current module object + if inspect.getargspec(self.obj.teardown_module)[0]: + self.obj.teardown_module(self.obj) + else: + self.obj.teardown_module() + +class Class(PyCollectorMixin, pytest.Collector): + + def collect(self): + return [Instance(name="()", parent=self)] + + def setup(self): + setup_class = getattr(self.obj, 'setup_class', None) + if setup_class is not None: + setup_class = getattr(setup_class, 'im_func', setup_class) + setup_class(self.obj) + + def teardown(self): + teardown_class = getattr(self.obj, 'teardown_class', None) + if teardown_class is not None: + teardown_class = getattr(teardown_class, 'im_func', teardown_class) + teardown_class(self.obj) + +class Instance(PyCollectorMixin, pytest.Collector): + def _getobj(self): + return self.parent.obj() + + def newinstance(self): + self.obj = self._getobj() + return self.obj + +class FunctionMixin(PyobjMixin): + """ mixin for the code common to Function and Generator. + """ + + def setup(self): + """ perform setup for this test function. """ + if inspect.ismethod(self.obj): + name = 'setup_method' + else: + name = 'setup_function' + if isinstance(self.parent, Instance): + obj = self.parent.newinstance() + self.obj = self._getobj() + else: + obj = self.parent.obj + setup_func_or_method = getattr(obj, name, None) + if setup_func_or_method is not None: + setup_func_or_method(self.obj) + + def teardown(self): + """ perform teardown for this test function. """ + if inspect.ismethod(self.obj): + name = 'teardown_method' + else: + name = 'teardown_function' + obj = self.parent.obj + teardown_func_or_meth = getattr(obj, name, None) + if teardown_func_or_meth is not None: + teardown_func_or_meth(self.obj) + + def _prunetraceback(self, excinfo): + if hasattr(self, '_obj') and not self.config.option.fulltrace: + code = py.code.Code(self.obj) + path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback + ntraceback = traceback.cut(path=path, firstlineno=firstlineno) + if ntraceback == traceback: + ntraceback = ntraceback.cut(path=path) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=cutdir) + excinfo.traceback = ntraceback.filter() + + def _repr_failure_py(self, excinfo, style="long"): + if excinfo.errisinstance(FuncargRequest.LookupError): + fspath, lineno, msg = self.reportinfo() + lines, _ = inspect.getsourcelines(self.obj) + for i, line in enumerate(lines): + if line.strip().startswith('def'): + return FuncargLookupErrorRepr(fspath, lineno, + lines[:i+1], str(excinfo.value)) + return super(FunctionMixin, self)._repr_failure_py(excinfo, + style=style) + + def repr_failure(self, excinfo, outerr=None): + assert outerr is None, "XXX outerr usage is deprecated" + return self._repr_failure_py(excinfo, + style=self.config.option.tbstyle) + +class FuncargLookupErrorRepr(TerminalRepr): + def __init__(self, filename, firstlineno, deflines, errorstring): + self.deflines = deflines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + + def toterminal(self, tw): + tw.line() + for line in self.deflines: + tw.line(" " + line.strip()) + for line in self.errorstring.split("\n"): + tw.line(" " + line.strip(), red=True) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + +class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): + def collect(self): + # test generators are seen as collectors but they also + # invoke setup/teardown on popular request + # (induced by the common "test_*" naming shared with normal tests) + self.config._setupstate.prepare(self) + + l = [] + seen = {} + for i, x in enumerate(self.obj()): + name, call, args = self.getcallargs(x) + if not py.builtin.callable(call): + raise TypeError("%r yielded non callable test %r" %(self.obj, call,)) + if name is None: + name = "[%d]" % i + else: + name = "['%s']" % name + if name in seen: + raise ValueError("%r generated tests with non-unique name %r" %(self, name)) + seen[name] = True + l.append(Function(name, self, args=args, callobj=call)) + return l + + def getcallargs(self, obj): + if not isinstance(obj, (tuple, list)): + obj = (obj,) + # explict naming + if isinstance(obj[0], py.builtin._basestring): + name = obj[0] + obj = obj[1:] + else: + name = None + call, args = obj[0], obj[1:] + return name, call, args + + +# +# Test Items +# +_dummy = object() +class Function(FunctionMixin, pytest.Item): + """ a Function Item is responsible for setting up + and executing a Python callable test object. + """ + _genid = None + def __init__(self, name, parent=None, args=None, config=None, + callspec=None, callobj=_dummy, keywords=None, session=None): + super(Function, self).__init__(name, parent, + config=config, session=session) + self._args = args + if self._isyieldedfunction(): + assert not callspec, ( + "yielded functions (deprecated) cannot have funcargs") + else: + if callspec is not None: + self.funcargs = callspec.funcargs or {} + self._genid = callspec.id + if hasattr(callspec, "param"): + self._requestparam = callspec.param + else: + self.funcargs = {} + if callobj is not _dummy: + self._obj = callobj + self.function = getattr(self.obj, 'im_func', self.obj) + self.keywords.update(py.builtin._getfuncdict(self.obj) or {}) + if keywords: + self.keywords.update(keywords) + + def _getobj(self): + name = self.name + i = name.find("[") # parametrization + if i != -1: + name = name[:i] + return getattr(self.parent.obj, name) + + def _isyieldedfunction(self): + return self._args is not None + + def runtest(self): + """ execute the underlying test function. """ + self.ihook.pytest_pyfunc_call(pyfuncitem=self) + + def setup(self): + super(Function, self).setup() + if hasattr(self, 'funcargs'): + fillfuncargs(self) + + def __eq__(self, other): + try: + return (self.name == other.name and + self._args == other._args and + self.parent == other.parent and + self.obj == other.obj and + getattr(self, '_genid', None) == + getattr(other, '_genid', None) + ) + except AttributeError: + pass + return False + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.parent, self.name)) + +def hasinit(obj): + init = getattr(obj, '__init__', None) + if init: + if init != object.__init__: + return True + + +def getfuncargnames(function): + # XXX merge with main.py's varnames + argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] + startindex = py.std.inspect.ismethod(function) and 1 or 0 + defaults = getattr(function, 'func_defaults', + getattr(function, '__defaults__', None)) or () + numdefaults = len(defaults) + if numdefaults: + return argnames[startindex:-numdefaults] + return argnames[startindex:] + +def fillfuncargs(function): + """ fill missing funcargs. """ + request = FuncargRequest(pyfuncitem=function) + request._fillfuncargs() + +def getplugins(node, withpy=False): # might by any node + plugins = node.config._getmatchingplugins(node.fspath) + if withpy: + mod = node.getparent(pytest.Module) + if mod is not None: + plugins.append(mod.obj) + inst = node.getparent(pytest.Instance) + if inst is not None: + plugins.append(inst.obj) + return plugins + +_notexists = object() +class CallSpec: + def __init__(self, funcargs, id, param): + self.funcargs = funcargs + self.id = id + if param is not _notexists: + self.param = param + def __repr__(self): + return "" %( + self.id, getattr(self, 'param', '?'), self.funcargs) + +class Metafunc: + def __init__(self, function, config=None, cls=None, module=None): + self.config = config + self.module = module + self.function = function + self.funcargnames = getfuncargnames(function) + self.cls = cls + self.module = module + self._calls = [] + self._ids = py.builtin.set() + + def addcall(self, funcargs=None, id=_notexists, param=_notexists): + """ add a new call to the underlying test function during the + collection phase of a test run. + + :arg funcargs: argument keyword dictionary used when invoking + the test function. + + :arg id: used for reporting and identification purposes. If you + don't supply an `id` the length of the currently + list of calls to the test function will be used. + + :arg param: will be exposed to a later funcarg factory invocation + through the ``request.param`` attribute. Setting it (instead of + directly providing a ``funcargs`` ditionary) is called + *indirect parametrization*. Indirect parametrization is + preferable if test values are expensive to setup or can + only be created after certain fixtures or test-run related + initialization code has been run. + """ + assert funcargs is None or isinstance(funcargs, dict) + if id is None: + raise ValueError("id=None not allowed") + if id is _notexists: + id = len(self._calls) + id = str(id) + if id in self._ids: + raise ValueError("duplicate id %r" % id) + self._ids.add(id) + self._calls.append(CallSpec(funcargs, id, param)) + +class FuncargRequest: + """ A request for function arguments from a test function. """ + _argprefix = "pytest_funcarg__" + _argname = None + + class LookupError(LookupError): + """ error on performing funcarg request. """ + + def __init__(self, pyfuncitem): + self._pyfuncitem = pyfuncitem + if hasattr(pyfuncitem, '_requestparam'): + self.param = pyfuncitem._requestparam + self._plugins = getplugins(pyfuncitem, withpy=True) + self._funcargs = self._pyfuncitem.funcargs.copy() + self._name2factory = {} + self._currentarg = None + + @property + def function(self): + """ function object of the test invocation. """ + return self._pyfuncitem.obj + + @property + def keywords(self): + """ keywords of the test function item. + + .. versionadded:: 2.0 + """ + return self._pyfuncitem.keywords + + @property + def module(self): + """ module where the test function was collected. """ + return self._pyfuncitem.getparent(pytest.Module).obj + + @property + def cls(self): + """ class (can be None) where the test function was collected. """ + clscol = self._pyfuncitem.getparent(pytest.Class) + if clscol: + return clscol.obj + @property + def instance(self): + """ instance (can be None) on which test function was collected. """ + return py.builtin._getimself(self.function) + + @property + def config(self): + """ the pytest config object associated with this request. """ + return self._pyfuncitem.config + + @property + def fspath(self): + """ the file system path of the test module which collected this test. """ + return self._pyfuncitem.fspath + + def _fillfuncargs(self): + argnames = getfuncargnames(self.function) + if argnames: + assert not getattr(self._pyfuncitem, '_args', None), ( + "yielded functions cannot have funcargs") + for argname in argnames: + if argname not in self._pyfuncitem.funcargs: + self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) + + + def applymarker(self, marker): + """ apply a marker to a single test function invocation. + This method is useful if you don't want to have a keyword/marker + on all function invocations. + + :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object + created by a call to ``py.test.mark.NAME(...)``. + """ + if not isinstance(marker, py.test.mark.XYZ.__class__): + raise ValueError("%r is not a py.test.mark.* object") + self._pyfuncitem.keywords[marker.markname] = marker + + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + """ return a testing resource managed by ``setup`` & + ``teardown`` calls. ``scope`` and ``extrakey`` determine when the + ``teardown`` function will be called so that subsequent calls to + ``setup`` would recreate the resource. + + :arg teardown: function receiving a previously setup resource. + :arg setup: a no-argument function creating a resource. + :arg scope: a string value out of ``function``, ``module`` or + ``session`` indicating the caching lifecycle of the resource. + :arg extrakey: added to internal caching key of (funcargname, scope). + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self._addfinalizer(finalizer, scope=scope) + return val + + def getfuncargvalue(self, argname): + """ Retrieve a function argument by name for this test + function invocation. This allows one function argument factory + to call another function argument factory. If there are two + funcarg factories for the same test function argument the first + factory may use ``getfuncargvalue`` to call the second one and + do something additional with the resource. + """ + try: + return self._funcargs[argname] + except KeyError: + pass + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + self._currentarg = argname + try: + self._funcargs[argname] = res = funcargfactory(request=self) + finally: + self._currentarg = oldarg + return res + + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "module": + return self._pyfuncitem.getparent(pytest.Module) + elif scope == "session": + return None + raise ValueError("unknown finalization scope %r" %(scope,)) + + def addfinalizer(self, finalizer): + """add finalizer function to be called after test function + finished execution. """ + self._addfinalizer(finalizer, scope="function") + + def _addfinalizer(self, finalizer, scope): + colitem = self._getscopeitem(scope) + self.config._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem) + + def __repr__(self): + return "" %(self._pyfuncitem) + + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self._pyfuncitem.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) + +def showfuncargs(config): + from _pytest.session import Session + session = Session(config) + session.perform_collect() + if session.items: + plugins = getplugins(session.items[0]) + else: + plugins = getplugins(session) + curdir = py.path.local() + tw = py.io.TerminalWriter() + verbose = config.getvalue("verbose") + for plugin in plugins: + available = [] + for name, factory in vars(plugin).items(): + if name.startswith(FuncargRequest._argprefix): + name = name[len(FuncargRequest._argprefix):] + if name not in available: + available.append([name, factory]) + if available: + pluginname = plugin.__name__ + for name, factory in available: + loc = getlocation(factory, curdir) + if verbose: + funcargspec = "%s -- %s" %(name, loc,) + else: + funcargspec = name + tw.line(funcargspec, green=True) + doc = factory.__doc__ or "" + if doc: + for line in doc.split("\n"): + tw.line(" " + line.strip()) + else: + tw.line(" %s: no docstring available" %(loc,), + red=True) + +def getlocation(function, curdir): + import inspect + fn = py.path.local(inspect.getfile(function)) + lineno = py.builtin._getcode(function).co_firstlineno + if fn.relto(curdir): + fn = fn.relto(curdir) + return "%s:%d" %(fn, lineno+1) + +# builtin pytest.raises helper + +def raises(ExpectedException, *args, **kwargs): + """ assert that a code block/function call raises an exception. + + If using Python 2.5 or above, you may use this function as a + context manager:: + + >>> with raises(ZeroDivisionError): + ... 1/0 + + Or you can one of two forms: + + if args[0] is callable: raise AssertionError if calling it with + the remaining arguments does not raise the expected exception. + if args[0] is a string: raise AssertionError if executing the + the string in the calling scope does not raise expected exception. + examples: + >>> x = 5 + >>> raises(TypeError, lambda x: x + 'hello', x=x) + >>> raises(TypeError, "x + 'hello'") + """ + __tracebackhide__ = True + + if not args: + return RaisesContext(ExpectedException) + elif isinstance(args[0], str): + code, = args + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + #print "raises frame scope: %r" % frame.f_locals + try: + code = py.code.Source(code).compile() + py.builtin.exec_(code, frame.f_globals, loc) + # XXX didn'T mean f_globals == f_locals something special? + # this is destroyed here ... + except ExpectedException: + return py.code.ExceptionInfo() + else: + func = args[0] + try: + func(*args[1:], **kwargs) + except ExpectedException: + return py.code.ExceptionInfo() + k = ", ".join(["%s=%r" % x for x in kwargs.items()]) + if k: + k = ', ' + k + expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) + pytest.fail("DID NOT RAISE") + +class RaisesContext(object): + def __init__(self, ExpectedException): + self.ExpectedException = ExpectedException + self.excinfo = None + + def __enter__(self): + self.excinfo = object.__new__(py.code.ExceptionInfo) + return self.excinfo + + def __exit__(self, *tp): + __tracebackhide__ = True + if tp[0] is None: + pytest.fail("DID NOT RAISE") + self.excinfo.__init__(tp) + return issubclass(self.excinfo.type, self.ExpectedException) --- /dev/null +++ b/_pytest/unittest.py @@ -0,0 +1,52 @@ +""" discovery and running of std-library "unittest" style tests. """ +import pytest, py +import sys + +def pytest_pycollect_makeitem(collector, name, obj): + unittest = sys.modules.get('unittest') + if unittest is None: + return # nobody can have derived unittest.TestCase + try: + isunit = issubclass(obj, unittest.TestCase) + except KeyboardInterrupt: + raise + except Exception: + pass + else: + if isunit: + return UnitTestCase(name, parent=collector) + +class UnitTestCase(pytest.Class): + def collect(self): + loader = py.std.unittest.TestLoader() + for name in loader.getTestCaseNames(self.obj): + yield TestCaseFunction(name, parent=self) + + def setup(self): + meth = getattr(self.obj, 'setUpClass', None) + if meth is not None: + meth() + + def teardown(self): + meth = getattr(self.obj, 'tearDownClass', None) + if meth is not None: + meth() + +class TestCaseFunction(pytest.Function): + def setup(self): + pass + def teardown(self): + pass + def startTest(self, testcase): + pass + def addError(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addFailure(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addSuccess(self, testcase): + pass + def stopTest(self, testcase): + pass + def runtest(self): + testcase = self.parent.obj(self.name) + testcase(result=self) --- /dev/null +++ b/_pytest/pytester.py @@ -0,0 +1,653 @@ +""" (disabled by default) support for testing py.test and py.test plugins. """ + +import py, pytest +import sys, os +import re +import inspect +import time +from fnmatch import fnmatch +from _pytest.session import Session +from py.builtin import print_ +from _pytest.core import HookRelay + +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_funcarg___pytest(request): + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + hookrecorder.start_recording(hook._hookspecs) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + +class ParsedCall: + def __init__(self, name, locals): + assert '_name' not in locals + self.__dict__.update(locals) + self.__dict__.pop('self') + self._name = name + + def __repr__(self): + d = self.__dict__.copy() + del d['_name'] + return "" %(self._name, d) + +class HookRecorder: + def __init__(self, pluginmanager): + self._pluginmanager = pluginmanager + self.calls = [] + self._recorders = {} + + def start_recording(self, hookspecs): + if not isinstance(hookspecs, (list, tuple)): + hookspecs = [hookspecs] + for hookspec in hookspecs: + assert hookspec not in self._recorders + class RecordCalls: + _recorder = self + for name, method in vars(hookspec).items(): + if name[0] != "_": + setattr(RecordCalls, name, self._makecallparser(method)) + recorder = RecordCalls() + self._recorders[hookspec] = recorder + self._pluginmanager.register(recorder) + self.hook = HookRelay(hookspecs, pm=self._pluginmanager, + prefix="pytest_") + + def finish_recording(self): + for recorder in self._recorders.values(): + self._pluginmanager.unregister(recorder) + self._recorders.clear() + + def _makecallparser(self, method): + name = method.__name__ + args, varargs, varkw, default = py.std.inspect.getargspec(method) + if not args or args[0] != "self": + args.insert(0, 'self') + fspec = py.std.inspect.formatargspec(args, varargs, varkw, default) + # we use exec because we want to have early type + # errors on wrong input arguments, using + # *args/**kwargs delays this and gives errors + # elsewhere + exec (py.code.compile(""" + def %(name)s%(fspec)s: + self._recorder.calls.append( + ParsedCall(%(name)r, locals())) + """ % locals())) + return locals()[name] + + def getcalls(self, names): + if isinstance(names, str): + names = names.split() + for name in names: + for cls in self._recorders: + if name in vars(cls): + break + else: + raise ValueError("callname %r not found in %r" %( + name, self._recorders.keys())) + l = [] + for call in self.calls: + if call._name in names: + l.append(call) + return l + + def contains(self, entries): + __tracebackhide__ = True + from py.builtin import print_ + i = 0 + entries = list(entries) + backlocals = py.std.sys._getframe(1).f_locals + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print_("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print_("CHECKERMATCH", repr(check), "->", call) + else: + print_("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print_("NONAMEMATCH", name, "with", call) + else: + py.test.fail("could not find %r check %r" % (name, check)) + + def popcall(self, name): + for i, call in enumerate(self.calls): + if call._name == name: + del self.calls[i] + return call + raise ValueError("could not find call %r" %(name, )) + + def getcall(self, name): + l = self.getcalls(name) + assert len(l) == 1, (name, l) + return l[0] + + +def pytest_funcarg__linecomp(request): + return LineComp() + +def pytest_funcarg__LineMatcher(request): + return LineMatcher + +def pytest_funcarg__testdir(request): + tmptestdir = TmpTestdir(request) + return tmptestdir + +rex_outcome = re.compile("(\d+) (\w+)") +class RunResult: + def __init__(self, ret, outlines, errlines, duration): + self.ret = ret + self.outlines = outlines + self.errlines = errlines + self.stdout = LineMatcher(outlines) + self.stderr = LineMatcher(errlines) + self.duration = duration + + def parseoutcomes(self): + for line in reversed(self.outlines): + if 'seconds' in line: + outcomes = rex_outcome.findall(line) + if outcomes: + d = {} + for num, cat in outcomes: + d[cat] = int(num) + return d + +class TmpTestdir: + def __init__(self, request): + self.request = request + self.Config = request.config.__class__ + self._pytest = request.getfuncargvalue("_pytest") + # XXX remove duplication with tmpdir plugin + basetmp = request.config.ensuretemp("testdir") + name = request.function.__name__ + for i in range(100): + try: + tmpdir = basetmp.mkdir(name + str(i)) + except py.error.EEXIST: + continue + break + # we need to create another subdir + # because Directory.collect() currently loads + # conftest.py from sibling directories + self.tmpdir = tmpdir.mkdir(name) + self.plugins = [] + self._syspathremove = [] + self.chdir() # always chdir + self.request.addfinalizer(self.finalize) + + def __repr__(self): + return "" % (self.tmpdir,) + + def finalize(self): + for p in self._syspathremove: + py.std.sys.path.remove(p) + if hasattr(self, '_olddir'): + self._olddir.chdir() + # delete modules that have been loaded from tmpdir + 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] + + def getreportrecorder(self, obj): + if hasattr(obj, 'config'): + obj = obj.config + if hasattr(obj, 'hook'): + obj = obj.hook + assert hasattr(obj, '_hookspecs'), obj + reprec = ReportRecorder(obj) + reprec.hookrecorder = self._pytest.gethookrecorder(obj) + reprec.hook = reprec.hookrecorder.hook + return reprec + + def chdir(self): + old = self.tmpdir.chdir() + if not hasattr(self, '_olddir'): + self._olddir = old + + def _makefile(self, ext, args, kwargs): + items = list(kwargs.items()) + if args: + source = "\n".join(map(str, args)) + "\n" + basename = self.request.function.__name__ + items.insert(0, (basename, source)) + ret = None + for name, value in items: + p = self.tmpdir.join(name).new(ext=ext) + source = str(py.code.Source(value)).lstrip() + p.write(source.encode("utf-8"), "wb") + if ret is None: + ret = p + return ret + + + def makefile(self, ext, *args, **kwargs): + return self._makefile(ext, args, kwargs) + + def makeini(self, source): + return self.makefile('cfg', setup=source) + + def makeconftest(self, source): + return self.makepyfile(conftest=source) + + def makeini(self, source): + return self.makefile('.ini', tox=source) + + def getinicfg(self, source): + p = self.makeini(source) + return py.iniconfig.IniConfig(p)['pytest'] + + def makepyfile(self, *args, **kwargs): + return self._makefile('.py', args, kwargs) + + def maketxtfile(self, *args, **kwargs): + return self._makefile('.txt', args, kwargs) + + def syspathinsert(self, path=None): + if path is None: + path = self.tmpdir + py.std.sys.path.insert(0, str(path)) + self._syspathremove.append(str(path)) + + def mkdir(self, name): + return self.tmpdir.mkdir(name) + + def mkpydir(self, name): + p = self.mkdir(name) + p.ensure("__init__.py") + return p + + Session = Session + def getnode(self, config, arg): + session = Session(config) + assert '::' not in str(arg) + p = py.path.local(arg) + x = session.fspath.bestrelpath(p) + return session.perform_collect([x], genitems=False)[0] + + def getpathnode(self, path): + config = self.parseconfig(path) + session = Session(config) + x = session.fspath.bestrelpath(path) + return session.perform_collect([x], genitems=False)[0] + + def genitems(self, colitems): + session = colitems[0].session + result = [] + for colitem in colitems: + result.extend(session.genitems(colitem)) + return result + + def inline_genitems(self, *args): + #config = self.parseconfig(*args) + config = self.parseconfigure(*args) + rec = self.getreportrecorder(config) + session = Session(config) + session.perform_collect() + return session.items, rec + + def runitem(self, source): + # used from runner functional tests + item = self.getitem(source) + # the test class where we are called from wants to provide the runner + testclassinstance = py.builtin._getimself(self.request.function) + runner = testclassinstance.getrunner() + return runner(item) + + def inline_runsource(self, source, *cmdlineargs): + p = self.makepyfile(source) + l = list(cmdlineargs) + [p] + return self.inline_run(*l) + + def inline_runsource1(self, *args): + 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) == 1, reports + return reports[0] + + def inline_run(self, *args): + args = ("-s", ) + args # otherwise FD leakage + config = self.parseconfig(*args) + reprec = self.getreportrecorder(config) + #config.pluginmanager.do_configure(config) + config.hook.pytest_cmdline_main(config=config) + #config.pluginmanager.do_unconfigure(config) + return reprec + + def config_preparse(self): + config = self.Config() + for plugin in self.plugins: + if isinstance(plugin, str): + config.pluginmanager.import_plugin(plugin) + else: + if isinstance(plugin, dict): + plugin = PseudoPlugin(plugin) + if not config.pluginmanager.isregistered(plugin): + config.pluginmanager.register(plugin) + return config + + def parseconfig(self, *args): + if not args: + args = (self.tmpdir,) + config = self.config_preparse() + args = list(args) + ["--basetemp=%s" % self.tmpdir.dirpath('basetemp')] + config.parse(args) + return config + + def reparseconfig(self, args=None): + """ this is used from tests that want to re-invoke parse(). """ + if not args: + args = [self.tmpdir] + oldconfig = getattr(py.test, 'config', None) + try: + c = py.test.config = self.Config() + c.basetemp = oldconfig.mktemp("reparse", numbered=True) + c.parse(args) + return c + finally: + py.test.config = oldconfig + + def parseconfigure(self, *args): + config = self.parseconfig(*args) + config.pluginmanager.do_configure(config) + self.request.addfinalizer(lambda: + config.pluginmanager.do_unconfigure(config)) + return config + + def getitem(self, source, funcname="test_func"): + for item in self.getitems(source): + if item.name == funcname: + return item + assert 0, "%r item not found in module:\n%s" %(funcname, source) + + def getitems(self, source): + modcol = self.getmodulecol(source) + return self.genitems([modcol]) + + def getmodulecol(self, source, configargs=(), withinit=False): + kw = {self.request.function.__name__: py.code.Source(source).strip()} + path = self.makepyfile(**kw) + if withinit: + self.makepyfile(__init__ = "#") + self.config = config = self.parseconfigure(path, *configargs) + node = self.getnode(config, path) + #config.pluginmanager.do_unconfigure(config) + return node + + def collect_by_name(self, modcol, name): + for colitem in modcol._memocollect(): + if colitem.name == name: + return colitem + + def popen(self, cmdargs, stdout, stderr, **kw): + if not hasattr(py.std, 'subprocess'): + py.test.skip("no subprocess module") + env = os.environ.copy() + env['PYTHONPATH'] = ":".join(filter(None, [ + str(os.getcwd()), env.get('PYTHONPATH', '')])) + kw['env'] = env + #print "env", env + return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + + def pytestmain(self, *args, **kwargs): + ret = pytest.main(*args, **kwargs) + if ret == 2: + raise KeyboardInterrupt() + def run(self, *cmdargs): + return self._run(*cmdargs) + + def _run(self, *cmdargs): + cmdargs = [str(x) for x in cmdargs] + p1 = self.tmpdir.join("stdout") + p2 = self.tmpdir.join("stderr") + print_("running", cmdargs, "curdir=", py.path.local()) + f1 = p1.open("wb") + f2 = p2.open("wb") + now = time.time() + popen = self.popen(cmdargs, stdout=f1, stderr=f2, + close_fds=(sys.platform != "win32")) + ret = popen.wait() + f1.close() + f2.close() + out = p1.read("rb") + out = getdecoded(out).splitlines() + err = p2.read("rb") + err = getdecoded(err).splitlines() + def dump_lines(lines, fp): + try: + for line in lines: + py.builtin.print_(line, file=fp) + except UnicodeEncodeError: + print("couldn't print to %s because of encoding" % (fp,)) + dump_lines(out, sys.stdout) + dump_lines(err, sys.stderr) + return RunResult(ret, out, err, time.time()-now) + + def runpybin(self, scriptname, *args): + fullargs = self._getpybinargs(scriptname) + args + return self.run(*fullargs) + + def _getpybinargs(self, scriptname): + if not self.request.config.getvalue("notoolsonpath"): + script = py.path.local.sysfind(scriptname) + assert script, "script %r not found" % scriptname + return (py.std.sys.executable, script,) + else: + py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) + + def runpython(self, script, prepend=True): + 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): + command = self._getsysprepend() + command + return self.run(py.std.sys.executable, "-c", command) + + def runpytest(self, *args): + p = py.path.local.make_numbered_dir(prefix="runpytest-", + keep=None, rootdir=self.tmpdir) + args = ('--basetemp=%s' % p, ) + args + #for x in args: + # if '--confcutdir' in str(x): + # break + #else: + # pass + # args = ('--confcutdir=.',) + args + plugins = [x for x in self.plugins if isinstance(x, str)] + if plugins: + args = ('-p', plugins[0]) + args + return self.runpybin("py.test", *args) + + def spawn_pytest(self, string, expect_timeout=10.0): + if self.request.config.getvalue("notoolsonpath"): + py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") + basetemp = self.tmpdir.mkdir("pexpect") + invoke = " ".join(map(str, self._getpybinargs("py.test"))) + cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) + return self.spawn(cmd, expect_timeout=expect_timeout) + + def spawn(self, cmd, expect_timeout=10.0): + pexpect = py.test.importorskip("pexpect", "2.4") + logfile = self.tmpdir.join("spawn.out") + child = pexpect.spawn(cmd, logfile=logfile.open("w")) + child.timeout = expect_timeout + return child + +def getdecoded(out): + try: + return out.decode("utf-8") + except UnicodeDecodeError: + return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( + py.io.saferepr(out),) + +class PseudoPlugin: + def __init__(self, vars): + self.__dict__.update(vars) + +class ReportRecorder(object): + def __init__(self, hook): + self.hook = hook + self.pluginmanager = hook._pm + self.pluginmanager.register(self) + + def getcall(self, name): + return self.hookrecorder.getcall(name) + + def popcall(self, name): + return self.hookrecorder.popcall(name) + + def getcalls(self, names): + """ return list of ParsedCall instances matching the given eventname. """ + return self.hookrecorder.getcalls(names) + + # functionality for test reports + + def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): + return [x.report for x in self.getcalls(names)] + + def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport"): + """ return a testreport whose dotted import path matches """ + l = [] + for rep in self.getreports(names=names): + if not inamepart or inamepart in rep.nodeid.split("::"): + l.append(rep) + if not l: + raise ValueError("could not find test report matching %r: no test reports at all!" % + (inamepart,)) + if len(l) > 1: + raise ValueError("found more than one testreport matching %r: %s" %( + inamepart, l)) + return l[0] + + def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'): + return [rep for rep in self.getreports(names) if rep.failed] + + def getfailedcollections(self): + return self.getfailures('pytest_collectreport') + + def listoutcomes(self): + passed = [] + skipped = [] + failed = [] + for rep in self.getreports("pytest_runtest_logreport"): + if rep.passed: + if rep.when == "call": + passed.append(rep) + elif rep.skipped: + skipped.append(rep) + elif rep.failed: + failed.append(rep) + return passed, skipped, failed + + def countoutcomes(self): + return [len(x) for x in self.listoutcomes()] + + def assertoutcome(self, passed=0, skipped=0, failed=0): + realpassed, realskipped, realfailed = self.listoutcomes() + assert passed == len(realpassed) + assert skipped == len(realskipped) + assert failed == len(realfailed) + + def clear(self): + self.hookrecorder.calls[:] = [] + + def unregister(self): + self.pluginmanager.unregister(self) + self.hookrecorder.finish_recording() + +class LineComp: + def __init__(self): + self.stringio = py.io.TextIO() + + def assert_contains_lines(self, lines2): + """ assert that lines2 are contained (linearly) in lines1. + return a list of extralines found. + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) + self.stringio.seek(0) + lines1 = val.split("\n") + return LineMatcher(lines1).fnmatch_lines(lines2) + +class LineMatcher: + def __init__(self, lines): + self.lines = lines + + def str(self): + return "\n".join(self.lines) + + def _getlines(self, lines2): + if isinstance(lines2, str): + lines2 = py.code.Source(lines2) + if isinstance(lines2, py.code.Source): + lines2 = lines2.strip().lines + return lines2 + + def fnmatch_lines_random(self, lines2): + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or fnmatch(x, line): + print_("matched: ", repr(line)) + break + else: + raise ValueError("line %r not found in output" % line) + + def fnmatch_lines(self, lines2): + def show(arg1, arg2): + py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) + lines2 = self._getlines(lines2) + lines1 = self.lines[:] + nextline = None + extralines = [] + __tracebackhide__ = True + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + show("exact match:", repr(line)) + break + elif fnmatch(nextline, line): + show("fnmatch:", repr(line)) + show(" with:", repr(nextline)) + break + else: + if not nomatchprinted: + show("nomatch:", repr(line)) + nomatchprinted = True + show(" and:", repr(nextline)) + extralines.append(nextline) + else: + py.test.fail("remains unmatched: %r, see stderr" % (line,)) --- a/pytest/plugin/pdb.py +++ /dev/null @@ -1,77 +0,0 @@ -""" interactive debugging with PDB, the Python Debugger. """ - -import py -import sys - -def pytest_addoption(parser): - group = parser.getgroup("general") - group._addoption('--pdb', - action="store_true", dest="usepdb", default=False, - help="start the interactive Python debugger on errors.") - -def pytest_namespace(): - return {'set_trace': pytestPDB().set_trace} - -def pytest_configure(config): - if config.getvalue("usepdb"): - config.pluginmanager.register(PdbInvoke(), 'pdbinvoke') - -class pytestPDB: - """ Pseudo PDB that defers to the real pdb. """ - item = None - - def set_trace(self): - """ invoke PDB set_trace debugging, dropping any IO capturing. """ - frame = sys._getframe().f_back - item = getattr(self, 'item', None) - if item is not None: - capman = item.config.pluginmanager.getplugin("capturemanager") - out, err = capman.suspendcapture() - if hasattr(item, 'outerr'): - item.outerr = (item.outerr[0] + out, item.outerr[1] + err) - tw = py.io.TerminalWriter() - tw.line() - tw.sep(">", "PDB set_trace (IO-capturing turned off)") - py.std.pdb.Pdb().set_trace(frame) - -def pdbitem(item): - pytestPDB.item = item -pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem - -def pytest_runtest_makereport(): - pytestPDB.item = None - -class PdbInvoke: - def pytest_sessionfinish(self, session): - # don't display failures again at the end - session.config.option.tbstyle = "no" - def pytest_runtest_makereport(self, item, call, __multicall__): - if not call.excinfo or \ - call.excinfo.errisinstance(py.test.skip.Exception) or \ - call.excinfo.errisinstance(py.std.bdb.BdbQuit): - return - rep = __multicall__.execute() - if "xfail" in rep.keywords: - return rep - # we assume that the above execute() suspended capturing - tw = py.io.TerminalWriter() - tw.line() - tw.sep(">", "traceback") - rep.toterminal(tw) - tw.sep(">", "entering PDB") - post_mortem(call.excinfo._excinfo[2]) - return rep - -def post_mortem(t): - pdb = py.std.pdb - class Pdb(pdb.Pdb): - def get_stack(self, f, t): - stack, i = pdb.Pdb.get_stack(self, f, t) - if f is None: - i = max(0, len(stack) - 1) - while i and stack[i][0].f_locals.get("__tracebackhide__", False): - i-=1 - return stack, i - p = Pdb() - p.reset() - p.interaction(None, t) --- a/testing/plugin/test_helpconfig.py +++ b/testing/plugin/test_helpconfig.py @@ -1,5 +1,5 @@ import py, pytest,os -from pytest.plugin.helpconfig import collectattr +from _pytest.helpconfig import collectattr def test_version(testdir): result = testdir.runpytest("--version") --- a/pytest/plugin/tmpdir.py +++ /dev/null @@ -1,26 +0,0 @@ -""" support for providing temporary directories to test functions. """ -import pytest, py - -def pytest_configure(config): - def ensuretemp(string, dir=1): - """ (deprecated) return temporary directory path with - the given string as the trailing part. It is usually - better to use the 'tmpdir' function argument which will - take care to provide empty unique directories for each - test call even if the test is called multiple times. - """ - #py.log._apiwarn(">1.1", "use tmpdir function argument") - return config.ensuretemp(string, dir=dir) - pytest.ensuretemp = ensuretemp - -def pytest_funcarg__tmpdir(request): - """return a temporary directory path object - unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a `py.path.local`_ - path object. - """ - name = request._pyfuncitem.name - name = py.std.re.sub("[\W]", "_", name) - x = request.config.mktemp(name, numbered=True) - return x.realpath() --- a/pytest/plugin/resultlog.py +++ /dev/null @@ -1,92 +0,0 @@ -""" (disabled by default) create result information in a plain text file. """ - -import py -from py.builtin import print_ - -def pytest_addoption(parser): - group = parser.getgroup("resultlog", "resultlog plugin options") - group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None, - help="path for machine-readable result log.") - -def pytest_configure(config): - resultlog = config.option.resultlog - # prevent opening resultlog on slave nodes (xdist) - if resultlog and not hasattr(config, 'slaveinput'): - logfile = open(resultlog, 'w', 1) # line buffered - config._resultlog = ResultLog(config, logfile) - config.pluginmanager.register(config._resultlog) - -def pytest_unconfigure(config): - resultlog = getattr(config, '_resultlog', None) - if resultlog: - resultlog.logfile.close() - del config._resultlog - config.pluginmanager.unregister(resultlog) - -def generic_path(item): - chain = item.listchain() - gpath = [chain[0].name] - fspath = chain[0].fspath - fspart = False - for node in chain[1:]: - newfspath = node.fspath - if newfspath == fspath: - if fspart: - gpath.append(':') - fspart = False - else: - gpath.append('.') - else: - gpath.append('/') - fspart = True - name = node.name - if name[0] in '([': - gpath.pop() - gpath.append(name) - fspath = newfspath - return ''.join(gpath) - -class ResultLog(object): - def __init__(self, config, logfile): - self.config = config - self.logfile = logfile # preferably line buffered - - def write_log_entry(self, testpath, lettercode, longrepr): - print_("%s %s" % (lettercode, testpath), file=self.logfile) - for line in longrepr.splitlines(): - print_(" %s" % line, file=self.logfile) - - def log_outcome(self, report, lettercode, longrepr): - testpath = getattr(report, 'nodeid', None) - if testpath is None: - testpath = report.fspath - self.write_log_entry(testpath, lettercode, longrepr) - - def pytest_runtest_logreport(self, report): - res = self.config.hook.pytest_report_teststatus(report=report) - code = res[1] - if code == 'x': - longrepr = str(report.longrepr) - elif code == 'X': - longrepr = '' - elif report.passed: - longrepr = "" - elif report.failed: - longrepr = str(report.longrepr) - elif report.skipped: - longrepr = str(report.longrepr.reprcrash.message) - self.log_outcome(report, code, longrepr) - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - code = "F" - else: - assert report.skipped - code = "S" - longrepr = str(report.longrepr.reprcrash) - self.log_outcome(report, code, longrepr) - - def pytest_internalerror(self, excrepr): - path = excrepr.reprcrash.path - self.write_log_entry(path, '!', str(excrepr)) --- /dev/null +++ b/_pytest/terminal.py @@ -0,0 +1,420 @@ +""" terminal reporting of the full testing process. + +This is a good source for looking at the various reporting hooks. +""" +import py +import sys +import os + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting", "reporting", after="general") + group._addoption('-v', '--verbose', action="count", + dest="verbose", default=0, help="increase verbosity."), + group._addoption('-q', '--quiet', action="count", + dest="quiet", default=0, help="decreate verbosity."), + group._addoption('-r', + action="store", dest="reportchars", default=None, metavar="chars", + help="show extra test summary info as specified by chars (f)ailed, " + "(s)skipped, (x)failed, (X)passed.") + group._addoption('-l', '--showlocals', + action="store_true", dest="showlocals", default=False, + help="show locals in tracebacks (disabled by default).") + group._addoption('--report', + action="store", dest="report", default=None, metavar="opts", + help="(deprecated, use -r)") + group._addoption('--tb', metavar="style", + action="store", dest="tbstyle", default='long', + type="choice", choices=['long', 'short', 'no', 'line', 'native'], + help="traceback print mode (long/short/line/no).") + group._addoption('--fulltrace', + action="store_true", dest="fulltrace", default=False, + help="don't cut any tracebacks (default is to cut).") + +def pytest_configure(config): + config.option.verbose -= config.option.quiet + if config.option.collectonly: + reporter = CollectonlyReporter(config) + else: + # we try hard to make printing resilient against + # later changes on FD level. + stdout = py.std.sys.stdout + if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): + try: + newfd = os.dup(stdout.fileno()) + #print "got newfd", newfd + except ValueError: + pass + else: + stdout = os.fdopen(newfd, stdout.mode, 1) + config._toclose = stdout + reporter = TerminalReporter(config, stdout) + config.pluginmanager.register(reporter, 'terminalreporter') + if config.option.debug or config.option.traceconfig: + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + config.trace.root.setprocessor("pytest:config", mywriter) + +def pytest_unconfigure(config): + if hasattr(config, '_toclose'): + #print "closing", config._toclose, config._toclose.fileno() + config._toclose.close() + +def getreportopt(config): + reportopts = "" + optvalue = config.option.report + if optvalue: + py.builtin.print_("DEPRECATED: use -r instead of --report option.", + file=py.std.sys.stderr) + if optvalue: + for setting in optvalue.split(","): + setting = setting.strip() + if setting == "skipped": + reportopts += "s" + elif setting == "xfailed": + reportopts += "x" + reportchars = config.option.reportchars + if reportchars: + for char in reportchars: + if char not in reportopts: + reportopts += char + return reportopts + +def pytest_report_teststatus(report): + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + elif report.failed: + letter = "F" + if report.when != "call": + letter = "f" + return report.outcome, letter, report.outcome.upper() + +class TerminalReporter: + def __init__(self, config, file=None): + self.config = config + self.verbosity = self.config.option.verbose + self.showheader = self.verbosity >= 0 + self.showfspath = self.verbosity >= 0 + self.showlongtestinfo = self.verbosity > 0 + + self.stats = {} + self.curdir = py.path.local() + if file is None: + file = py.std.sys.stdout + self._tw = py.io.TerminalWriter(file) + self.currentfspath = None + self.reportchars = getreportopt(config) + + def hasopt(self, char): + char = {'xfailed': 'x', 'skipped': 's'}.get(char,char) + return char in self.reportchars + + def write_fspath_result(self, fspath, res): + if fspath != self.currentfspath: + self.currentfspath = fspath + #fspath = self.curdir.bestrelpath(fspath) + self._tw.line() + #relpath = self.curdir.bestrelpath(fspath) + self._tw.write(fspath + " ") + self._tw.write(res) + + def write_ensure_prefix(self, prefix, extra="", **kwargs): + if self.currentfspath != prefix: + self._tw.line() + self.currentfspath = prefix + self._tw.write(prefix) + if extra: + self._tw.write(extra, **kwargs) + self.currentfspath = -2 + + def ensure_newline(self): + if self.currentfspath: + self._tw.line() + self.currentfspath = None + + def write_line(self, line, **markup): + line = str(line) + self.ensure_newline() + self._tw.line(line, **markup) + + def write_sep(self, sep, title=None, **markup): + self.ensure_newline() + self._tw.sep(sep, title, **markup) + + def pytest_internalerror(self, excrepr): + for line in str(excrepr).split("\n"): + self.write_line("INTERNALERROR> " + line) + return 1 + + def pytest_plugin_registered(self, plugin): + if self.config.option.traceconfig: + msg = "PLUGIN registered: %s" %(plugin,) + # XXX this event may happen during setup/teardown time + # which unfortunately captures our output here + # which garbles our output if we use self.write_line + self.write_line(msg) + + def pytest_deselected(self, items): + self.stats.setdefault('deselected', []).extend(items) + + def pytest__teardown_final_logerror(self, report): + self.stats.setdefault("error", []).append(report) + + def pytest_runtest_logstart(self, nodeid, location): + # ensure that the path is printed before the + # 1st test of a module starts running + fspath = nodeid.split("::")[0] + if self.showlongtestinfo: + line = self._locationline(fspath, *location) + self.write_ensure_prefix(line, "") + elif self.showfspath: + self.write_fspath_result(fspath, "") + + def pytest_runtest_logreport(self, report): + rep = report + res = self.config.hook.pytest_report_teststatus(report=rep) + cat, letter, word = res + self.stats.setdefault(cat, []).append(rep) + if not letter and not word: + # probably passed setup/teardown + return + if self.verbosity <= 0: + if not hasattr(rep, 'node') and self.showfspath: + self.write_fspath_result(rep.fspath, letter) + else: + self._tw.write(letter) + else: + if isinstance(word, tuple): + word, markup = word + else: + if rep.passed: + markup = {'green':True} + elif rep.failed: + markup = {'red':True} + elif rep.skipped: + markup = {'yellow':True} + line = self._locationline(str(rep.fspath), *rep.location) + if not hasattr(rep, 'node'): + self.write_ensure_prefix(line, word, **markup) + #self._tw.write(word, **markup) + else: + self.ensure_newline() + if hasattr(rep, 'node'): + self._tw.write("[%s] " % rep.node.gateway.id) + self._tw.write(word, **markup) + self._tw.write(" " + line) + self.currentfspath = -2 + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + self.stats.setdefault("error", []).append(report) + self.write_fspath_result(report.fspath, "E") + elif report.skipped: + self.stats.setdefault("skipped", []).append(report) + self.write_fspath_result(report.fspath, "S") + + def pytest_sessionstart(self, session): + self._sessionstarttime = py.std.time.time() + if not self.showheader: + return + self.write_sep("=", "test session starts", bold=True) + verinfo = ".".join(map(str, sys.version_info[:3])) + msg = "platform %s -- Python %s" % (sys.platform, verinfo) + msg += " -- pytest-%s" % (py.test.__version__) + if self.verbosity > 0 or self.config.option.debug or \ + getattr(self.config.option, 'pastebin', None): + msg += " -- " + str(sys.executable) + self.write_line(msg) + lines = self.config.hook.pytest_report_header(config=self.config) + lines.reverse() + for line in flatten(lines): + self.write_line(line) + + def pytest_collection_finish(self): + if not self.showheader: + return + for i, testarg in enumerate(self.config.args): + self.write_line("test path %d: %s" %(i+1, testarg)) + + def pytest_sessionfinish(self, exitstatus, __multicall__): + __multicall__.execute() + self._tw.line("") + if exitstatus in (0, 1, 2): + self.summary_errors() + self.summary_failures() + self.config.hook.pytest_terminal_summary(terminalreporter=self) + if exitstatus == 2: + self._report_keyboardinterrupt() + self.summary_deselected() + self.summary_stats() + + def pytest_keyboard_interrupt(self, excinfo): + self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) + + def _report_keyboardinterrupt(self): + excrepr = self._keyboardinterrupt_memo + msg = excrepr.reprcrash.message + self.write_sep("!", msg) + if "KeyboardInterrupt" in msg: + if self.config.option.fulltrace: + excrepr.toterminal(self._tw) + else: + excrepr.reprcrash.toterminal(self._tw) + + def _locationline(self, collect_fspath, fspath, lineno, domain): + if fspath and fspath != collect_fspath: + fspath = "%s <- %s" % ( + self.curdir.bestrelpath(py.path.local(collect_fspath)), + self.curdir.bestrelpath(py.path.local(fspath))) + elif fspath: + fspath = self.curdir.bestrelpath(py.path.local(fspath)) + if lineno is not None: + lineno += 1 + if fspath and lineno and domain: + line = "%(fspath)s:%(lineno)s: %(domain)s" + elif fspath and domain: + line = "%(fspath)s: %(domain)s" + elif fspath and lineno: + line = "%(fspath)s:%(lineno)s %(extrapath)s" + else: + line = "[nolocation]" + return line % locals() + " " + + def _getfailureheadline(self, rep): + if hasattr(rep, 'location'): + fspath, lineno, domain = rep.location + return domain + else: + return "test session" # XXX? + + def _getcrashline(self, rep): + try: + return str(rep.longrepr.reprcrash) + except AttributeError: + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" + + # + # summaries for sessionfinish + # + + def summary_failures(self): + tbstyle = self.config.option.tbstyle + if 'failed' in self.stats and tbstyle != "no": + self.write_sep("=", "FAILURES") + for rep in self.stats['failed']: + if tbstyle == "line": + line = self._getcrashline(rep) + self.write_line(line) + else: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg) + rep.toterminal(self._tw) + + def summary_errors(self): + if 'error' in self.stats and self.config.option.tbstyle != "no": + self.write_sep("=", "ERRORS") + for rep in self.stats['error']: + msg = self._getfailureheadline(rep) + if not hasattr(rep, 'when'): + # collect + msg = "ERROR collecting " + msg + elif rep.when == "setup": + msg = "ERROR at setup of " + msg + elif rep.when == "teardown": + msg = "ERROR at teardown of " + msg + self.write_sep("_", msg) + rep.toterminal(self._tw) + + def summary_stats(self): + session_duration = py.std.time.time() - self._sessionstarttime + + keys = "failed passed skipped deselected".split() + for key in self.stats.keys(): + if key not in keys: + keys.append(key) + parts = [] + for key in keys: + val = self.stats.get(key, None) + if val: + parts.append("%d %s" %(len(val), key)) + line = ", ".join(parts) + # XXX coloring + msg = "%s in %.2f seconds" %(line, session_duration) + if self.verbosity >= 0: + self.write_sep("=", msg, bold=True) + else: + self.write_line(msg, bold=True) + + def summary_deselected(self): + if 'deselected' in self.stats: + self.write_sep("=", "%d tests deselected by %r" %( + len(self.stats['deselected']), self.config.option.keyword), bold=True) + + +class CollectonlyReporter: + INDENT = " " + + def __init__(self, config, out=None): + self.config = config + if out is None: + out = py.std.sys.stdout + self._tw = py.io.TerminalWriter(out) + self.indent = "" + self._failed = [] + + def outindent(self, line): + self._tw.line(self.indent + str(line)) + + def pytest_internalerror(self, excrepr): + for line in str(excrepr).split("\n"): + self._tw.line("INTERNALERROR> " + line) + + def pytest_collectstart(self, collector): + if collector.session != collector: + self.outindent(collector) + self.indent += self.INDENT + + def pytest_itemcollected(self, item): + self.outindent(item) + + def pytest_collectreport(self, report): + if not report.passed: + if hasattr(report.longrepr, 'reprcrash'): + msg = report.longrepr.reprcrash.message + else: + # XXX unify (we have CollectErrorRepr here) + msg = str(report.longrepr.longrepr) + self.outindent("!!! %s !!!" % msg) + #self.outindent("!!! error !!!") + self._failed.append(report) + self.indent = self.indent[:-len(self.INDENT)] + + def pytest_collection_finish(self): + if self._failed: + self._tw.sep("!", "collection failures") + for rep in self._failed: + rep.toterminal(self._tw) + return self._failed and 1 or 0 + +def repr_pythonversion(v=None): + if v is None: + v = sys.version_info + try: + return "%s.%s.%s-%s-%s" % v + except (TypeError, ValueError): + return str(v) + +def flatten(l): + for x in l: + if isinstance(x, (list, tuple)): + for y in flatten(x): + yield y + else: + yield x + --- a/pytest/plugin/unittest.py +++ /dev/null @@ -1,52 +0,0 @@ -""" discovery and running of std-library "unittest" style tests. """ -import pytest, py -import sys - -def pytest_pycollect_makeitem(collector, name, obj): - unittest = sys.modules.get('unittest') - if unittest is None: - return # nobody can have derived unittest.TestCase - try: - isunit = issubclass(obj, unittest.TestCase) - except KeyboardInterrupt: - raise - except Exception: - pass - else: - if isunit: - return UnitTestCase(name, parent=collector) - -class UnitTestCase(pytest.Class): - def collect(self): - loader = py.std.unittest.TestLoader() - for name in loader.getTestCaseNames(self.obj): - yield TestCaseFunction(name, parent=self) - - def setup(self): - meth = getattr(self.obj, 'setUpClass', None) - if meth is not None: - meth() - - def teardown(self): - meth = getattr(self.obj, 'tearDownClass', None) - if meth is not None: - meth() - -class TestCaseFunction(pytest.Function): - def setup(self): - pass - def teardown(self): - pass - def startTest(self, testcase): - pass - def addError(self, testcase, rawexcinfo): - py.builtin._reraise(*rawexcinfo) - def addFailure(self, testcase, rawexcinfo): - py.builtin._reraise(*rawexcinfo) - def addSuccess(self, testcase): - pass - def stopTest(self, testcase): - pass - def runtest(self): - testcase = self.parent.obj(self.name) - testcase(result=self) --- a/pytest/plugin/pytester.py +++ /dev/null @@ -1,653 +0,0 @@ -""" (disabled by default) support for testing py.test and py.test plugins. """ - -import py, pytest -import sys, os -import re -import inspect -import time -from fnmatch import fnmatch -from pytest.plugin.session import Session -from py.builtin import print_ -from pytest.main import HookRelay - -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_funcarg___pytest(request): - return PytestArg(request) - -class PytestArg: - def __init__(self, request): - self.request = request - - def gethookrecorder(self, hook): - hookrecorder = HookRecorder(hook._pm) - hookrecorder.start_recording(hook._hookspecs) - self.request.addfinalizer(hookrecorder.finish_recording) - return hookrecorder - -class ParsedCall: - def __init__(self, name, locals): - assert '_name' not in locals - self.__dict__.update(locals) - self.__dict__.pop('self') - self._name = name - - def __repr__(self): - d = self.__dict__.copy() - del d['_name'] - return "" %(self._name, d) - -class HookRecorder: - def __init__(self, pluginmanager): - self._pluginmanager = pluginmanager - self.calls = [] - self._recorders = {} - - def start_recording(self, hookspecs): - if not isinstance(hookspecs, (list, tuple)): - hookspecs = [hookspecs] - for hookspec in hookspecs: - assert hookspec not in self._recorders - class RecordCalls: - _recorder = self - for name, method in vars(hookspec).items(): - if name[0] != "_": - setattr(RecordCalls, name, self._makecallparser(method)) - recorder = RecordCalls() - self._recorders[hookspec] = recorder - self._pluginmanager.register(recorder) - self.hook = HookRelay(hookspecs, pm=self._pluginmanager, - prefix="pytest_") - - def finish_recording(self): - for recorder in self._recorders.values(): - self._pluginmanager.unregister(recorder) - self._recorders.clear() - - def _makecallparser(self, method): - name = method.__name__ - args, varargs, varkw, default = py.std.inspect.getargspec(method) - if not args or args[0] != "self": - args.insert(0, 'self') - fspec = py.std.inspect.formatargspec(args, varargs, varkw, default) - # we use exec because we want to have early type - # errors on wrong input arguments, using - # *args/**kwargs delays this and gives errors - # elsewhere - exec (py.code.compile(""" - def %(name)s%(fspec)s: - self._recorder.calls.append( - ParsedCall(%(name)r, locals())) - """ % locals())) - return locals()[name] - - def getcalls(self, names): - if isinstance(names, str): - names = names.split() - for name in names: - for cls in self._recorders: - if name in vars(cls): - break - else: - raise ValueError("callname %r not found in %r" %( - name, self._recorders.keys())) - l = [] - for call in self.calls: - if call._name in names: - l.append(call) - return l - - def contains(self, entries): - __tracebackhide__ = True - from py.builtin import print_ - i = 0 - entries = list(entries) - backlocals = py.std.sys._getframe(1).f_locals - while entries: - name, check = entries.pop(0) - for ind, call in enumerate(self.calls[i:]): - if call._name == name: - print_("NAMEMATCH", name, call) - if eval(check, backlocals, call.__dict__): - print_("CHECKERMATCH", repr(check), "->", call) - else: - print_("NOCHECKERMATCH", repr(check), "-", call) - continue - i += ind + 1 - break - print_("NONAMEMATCH", name, "with", call) - else: - py.test.fail("could not find %r check %r" % (name, check)) - - def popcall(self, name): - for i, call in enumerate(self.calls): - if call._name == name: - del self.calls[i] - return call - raise ValueError("could not find call %r" %(name, )) - - def getcall(self, name): - l = self.getcalls(name) - assert len(l) == 1, (name, l) - return l[0] - - -def pytest_funcarg__linecomp(request): - return LineComp() - -def pytest_funcarg__LineMatcher(request): - return LineMatcher - -def pytest_funcarg__testdir(request): - tmptestdir = TmpTestdir(request) - return tmptestdir - -rex_outcome = re.compile("(\d+) (\w+)") -class RunResult: - def __init__(self, ret, outlines, errlines, duration): - self.ret = ret - self.outlines = outlines - self.errlines = errlines - self.stdout = LineMatcher(outlines) - self.stderr = LineMatcher(errlines) - self.duration = duration - - def parseoutcomes(self): - for line in reversed(self.outlines): - if 'seconds' in line: - outcomes = rex_outcome.findall(line) - if outcomes: - d = {} - for num, cat in outcomes: - d[cat] = int(num) - return d - -class TmpTestdir: - def __init__(self, request): - self.request = request - self.Config = request.config.__class__ - self._pytest = request.getfuncargvalue("_pytest") - # XXX remove duplication with tmpdir plugin - basetmp = request.config.ensuretemp("testdir") - name = request.function.__name__ - for i in range(100): - try: - tmpdir = basetmp.mkdir(name + str(i)) - except py.error.EEXIST: - continue - break - # we need to create another subdir - # because Directory.collect() currently loads - # conftest.py from sibling directories - self.tmpdir = tmpdir.mkdir(name) - self.plugins = [] - self._syspathremove = [] - self.chdir() # always chdir - self.request.addfinalizer(self.finalize) - - def __repr__(self): - return "" % (self.tmpdir,) - - def finalize(self): - for p in self._syspathremove: - py.std.sys.path.remove(p) - if hasattr(self, '_olddir'): - self._olddir.chdir() - # delete modules that have been loaded from tmpdir - 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] - - def getreportrecorder(self, obj): - if hasattr(obj, 'config'): - obj = obj.config - if hasattr(obj, 'hook'): - obj = obj.hook - assert hasattr(obj, '_hookspecs'), obj - reprec = ReportRecorder(obj) - reprec.hookrecorder = self._pytest.gethookrecorder(obj) - reprec.hook = reprec.hookrecorder.hook - return reprec - - def chdir(self): - old = self.tmpdir.chdir() - if not hasattr(self, '_olddir'): - self._olddir = old - - def _makefile(self, ext, args, kwargs): - items = list(kwargs.items()) - if args: - source = "\n".join(map(str, args)) + "\n" - basename = self.request.function.__name__ - items.insert(0, (basename, source)) - ret = None - for name, value in items: - p = self.tmpdir.join(name).new(ext=ext) - source = str(py.code.Source(value)).lstrip() - p.write(source.encode("utf-8"), "wb") - if ret is None: - ret = p - return ret - - - def makefile(self, ext, *args, **kwargs): - return self._makefile(ext, args, kwargs) - - def makeini(self, source): - return self.makefile('cfg', setup=source) - - def makeconftest(self, source): - return self.makepyfile(conftest=source) - - def makeini(self, source): - return self.makefile('.ini', tox=source) - - def getinicfg(self, source): - p = self.makeini(source) - return py.iniconfig.IniConfig(p)['pytest'] - - def makepyfile(self, *args, **kwargs): - return self._makefile('.py', args, kwargs) - - def maketxtfile(self, *args, **kwargs): - return self._makefile('.txt', args, kwargs) - - def syspathinsert(self, path=None): - if path is None: - path = self.tmpdir - py.std.sys.path.insert(0, str(path)) - self._syspathremove.append(str(path)) - - def mkdir(self, name): - return self.tmpdir.mkdir(name) - - def mkpydir(self, name): - p = self.mkdir(name) - p.ensure("__init__.py") - return p - - Session = Session - def getnode(self, config, arg): - session = Session(config) - assert '::' not in str(arg) - p = py.path.local(arg) - x = session.fspath.bestrelpath(p) - return session.perform_collect([x], genitems=False)[0] - - def getpathnode(self, path): - config = self.parseconfig(path) - session = Session(config) - x = session.fspath.bestrelpath(path) - return session.perform_collect([x], genitems=False)[0] - - def genitems(self, colitems): - session = colitems[0].session - result = [] - for colitem in colitems: - result.extend(session.genitems(colitem)) - return result - - def inline_genitems(self, *args): - #config = self.parseconfig(*args) - config = self.parseconfigure(*args) - rec = self.getreportrecorder(config) - session = Session(config) - session.perform_collect() - return session.items, rec - - def runitem(self, source): - # used from runner functional tests - item = self.getitem(source) - # the test class where we are called from wants to provide the runner - testclassinstance = py.builtin._getimself(self.request.function) - runner = testclassinstance.getrunner() - return runner(item) - - def inline_runsource(self, source, *cmdlineargs): - p = self.makepyfile(source) - l = list(cmdlineargs) + [p] - return self.inline_run(*l) - - def inline_runsource1(self, *args): - 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) == 1, reports - return reports[0] - - def inline_run(self, *args): - args = ("-s", ) + args # otherwise FD leakage - config = self.parseconfig(*args) - reprec = self.getreportrecorder(config) - #config.pluginmanager.do_configure(config) - config.hook.pytest_cmdline_main(config=config) - #config.pluginmanager.do_unconfigure(config) - return reprec - - def config_preparse(self): - config = self.Config() - for plugin in self.plugins: - if isinstance(plugin, str): - config.pluginmanager.import_plugin(plugin) - else: - if isinstance(plugin, dict): - plugin = PseudoPlugin(plugin) - if not config.pluginmanager.isregistered(plugin): - config.pluginmanager.register(plugin) - return config - - def parseconfig(self, *args): - if not args: - args = (self.tmpdir,) - config = self.config_preparse() - args = list(args) + ["--basetemp=%s" % self.tmpdir.dirpath('basetemp')] - config.parse(args) - return config - - def reparseconfig(self, args=None): - """ this is used from tests that want to re-invoke parse(). """ - if not args: - args = [self.tmpdir] - oldconfig = getattr(py.test, 'config', None) - try: - c = py.test.config = self.Config() - c.basetemp = oldconfig.mktemp("reparse", numbered=True) - c.parse(args) - return c - finally: - py.test.config = oldconfig - - def parseconfigure(self, *args): - config = self.parseconfig(*args) - config.pluginmanager.do_configure(config) - self.request.addfinalizer(lambda: - config.pluginmanager.do_unconfigure(config)) - return config - - def getitem(self, source, funcname="test_func"): - for item in self.getitems(source): - if item.name == funcname: - return item - assert 0, "%r item not found in module:\n%s" %(funcname, source) - - def getitems(self, source): - modcol = self.getmodulecol(source) - return self.genitems([modcol]) - - def getmodulecol(self, source, configargs=(), withinit=False): - kw = {self.request.function.__name__: py.code.Source(source).strip()} - path = self.makepyfile(**kw) - if withinit: - self.makepyfile(__init__ = "#") - self.config = config = self.parseconfigure(path, *configargs) - node = self.getnode(config, path) - #config.pluginmanager.do_unconfigure(config) - return node - - def collect_by_name(self, modcol, name): - for colitem in modcol._memocollect(): - if colitem.name == name: - return colitem - - def popen(self, cmdargs, stdout, stderr, **kw): - if not hasattr(py.std, 'subprocess'): - py.test.skip("no subprocess module") - env = os.environ.copy() - env['PYTHONPATH'] = ":".join(filter(None, [ - str(os.getcwd()), env.get('PYTHONPATH', '')])) - kw['env'] = env - #print "env", env - return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - - def pytestmain(self, *args, **kwargs): - ret = pytest.main(*args, **kwargs) - if ret == 2: - raise KeyboardInterrupt() - def run(self, *cmdargs): - return self._run(*cmdargs) - - def _run(self, *cmdargs): - cmdargs = [str(x) for x in cmdargs] - p1 = self.tmpdir.join("stdout") - p2 = self.tmpdir.join("stderr") - print_("running", cmdargs, "curdir=", py.path.local()) - f1 = p1.open("wb") - f2 = p2.open("wb") - now = time.time() - popen = self.popen(cmdargs, stdout=f1, stderr=f2, - close_fds=(sys.platform != "win32")) - ret = popen.wait() - f1.close() - f2.close() - out = p1.read("rb") - out = getdecoded(out).splitlines() - err = p2.read("rb") - err = getdecoded(err).splitlines() - def dump_lines(lines, fp): - try: - for line in lines: - py.builtin.print_(line, file=fp) - except UnicodeEncodeError: - print("couldn't print to %s because of encoding" % (fp,)) - dump_lines(out, sys.stdout) - dump_lines(err, sys.stderr) - return RunResult(ret, out, err, time.time()-now) - - def runpybin(self, scriptname, *args): - fullargs = self._getpybinargs(scriptname) + args - return self.run(*fullargs) - - def _getpybinargs(self, scriptname): - if not self.request.config.getvalue("notoolsonpath"): - script = py.path.local.sysfind(scriptname) - assert script, "script %r not found" % scriptname - return (py.std.sys.executable, script,) - else: - py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) - - def runpython(self, script, prepend=True): - 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): - command = self._getsysprepend() + command - return self.run(py.std.sys.executable, "-c", command) - - def runpytest(self, *args): - p = py.path.local.make_numbered_dir(prefix="runpytest-", - keep=None, rootdir=self.tmpdir) - args = ('--basetemp=%s' % p, ) + args - #for x in args: - # if '--confcutdir' in str(x): - # break - #else: - # pass - # args = ('--confcutdir=.',) + args - plugins = [x for x in self.plugins if isinstance(x, str)] - if plugins: - args = ('-p', plugins[0]) + args - return self.runpybin("py.test", *args) - - def spawn_pytest(self, string, expect_timeout=10.0): - if self.request.config.getvalue("notoolsonpath"): - py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") - basetemp = self.tmpdir.mkdir("pexpect") - invoke = self._getpybinargs("py.test")[0] - cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) - return self.spawn(cmd, expect_timeout=expect_timeout) - - def spawn(self, cmd, expect_timeout=10.0): - pexpect = py.test.importorskip("pexpect", "2.4") - logfile = self.tmpdir.join("spawn.out") - child = pexpect.spawn(cmd, logfile=logfile.open("w")) - child.timeout = expect_timeout - return child - -def getdecoded(out): - try: - return out.decode("utf-8") - except UnicodeDecodeError: - return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( - py.io.saferepr(out),) - -class PseudoPlugin: - def __init__(self, vars): - self.__dict__.update(vars) - -class ReportRecorder(object): - def __init__(self, hook): - self.hook = hook - self.pluginmanager = hook._pm - self.pluginmanager.register(self) - - def getcall(self, name): - return self.hookrecorder.getcall(name) - - def popcall(self, name): - return self.hookrecorder.popcall(name) - - def getcalls(self, names): - """ return list of ParsedCall instances matching the given eventname. """ - return self.hookrecorder.getcalls(names) - - # functionality for test reports - - def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): - return [x.report for x in self.getcalls(names)] - - def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport"): - """ return a testreport whose dotted import path matches """ - l = [] - for rep in self.getreports(names=names): - if not inamepart or inamepart in rep.nodeid.split("::"): - l.append(rep) - if not l: - raise ValueError("could not find test report matching %r: no test reports at all!" % - (inamepart,)) - if len(l) > 1: - raise ValueError("found more than one testreport matching %r: %s" %( - inamepart, l)) - return l[0] - - def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'): - return [rep for rep in self.getreports(names) if rep.failed] - - def getfailedcollections(self): - return self.getfailures('pytest_collectreport') - - def listoutcomes(self): - passed = [] - skipped = [] - failed = [] - for rep in self.getreports("pytest_runtest_logreport"): - if rep.passed: - if rep.when == "call": - passed.append(rep) - elif rep.skipped: - skipped.append(rep) - elif rep.failed: - failed.append(rep) - return passed, skipped, failed - - def countoutcomes(self): - return [len(x) for x in self.listoutcomes()] - - def assertoutcome(self, passed=0, skipped=0, failed=0): - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) - - def clear(self): - self.hookrecorder.calls[:] = [] - - def unregister(self): - self.pluginmanager.unregister(self) - self.hookrecorder.finish_recording() - -class LineComp: - def __init__(self): - self.stringio = py.io.TextIO() - - def assert_contains_lines(self, lines2): - """ assert that lines2 are contained (linearly) in lines1. - return a list of extralines found. - """ - __tracebackhide__ = True - val = self.stringio.getvalue() - self.stringio.truncate(0) - self.stringio.seek(0) - lines1 = val.split("\n") - return LineMatcher(lines1).fnmatch_lines(lines2) - -class LineMatcher: - def __init__(self, lines): - self.lines = lines - - def str(self): - return "\n".join(self.lines) - - def _getlines(self, lines2): - if isinstance(lines2, str): - lines2 = py.code.Source(lines2) - if isinstance(lines2, py.code.Source): - lines2 = lines2.strip().lines - return lines2 - - def fnmatch_lines_random(self, lines2): - lines2 = self._getlines(lines2) - for line in lines2: - for x in self.lines: - if line == x or fnmatch(x, line): - print_("matched: ", repr(line)) - break - else: - raise ValueError("line %r not found in output" % line) - - def fnmatch_lines(self, lines2): - def show(arg1, arg2): - py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) - lines2 = self._getlines(lines2) - lines1 = self.lines[:] - nextline = None - extralines = [] - __tracebackhide__ = True - for line in lines2: - nomatchprinted = False - while lines1: - nextline = lines1.pop(0) - if line == nextline: - show("exact match:", repr(line)) - break - elif fnmatch(nextline, line): - show("fnmatch:", repr(line)) - show(" with:", repr(nextline)) - break - else: - if not nomatchprinted: - show("nomatch:", repr(line)) - nomatchprinted = True - show(" and:", repr(nextline)) - extralines.append(nextline) - else: - py.test.fail("remains unmatched: %r, see stderr" % (line,)) --- a/pytest/plugin/genscript.py +++ /dev/null @@ -1,68 +0,0 @@ -""" generate a single-file self-contained version of py.test """ -import py -import pickle -import zlib -import base64 - -def find_toplevel(name): - for syspath in py.std.sys.path: - base = py.path.local(syspath) - lib = base/name - if lib.check(dir=1): - return lib - raise LookupError(name) - -def pkgname(toplevel, rootpath, path): - parts = path.parts()[len(rootpath.parts()):] - return '.'.join([toplevel] + [x.purebasename for x in parts]) - -def pkg_to_mapping(name): - toplevel = find_toplevel(name) - name2src = {} - for pyfile in toplevel.visit('*.py'): - pkg = pkgname(name, toplevel, pyfile) - name2src[pkg] = pyfile.read() - return name2src - - -def compress_mapping(mapping): - data = pickle.dumps(mapping, 2) - data = zlib.compress(data, 9) - data = base64.encodestring(data) - data = data.decode('ascii') - return data - - -def compress_packages(names): - mapping = {} - for name in names: - mapping.update(pkg_to_mapping(name)) - return compress_mapping(mapping) - - -def generate_script(entry, packages): - data = compress_packages(packages) - tmpl = py.path.local(__file__).dirpath().join('standalonetemplate.py') - exe = tmpl.read() - exe = exe.replace('@SOURCES@', data) - exe = exe.replace('@ENTRY@', entry) - return exe - - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group.addoption("--genscript", action="store", default=None, - dest="genscript", metavar="path", - help="create standalone py.test script at given target path.") - -def pytest_cmdline_main(config): - genscript = config.getvalue("genscript") - if genscript: - script = generate_script( - 'import py; raise SystemExit(py.test.cmdline.main())', - ['py', 'pytest'], - ) - - genscript = py.path.local(genscript) - genscript.write(script) - return 0 --- a/pytest/plugin/skipping.py +++ /dev/null @@ -1,213 +0,0 @@ -""" support for skip/xfail functions and markers. """ - -import py, pytest - -def pytest_addoption(parser): - group = parser.getgroup("general") - group.addoption('--runxfail', - action="store_true", dest="runxfail", default=False, - help="run tests even if they are marked xfail") - -def pytest_namespace(): - return dict(xfail=xfail) - -class XFailed(pytest.fail.Exception): - """ raised from an explicit call to py.test.xfail() """ - -def xfail(reason=""): - """ xfail an executing test or setup functions with the given reason.""" - __tracebackhide__ = True - raise XFailed(reason) -xfail.Exception = XFailed - -class MarkEvaluator: - def __init__(self, item, name): - self.item = item - self.name = name - - @property - def holder(self): - return self.item.keywords.get(self.name, None) - def __bool__(self): - return bool(self.holder) - __nonzero__ = __bool__ - - def istrue(self): - if self.holder: - d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config} - if self.holder.args: - self.result = False - for expr in self.holder.args: - self.expr = expr - if isinstance(expr, str): - result = cached_eval(self.item.config, expr, d) - else: - result = expr - if result: - self.result = True - self.expr = expr - break - else: - self.result = True - return getattr(self, 'result', False) - - def get(self, attr, default=None): - return self.holder.kwargs.get(attr, default) - - def getexplanation(self): - expl = self.get('reason', None) - if not expl: - if not hasattr(self, 'expr'): - return "" - else: - return "condition: " + self.expr - return expl - - -def pytest_runtest_setup(item): - if not isinstance(item, pytest.Function): - return - evalskip = MarkEvaluator(item, 'skipif') - if evalskip.istrue(): - py.test.skip(evalskip.getexplanation()) - item._evalxfail = MarkEvaluator(item, 'xfail') - check_xfail_no_run(item) - -def pytest_pyfunc_call(pyfuncitem): - check_xfail_no_run(pyfuncitem) - -def check_xfail_no_run(item): - if not item.config.option.runxfail: - evalxfail = item._evalxfail - if evalxfail.istrue(): - if not evalxfail.get('run', True): - py.test.xfail("[NOTRUN] " + evalxfail.getexplanation()) - -def pytest_runtest_makereport(__multicall__, item, call): - if not isinstance(item, pytest.Function): - return - if not (call.excinfo and - call.excinfo.errisinstance(py.test.xfail.Exception)): - evalxfail = getattr(item, '_evalxfail', None) - if not evalxfail: - return - if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception): - if not item.config.getvalue("runxfail"): - rep = __multicall__.execute() - rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg - rep.outcome = "skipped" - return rep - if call.when == "call": - rep = __multicall__.execute() - evalxfail = getattr(item, '_evalxfail') - if not item.config.getvalue("runxfail") and evalxfail.istrue(): - if call.excinfo: - rep.outcome = "skipped" - else: - rep.outcome = "failed" - rep.keywords['xfail'] = evalxfail.getexplanation() - else: - if 'xfail' in rep.keywords: - del rep.keywords['xfail'] - return rep - -# called by terminalreporter progress reporting -def pytest_report_teststatus(report): - if 'xfail' in report.keywords: - if report.skipped: - return "xfailed", "x", "xfail" - elif report.failed: - return "xpassed", "X", "XPASS" - -# called by the terminalreporter instance/plugin -def pytest_terminal_summary(terminalreporter): - tr = terminalreporter - if not tr.reportchars: - #for name in "xfailed skipped failed xpassed": - # if not tr.stats.get(name, 0): - # tr.write_line("HINT: use '-r' option to see extra " - # "summary info about tests") - # break - return - - lines = [] - for char in tr.reportchars: - if char == "x": - show_xfailed(terminalreporter, lines) - elif char == "X": - show_xpassed(terminalreporter, lines) - elif char in "fF": - show_failed(terminalreporter, lines) - elif char in "sS": - show_skipped(terminalreporter, lines) - if lines: - tr._tw.sep("=", "short test summary info") - for line in lines: - tr._tw.line(line) - -def show_failed(terminalreporter, lines): - tw = terminalreporter._tw - failed = terminalreporter.stats.get("failed") - if failed: - for rep in failed: - pos = rep.nodeid - lines.append("FAIL %s" %(pos, )) - -def show_xfailed(terminalreporter, lines): - xfailed = terminalreporter.stats.get("xfailed") - if xfailed: - for rep in xfailed: - pos = rep.nodeid - reason = rep.keywords['xfail'] - lines.append("XFAIL %s" % (pos,)) - if reason: - lines.append(" " + str(reason)) - -def show_xpassed(terminalreporter, lines): - xpassed = terminalreporter.stats.get("xpassed") - if xpassed: - for rep in xpassed: - pos = rep.nodeid - reason = rep.keywords['xfail'] - lines.append("XPASS %s %s" %(pos, reason)) - -def cached_eval(config, expr, d): - if not hasattr(config, '_evalcache'): - config._evalcache = {} - try: - return config._evalcache[expr] - except KeyError: - #import sys - #print >>sys.stderr, ("cache-miss: %r" % expr) - config._evalcache[expr] = x = eval(expr, d) - return x - - -def folded_skips(skipped): - d = {} - for event in skipped: - entry = event.longrepr.reprcrash - key = entry.path, entry.lineno, entry.message - d.setdefault(key, []).append(event) - l = [] - for key, events in d.items(): - l.append((len(events),) + key) - return l - -def show_skipped(terminalreporter, lines): - tr = terminalreporter - skipped = tr.stats.get('skipped', []) - if skipped: - #if not tr.hasopt('skipped'): - # tr.write_line( - # "%d skipped tests, specify -rs for more info" % - # len(skipped)) - # return - fskips = folded_skips(skipped) - if fskips: - #tr.write_sep("_", "skipped test summary") - for num, fspath, lineno, reason in fskips: - if reason.startswith("Skipped: "): - reason = reason[9:] - lines.append("SKIP [%d] %s:%d: %s" % - (num, fspath, lineno, reason)) --- a/pytest/plugin/terminal.py +++ /dev/null @@ -1,420 +0,0 @@ -""" terminal reporting of the full testing process. - -This is a good source for looking at the various reporting hooks. -""" -import py -import sys -import os - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "reporting", after="general") - group._addoption('-v', '--verbose', action="count", - dest="verbose", default=0, help="increase verbosity."), - group._addoption('-q', '--quiet', action="count", - dest="quiet", default=0, help="decreate verbosity."), - group._addoption('-r', - action="store", dest="reportchars", default=None, metavar="chars", - help="show extra test summary info as specified by chars (f)ailed, " - "(s)skipped, (x)failed, (X)passed.") - group._addoption('-l', '--showlocals', - action="store_true", dest="showlocals", default=False, - help="show locals in tracebacks (disabled by default).") - group._addoption('--report', - action="store", dest="report", default=None, metavar="opts", - help="(deprecated, use -r)") - group._addoption('--tb', metavar="style", - action="store", dest="tbstyle", default='long', - type="choice", choices=['long', 'short', 'no', 'line', 'native'], - help="traceback print mode (long/short/line/no).") - group._addoption('--fulltrace', - action="store_true", dest="fulltrace", default=False, - help="don't cut any tracebacks (default is to cut).") - -def pytest_configure(config): - config.option.verbose -= config.option.quiet - if config.option.collectonly: - reporter = CollectonlyReporter(config) - else: - # we try hard to make printing resilient against - # later changes on FD level. - stdout = py.std.sys.stdout - if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): - try: - newfd = os.dup(stdout.fileno()) - #print "got newfd", newfd - except ValueError: - pass - else: - stdout = os.fdopen(newfd, stdout.mode, 1) - config._toclose = stdout - reporter = TerminalReporter(config, stdout) - config.pluginmanager.register(reporter, 'terminalreporter') - if config.option.debug or config.option.traceconfig: - def mywriter(tags, args): - msg = " ".join(map(str, args)) - reporter.write_line("[traceconfig] " + msg) - config.trace.root.setprocessor("pytest:config", mywriter) - -def pytest_unconfigure(config): - if hasattr(config, '_toclose'): - #print "closing", config._toclose, config._toclose.fileno() - config._toclose.close() - -def getreportopt(config): - reportopts = "" - optvalue = config.option.report - if optvalue: - py.builtin.print_("DEPRECATED: use -r instead of --report option.", - file=py.std.sys.stderr) - if optvalue: - for setting in optvalue.split(","): - setting = setting.strip() - if setting == "skipped": - reportopts += "s" - elif setting == "xfailed": - reportopts += "x" - reportchars = config.option.reportchars - if reportchars: - for char in reportchars: - if char not in reportopts: - reportopts += char - return reportopts - -def pytest_report_teststatus(report): - if report.passed: - letter = "." - elif report.skipped: - letter = "s" - elif report.failed: - letter = "F" - if report.when != "call": - letter = "f" - return report.outcome, letter, report.outcome.upper() - -class TerminalReporter: - def __init__(self, config, file=None): - self.config = config - self.verbosity = self.config.option.verbose - self.showheader = self.verbosity >= 0 - self.showfspath = self.verbosity >= 0 - self.showlongtestinfo = self.verbosity > 0 - - self.stats = {} - self.curdir = py.path.local() - if file is None: - file = py.std.sys.stdout - self._tw = py.io.TerminalWriter(file) - self.currentfspath = None - self.reportchars = getreportopt(config) - - def hasopt(self, char): - char = {'xfailed': 'x', 'skipped': 's'}.get(char,char) - return char in self.reportchars - - def write_fspath_result(self, fspath, res): - if fspath != self.currentfspath: - self.currentfspath = fspath - #fspath = self.curdir.bestrelpath(fspath) - self._tw.line() - #relpath = self.curdir.bestrelpath(fspath) - self._tw.write(fspath + " ") - self._tw.write(res) - - def write_ensure_prefix(self, prefix, extra="", **kwargs): - if self.currentfspath != prefix: - self._tw.line() - self.currentfspath = prefix - self._tw.write(prefix) - if extra: - self._tw.write(extra, **kwargs) - self.currentfspath = -2 - - def ensure_newline(self): - if self.currentfspath: - self._tw.line() - self.currentfspath = None - - def write_line(self, line, **markup): - line = str(line) - self.ensure_newline() - self._tw.line(line, **markup) - - def write_sep(self, sep, title=None, **markup): - self.ensure_newline() - self._tw.sep(sep, title, **markup) - - def pytest_internalerror(self, excrepr): - for line in str(excrepr).split("\n"): - self.write_line("INTERNALERROR> " + line) - return 1 - - def pytest_plugin_registered(self, plugin): - if self.config.option.traceconfig: - msg = "PLUGIN registered: %s" %(plugin,) - # XXX this event may happen during setup/teardown time - # which unfortunately captures our output here - # which garbles our output if we use self.write_line - self.write_line(msg) - - def pytest_deselected(self, items): - self.stats.setdefault('deselected', []).extend(items) - - def pytest__teardown_final_logerror(self, report): - self.stats.setdefault("error", []).append(report) - - def pytest_runtest_logstart(self, nodeid, location): - # ensure that the path is printed before the - # 1st test of a module starts running - fspath = nodeid.split("::")[0] - if self.showlongtestinfo: - line = self._locationline(fspath, *location) - self.write_ensure_prefix(line, "") - elif self.showfspath: - self.write_fspath_result(fspath, "") - - def pytest_runtest_logreport(self, report): - rep = report - res = self.config.hook.pytest_report_teststatus(report=rep) - cat, letter, word = res - self.stats.setdefault(cat, []).append(rep) - if not letter and not word: - # probably passed setup/teardown - return - if self.verbosity <= 0: - if not hasattr(rep, 'node') and self.showfspath: - self.write_fspath_result(rep.fspath, letter) - else: - self._tw.write(letter) - else: - if isinstance(word, tuple): - word, markup = word - else: - if rep.passed: - markup = {'green':True} - elif rep.failed: - markup = {'red':True} - elif rep.skipped: - markup = {'yellow':True} - line = self._locationline(str(rep.fspath), *rep.location) - if not hasattr(rep, 'node'): - self.write_ensure_prefix(line, word, **markup) - #self._tw.write(word, **markup) - else: - self.ensure_newline() - if hasattr(rep, 'node'): - self._tw.write("[%s] " % rep.node.gateway.id) - self._tw.write(word, **markup) - self._tw.write(" " + line) - self.currentfspath = -2 - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - self.stats.setdefault("error", []).append(report) - self.write_fspath_result(report.fspath, "E") - elif report.skipped: - self.stats.setdefault("skipped", []).append(report) - self.write_fspath_result(report.fspath, "S") - - def pytest_sessionstart(self, session): - self._sessionstarttime = py.std.time.time() - if not self.showheader: - return - self.write_sep("=", "test session starts", bold=True) - verinfo = ".".join(map(str, sys.version_info[:3])) - msg = "platform %s -- Python %s" % (sys.platform, verinfo) - msg += " -- pytest-%s" % (py.test.__version__) - if self.verbosity > 0 or self.config.option.debug or \ - getattr(self.config.option, 'pastebin', None): - msg += " -- " + str(sys.executable) - self.write_line(msg) - lines = self.config.hook.pytest_report_header(config=self.config) - lines.reverse() - for line in flatten(lines): - self.write_line(line) - - def pytest_collection_finish(self): - if not self.showheader: - return - for i, testarg in enumerate(self.config.args): - self.write_line("test path %d: %s" %(i+1, testarg)) - - def pytest_sessionfinish(self, exitstatus, __multicall__): - __multicall__.execute() - self._tw.line("") - if exitstatus in (0, 1, 2): - self.summary_errors() - self.summary_failures() - self.config.hook.pytest_terminal_summary(terminalreporter=self) - if exitstatus == 2: - self._report_keyboardinterrupt() - self.summary_deselected() - self.summary_stats() - - def pytest_keyboard_interrupt(self, excinfo): - self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) - - def _report_keyboardinterrupt(self): - excrepr = self._keyboardinterrupt_memo - msg = excrepr.reprcrash.message - self.write_sep("!", msg) - if "KeyboardInterrupt" in msg: - if self.config.option.fulltrace: - excrepr.toterminal(self._tw) - else: - excrepr.reprcrash.toterminal(self._tw) - - def _locationline(self, collect_fspath, fspath, lineno, domain): - if fspath and fspath != collect_fspath: - fspath = "%s <- %s" % ( - self.curdir.bestrelpath(py.path.local(collect_fspath)), - self.curdir.bestrelpath(py.path.local(fspath))) - elif fspath: - fspath = self.curdir.bestrelpath(py.path.local(fspath)) - if lineno is not None: - lineno += 1 - if fspath and lineno and domain: - line = "%(fspath)s:%(lineno)s: %(domain)s" - elif fspath and domain: - line = "%(fspath)s: %(domain)s" - elif fspath and lineno: - line = "%(fspath)s:%(lineno)s %(extrapath)s" - else: - line = "[nolocation]" - return line % locals() + " " - - def _getfailureheadline(self, rep): - if hasattr(rep, 'location'): - fspath, lineno, domain = rep.location - return domain - else: - return "test session" # XXX? - - def _getcrashline(self, rep): - try: - return str(rep.longrepr.reprcrash) - except AttributeError: - try: - return str(rep.longrepr)[:50] - except AttributeError: - return "" - - # - # summaries for sessionfinish - # - - def summary_failures(self): - tbstyle = self.config.option.tbstyle - if 'failed' in self.stats and tbstyle != "no": - self.write_sep("=", "FAILURES") - for rep in self.stats['failed']: - if tbstyle == "line": - line = self._getcrashline(rep) - self.write_line(line) - else: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg) - rep.toterminal(self._tw) - - def summary_errors(self): - if 'error' in self.stats and self.config.option.tbstyle != "no": - self.write_sep("=", "ERRORS") - for rep in self.stats['error']: - msg = self._getfailureheadline(rep) - if not hasattr(rep, 'when'): - # collect - msg = "ERROR collecting " + msg - elif rep.when == "setup": - msg = "ERROR at setup of " + msg - elif rep.when == "teardown": - msg = "ERROR at teardown of " + msg - self.write_sep("_", msg) - rep.toterminal(self._tw) - - def summary_stats(self): - session_duration = py.std.time.time() - self._sessionstarttime - - keys = "failed passed skipped deselected".split() - for key in self.stats.keys(): - if key not in keys: - keys.append(key) - parts = [] - for key in keys: - val = self.stats.get(key, None) - if val: - parts.append("%d %s" %(len(val), key)) - line = ", ".join(parts) - # XXX coloring - msg = "%s in %.2f seconds" %(line, session_duration) - if self.verbosity >= 0: - self.write_sep("=", msg, bold=True) - else: - self.write_line(msg, bold=True) - - def summary_deselected(self): - if 'deselected' in self.stats: - self.write_sep("=", "%d tests deselected by %r" %( - len(self.stats['deselected']), self.config.option.keyword), bold=True) - - -class CollectonlyReporter: - INDENT = " " - - def __init__(self, config, out=None): - self.config = config - if out is None: - out = py.std.sys.stdout - self._tw = py.io.TerminalWriter(out) - self.indent = "" - self._failed = [] - - def outindent(self, line): - self._tw.line(self.indent + str(line)) - - def pytest_internalerror(self, excrepr): - for line in str(excrepr).split("\n"): - self._tw.line("INTERNALERROR> " + line) - - def pytest_collectstart(self, collector): - if collector.session != collector: - self.outindent(collector) - self.indent += self.INDENT - - def pytest_itemcollected(self, item): - self.outindent(item) - - def pytest_collectreport(self, report): - if not report.passed: - if hasattr(report.longrepr, 'reprcrash'): - msg = report.longrepr.reprcrash.message - else: - # XXX unify (we have CollectErrorRepr here) - msg = str(report.longrepr.longrepr) - self.outindent("!!! %s !!!" % msg) - #self.outindent("!!! error !!!") - self._failed.append(report) - self.indent = self.indent[:-len(self.INDENT)] - - def pytest_collection_finish(self): - if self._failed: - self._tw.sep("!", "collection failures") - for rep in self._failed: - rep.toterminal(self._tw) - return self._failed and 1 or 0 - -def repr_pythonversion(v=None): - if v is None: - v = sys.version_info - try: - return "%s.%s.%s-%s-%s" % v - except (TypeError, ValueError): - return str(v) - -def flatten(l): - for x in l: - if isinstance(x, (list, tuple)): - for y in flatten(x): - yield y - else: - yield x - --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -1,5 +1,5 @@ import pytest, py, sys -from pytest.plugin import python as funcargs +from _pytest import python as funcargs class TestModule: def test_failing_import(self, testdir): --- a/testing/plugin/test_mark.py +++ b/testing/plugin/test_mark.py @@ -1,5 +1,5 @@ import py -from pytest.plugin.mark import MarkGenerator as Mark +from _pytest.mark import MarkGenerator as Mark class TestMark: def test_pytest_mark_notcallable(self): --- /dev/null +++ b/_pytest/helpconfig.py @@ -0,0 +1,158 @@ +""" version info, help messages, tracing configuration. """ +import py +import pytest +import inspect, sys + +def pytest_addoption(parser): + group = parser.getgroup('debugconfig') + group.addoption('--version', action="store_true", + help="display pytest lib version and import information.") + group._addoption("-h", "--help", action="store_true", dest="help", + help="show help message and configuration info") + group._addoption('-p', action="append", dest="plugins", default = [], + metavar="name", + help="early-load given plugin (multi-allowed).") + group.addoption('--traceconfig', + action="store_true", dest="traceconfig", default=False, + help="trace considerations of conftest.py files."), + group._addoption('--nomagic', + action="store_true", dest="nomagic", default=False, + help="don't reinterpret asserts, no traceback cutting. ") + group.addoption('--debug', + action="store_true", dest="debug", default=False, + help="generate and show internal debugging information.") + + +def pytest_cmdline_main(config): + if config.option.version: + p = py.path.local(pytest.__file__) + sys.stderr.write("This is py.test version %s, imported from %s\n" % + (pytest.__version__, p)) + return 0 + elif config.option.help: + config.pluginmanager.do_configure(config) + showhelp(config) + return 0 + +def showhelp(config): + tw = py.io.TerminalWriter() + tw.write(config._parser.optparser.format_help()) + tw.line() + tw.line() + #tw.sep( "=", "config file settings") + tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") + tw.line() + + for name in config._parser._ininames: + help, type, default = config._parser._inidict[name] + if type is None: + type = "string" + spec = "%s (%s)" % (name, type) + line = " %-24s %s" %(spec, help) + tw.line(line[:tw.fullwidth]) + + tw.line() ; tw.line() + #tw.sep("=") + return + + tw.line("conftest.py options:") + tw.line() + conftestitems = sorted(config._parser._conftestdict.items()) + for name, help in conftest_options + conftestitems: + line = " %-15s %s" %(name, help) + tw.line(line[:tw.fullwidth]) + tw.line() + #tw.sep( "=") + +conftest_options = [ + ('pytest_plugins', 'list of plugin names to load'), +] + +def pytest_report_header(config): + lines = [] + if config.option.debug or config.option.traceconfig: + lines.append("using: pytest-%s pylib-%s" % + (pytest.__version__,py.__version__)) + + if config.option.traceconfig: + lines.append("active plugins:") + plugins = [] + items = config.pluginmanager._name2plugin.items() + for name, plugin in items: + lines.append(" %-20s: %s" %(name, repr(plugin))) + return lines + + +# ===================================================== +# validate plugin syntax and hooks +# ===================================================== + +def pytest_plugin_registered(manager, plugin): + methods = collectattr(plugin) + hooks = {} + for hookspec in manager.hook._hookspecs: + hooks.update(collectattr(hookspec)) + + stringio = py.io.TextIO() + def Print(*args): + if args: + stringio.write(" ".join(map(str, args))) + stringio.write("\n") + + fail = False + while methods: + name, method = methods.popitem() + #print "checking", name + if isgenerichook(name): + continue + if name not in hooks: + if not getattr(method, 'optionalhook', False): + Print("found unknown hook:", name) + fail = True + else: + #print "checking", method + method_args = getargs(method) + #print "method_args", method_args + if '__multicall__' in method_args: + method_args.remove('__multicall__') + hook = hooks[name] + hookargs = getargs(hook) + for arg in method_args: + if arg not in hookargs: + Print("argument %r not available" %(arg, )) + Print("actual definition: %s" %(formatdef(method))) + Print("available hook arguments: %s" % + ", ".join(hookargs)) + fail = True + break + #if not fail: + # print "matching hook:", formatdef(method) + if fail: + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue())) + +class PluginValidationError(Exception): + """ plugin failed validation. """ + +def isgenerichook(name): + return name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + +def getargs(func): + args = inspect.getargs(py.code.getrawcode(func))[0] + startindex = inspect.ismethod(func) and 1 or 0 + return args[startindex:] + +def collectattr(obj): + methods = {} + for apiname in dir(obj): + if apiname.startswith("pytest_"): + methods[apiname] = getattr(obj, apiname) + return methods + +def formatdef(func): + return "%s%s" %( + func.__name__, + inspect.formatargspec(*inspect.getargspec(func)) + ) + --- a/testing/plugin/test_genscript.py +++ b/testing/plugin/test_genscript.py @@ -22,7 +22,7 @@ def test_gen(testdir, anypython, standal result = standalone.run(anypython, testdir, '--version') assert result.ret == 0 result.stderr.fnmatch_lines([ - "*imported from*mypytest" + "*imported from*mypytest*" ]) p = testdir.makepyfile("def test_func(): assert 0") result = standalone.run(anypython, testdir, p) --- a/pytest/main.py +++ /dev/null @@ -1,450 +0,0 @@ -""" -pytest PluginManager, basic initialization and tracing. -All else is in pytest/plugin. -(c) Holger Krekel 2004-2010 -""" -import sys, os -import inspect -import py -import pytest -from pytest import hookspec # the extension point definitions - -assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) - -default_plugins = ( - "config session terminal runner python pdb capture unittest mark skipping " - "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml doctest").split() - -IMPORTPREFIX = "pytest_" - -class TagTracer: - def __init__(self, prefix="[pytest] "): - self._tag2proc = {} - self.writer = None - self.indent = 0 - self.prefix = prefix - - def get(self, name): - return TagTracerSub(self, (name,)) - - def processmessage(self, tags, args): - if self.writer is not None: - if args: - indent = " " * self.indent - content = " ".join(map(str, args)) - self.writer("%s%s%s\n" %(self.prefix, indent, content)) - try: - self._tag2proc[tags](tags, args) - except KeyError: - pass - - def setwriter(self, writer): - self.writer = writer - - def setprocessor(self, tags, processor): - if isinstance(tags, str): - tags = tuple(tags.split(":")) - else: - 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,)) - -class PluginManager(object): - def __init__(self, load=False): - self._name2plugin = {} - self._plugins = [] - self._hints = [] - self.trace = TagTracer().get("pluginmanage") - if os.environ.get('PYTEST_DEBUG'): - err = sys.stderr - encoding = getattr(err, 'encoding', 'utf8') - try: - err = py.io.dupfile(err, encoding=encoding) - except Exception: - pass - self.trace.root.setwriter(err.write) - self.hook = HookRelay([hookspec], pm=self) - self.register(self) - if load: - for spec in default_plugins: - self.import_plugin(spec) - - def _getpluginname(self, plugin, name): - if name is None: - if hasattr(plugin, '__name__'): - name = plugin.__name__.split(".")[-1] - else: - name = id(plugin) - return name - - def register(self, plugin, name=None, prepend=False): - assert not self.isregistered(plugin), plugin - assert not self.isregistered(plugin), plugin - name = self._getpluginname(plugin, name) - if name in self._name2plugin: - return False - self._name2plugin[name] = plugin - self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) - self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - return True - - def unregister(self, plugin=None, name=None): - if plugin is None: - plugin = self.getplugin(name=name) - self._plugins.remove(plugin) - self.hook.pytest_plugin_unregistered(plugin=plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - - def isregistered(self, plugin, name=None): - if self._getpluginname(plugin, name) in self._name2plugin: - return True - for val in self._name2plugin.values(): - if plugin == val: - return True - - def addhooks(self, spec): - self.hook._addhooks(spec, prefix="pytest_") - - def getplugins(self): - return list(self._plugins) - - def skipifmissing(self, name): - if not self.hasplugin(name): - py.test.skip("plugin %r is missing" % name) - - def hasplugin(self, name): - try: - self.getplugin(name) - return True - except KeyError: - return False - - def getplugin(self, name): - try: - return self._name2plugin[name] - except KeyError: - impname = canonical_importname(name) - return self._name2plugin[impname] - - # API for bootstrapping - # - def _envlist(self, varname): - val = py.std.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 - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = canonical_importname(ep.name) - if name in self._name2plugin: - continue - plugin = ep.load() - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.import_plugin(opt2) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__): - 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, spec): - assert isinstance(spec, str) - modname = canonical_importname(spec) - if modname in self._name2plugin: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except: - e = py.std.sys.exc_info()[1] - if not hasattr(py.test, 'skip'): - raise - elif not isinstance(e, py.test.skip.Exception): - raise - self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) - - def pytest_plugin_registered(self, plugin): - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(pytest, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) - - def _setns(self, obj, dic): - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - modname = "pytest.%s" % name - mod = py.std.types.ModuleType(modname) - sys.modules[modname] = mod - mod.__all__ = [] - setattr(obj, name, mod) - #print "setns", mod, value - self._setns(mod, value) - else: - #print "setting", name, value, "on", obj - setattr(obj, name, value) - obj.__all__.append(name) - #print "appending", name, "to", obj - #pytest.__all__.append(name) # don't show in help(py.test) - setattr(pytest, name, value) - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - def do_addoption(self, parser): - mname = "pytest_addoption" - methods = reversed(self.listattr(mname)) - MultiCall(methods, {'parser': parser}).execute() - - def do_configure(self, config): - assert not hasattr(self, '_config') - self._config = config - config.hook.pytest_configure(config=self._config) - - def do_unconfigure(self, config): - config = self._config - del self._config - config.hook.pytest_unconfigure(config=config) - config.pluginmanager.unregister(self) - - def notify_exception(self, excinfo): - excrepr = excinfo.getrepr(funcargs=True, showlocals=True) - res = self.hook.pytest_internalerror(excrepr=excrepr) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - for plugin in plugins: - try: - l.append(getattr(plugin, attrname)) - except AttributeError: - continue - return l - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() - -def canonical_importname(name): - if '.' in name: - return name - name = name.lower() - if not name.startswith(IMPORTPREFIX): - name = IMPORTPREFIX + name - return name - -def importplugin(importspec): - #print "importing", importspec - try: - return __import__(importspec, None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(importspec) == -1: - raise - name = importspec - try: - if name.startswith("pytest_"): - name = importspec[7:] - return __import__("pytest.plugin.%s" %(name), None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(name) == -1: - raise - # show the original exception, not the failing internal one - return __import__(importspec, None, None, '__doc__') - - -class MultiCall: - """ execute a call into multiple python functions/methods. """ - def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) - self.kwargs = kwargs - 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): - while self.methods: - method = self.methods.pop() - kwargs = self.getkwargs(method) - res = method(**kwargs) - if res is not None: - self.results.append(res) - if self.firstresult: - return res - if not self.firstresult: - return self.results - - def getkwargs(self, method): - kwargs = {} - for argname in varnames(method): - try: - kwargs[argname] = self.kwargs[argname] - except KeyError: - if argname == "__multicall__": - kwargs[argname] = self - return kwargs - -def varnames(func): - if not inspect.isfunction(func) and not inspect.ismethod(func): - func = getattr(func, '__call__', func) - ismethod = inspect.ismethod(func) - rawcode = py.code.getrawcode(func) - try: - return rawcode.co_varnames[ismethod:rawcode.co_argcount] - except AttributeError: - return () - -class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] - self._hookspecs = [] - self._pm = pm - self.trace = pm.trace.root.get("hook") - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspecs, prefix): - self._hookspecs.append(hookspecs) - added = False - for name, method in vars(hookspecs).items(): - if name.startswith(prefix): - if not method.__doc__: - raise ValueError("docstring required for hook %r, in %r" - % (method, hookspecs)) - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult) - 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, hookspecs,)) - - -class HookCaller: - def __init__(self, hookrelay, name, firstresult): - self.hookrelay = hookrelay - self.name = name - self.firstresult = firstresult - self.trace = self.hookrelay.trace - - def __repr__(self): - return "" %(self.name,) - - def __call__(self, **kwargs): - methods = self.hookrelay._pm.listattr(self.name) - return self._docall(methods, kwargs) - - def pcall(self, plugins, **kwargs): - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) - return self._docall(methods, kwargs) - - def _docall(self, methods, kwargs): - self.trace(self.name, kwargs) - self.trace.root.indent += 1 - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - try: - res = mc.execute() - if res: - self.trace("finish", self.name, "-->", res) - finally: - self.trace.root.indent -= 1 - return res - -_preinit = [PluginManager(load=True)] # triggers default plugin importing - -def main(args=None, plugins=None): - """ returned exit code integer, after an in-process testing run - with the given command line arguments, preloading an optional list - of passed in plugin objects. """ - if args is None: - args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] - elif not isinstance(args, (tuple, list)): - if not isinstance(args, str): - raise ValueError("not a string or argument list: %r" % (args,)) - args = py.std.shlex.split(args) - if _preinit: - _pluginmanager = _preinit.pop(0) - else: # subsequent calls to main will create a fresh instance - _pluginmanager = PluginManager(load=True) - hook = _pluginmanager.hook - try: - if plugins: - for plugin in plugins: - _pluginmanager.register(plugin) - config = hook.pytest_cmdline_parse( - pluginmanager=_pluginmanager, args=args) - exitstatus = hook.pytest_cmdline_main(config=config) - except UsageError: - e = sys.exc_info()[1] - sys.stderr.write("ERROR: %s\n" %(e.args[0],)) - exitstatus = 3 - return exitstatus - -class UsageError(Exception): - """ error in py.test usage or invocation""" - -if __name__ == '__main__': - raise SystemExit(main()) --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -2,7 +2,7 @@ creating and managing test function arguments ============================================================== -.. currentmodule:: pytest.plugin.python +.. currentmodule:: _pytest.python .. _`funcargs`: @@ -92,7 +92,7 @@ Each funcarg factory receives a **reques specific test function call. A request object is passed to a funcarg factory and provides access to test configuration and context: -.. autoclass:: pytest.plugin.python.FuncargRequest() +.. autoclass:: _pytest.python.FuncargRequest() :members: function,cls,module,keywords,config .. _`useful caching and finalization helpers`: --- a/pytest/plugin/junitxml.py +++ /dev/null @@ -1,174 +0,0 @@ -""" report test results in JUnit-XML format, for use with Hudson and build integration servers. - -Based on initial code from Ross Lawley. -""" - -import py -import os -import time - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group.addoption('--junitxml', action="store", dest="xmlpath", - metavar="path", default=None, - help="create junit-xml style report file at given path.") - group.addoption('--junitprefix', action="store", dest="junitprefix", - metavar="str", default=None, - help="prepend prefix to classnames in junit-xml output") - -def pytest_configure(config): - xmlpath = config.option.xmlpath - if xmlpath: - config._xml = LogXML(xmlpath, config.option.junitprefix) - config.pluginmanager.register(config._xml) - -def pytest_unconfigure(config): - xml = getattr(config, '_xml', None) - if xml: - del config._xml - config.pluginmanager.unregister(xml) - -class LogXML(object): - def __init__(self, logfile, prefix): - self.logfile = logfile - self.prefix = prefix - self.test_logs = [] - self.passed = self.skipped = 0 - self.failed = self.errors = 0 - self._durations = {} - - def _opentestcase(self, report): - names = report.nodeid.split("::") - names[0] = names[0].replace("/", '.') - names = tuple(names) - d = {'time': self._durations.pop(names, "0")} - names = [x.replace(".py", "") for x in names if x != "()"] - classnames = names[:-1] - if self.prefix: - classnames.insert(0, self.prefix) - d['classname'] = ".".join(classnames) - d['name'] = py.xml.escape(names[-1]) - attrs = ['%s="%s"' % item for item in sorted(d.items())] - self.test_logs.append("\n" % " ".join(attrs)) - - def _closetestcase(self): - self.test_logs.append("") - - def appendlog(self, fmt, *args): - args = tuple([py.xml.escape(arg) for arg in args]) - self.test_logs.append(fmt % args) - - def append_pass(self, report): - self.passed += 1 - self._opentestcase(report) - self._closetestcase() - - def append_failure(self, report): - self._opentestcase(report) - #msg = str(report.longrepr.reprtraceback.extraline) - if "xfail" in report.keywords: - self.appendlog( - '') - self.skipped += 1 - else: - self.appendlog('%s', - report.longrepr) - self.failed += 1 - self._closetestcase() - - def append_collect_failure(self, report): - self._opentestcase(report) - #msg = str(report.longrepr.reprtraceback.extraline) - self.appendlog('%s', - report.longrepr) - self._closetestcase() - self.errors += 1 - - def append_collect_skipped(self, report): - self._opentestcase(report) - #msg = str(report.longrepr.reprtraceback.extraline) - self.appendlog('%s', - report.longrepr) - self._closetestcase() - self.skipped += 1 - - def append_error(self, report): - self._opentestcase(report) - self.appendlog('%s', - report.longrepr) - self._closetestcase() - self.errors += 1 - - def append_skipped(self, report): - self._opentestcase(report) - if "xfail" in report.keywords: - self.appendlog( - '%s', - report.keywords['xfail']) - else: - self.appendlog("") - self._closetestcase() - self.skipped += 1 - - def pytest_runtest_logreport(self, report): - if report.passed: - self.append_pass(report) - elif report.failed: - if report.when != "call": - self.append_error(report) - else: - self.append_failure(report) - elif report.skipped: - self.append_skipped(report) - - def pytest_runtest_call(self, item, __multicall__): - names = tuple(item.listnames()) - start = time.time() - try: - return __multicall__.execute() - finally: - self._durations[names] = time.time() - start - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - self.append_collect_failure(report) - else: - self.append_collect_skipped(report) - - def pytest_internalerror(self, excrepr): - self.errors += 1 - data = py.xml.escape(excrepr) - self.test_logs.append( - '\n' - ' ' - '%s' % data) - - def pytest_sessionstart(self, session): - self.suite_start_time = time.time() - - def pytest_sessionfinish(self, session, exitstatus, __multicall__): - if py.std.sys.version_info[0] < 3: - logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') - else: - logfile = open(self.logfile, 'w', encoding='utf-8') - - suite_stop_time = time.time() - suite_time_delta = suite_stop_time - self.suite_start_time - numtests = self.passed + self.failed - logfile.write('') - logfile.write('') - logfile.writelines(self.test_logs) - logfile.write('') - logfile.close() - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile)) --- a/doc/mark.txt +++ b/doc/mark.txt @@ -4,7 +4,7 @@ mark test functions with attributes ================================================================= -.. currentmodule:: pytest.plugin.mark +.. currentmodule:: _pytest.mark By using the ``py.test.mark`` helper you can instantiate decorators that will set named meta data on test functions. --- /dev/null +++ b/_pytest/nose.py @@ -0,0 +1,47 @@ +"""run test suites written for nose. """ + +import pytest, py +import inspect +import sys + +def pytest_runtest_makereport(__multicall__, item, call): + SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) + if SkipTest: + if call.excinfo and call.excinfo.errisinstance(SkipTest): + # let's substitute the excinfo with a py.test.skip one + call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) + call.excinfo = call2.excinfo + + +def pytest_runtest_setup(item): + if isinstance(item, (pytest.Function)): + if isinstance(item.parent, pytest.Generator): + gen = item.parent + if not hasattr(gen, '_nosegensetup'): + call_optional(gen.obj, 'setup') + if isinstance(gen.parent, pytest.Instance): + call_optional(gen.parent.obj, 'setup') + gen._nosegensetup = True + if not call_optional(item.obj, 'setup'): + # call module level setup if there is no object level one + call_optional(item.parent.obj, 'setup') + +def pytest_runtest_teardown(item): + if isinstance(item, pytest.Function): + if not call_optional(item.obj, 'teardown'): + call_optional(item.parent.obj, 'teardown') + #if hasattr(item.parent, '_nosegensetup'): + # #call_optional(item._nosegensetup, 'teardown') + # del item.parent._nosegensetup + +def pytest_make_collect_report(collector): + if isinstance(collector, pytest.Generator): + call_optional(collector.obj, 'setup') + +def call_optional(obj, name): + method = getattr(obj, name, None) + if method: + # If there's any problems allow the exception to raise rather than + # silently ignoring them + method() + return True --- a/pytest/plugin/config.py +++ /dev/null @@ -1,453 +0,0 @@ -""" command line configuration, ini-file and conftest.py processing. """ - -import py -import sys, os -from pytest.main import PluginManager -import pytest - -def pytest_cmdline_parse(pluginmanager, args): - config = Config(pluginmanager) - config.parse(args) - if config.option.debug: - config.trace.root.setwriter(sys.stderr.write) - return config - -class Parser: - """ Parser for command line arguments. """ - - def __init__(self, usage=None, processopt=None): - self._anonymous = OptionGroup("custom options", parser=self) - self._groups = [] - self._processopt = processopt - self._usage = usage - self._inidict = {} - self._ininames = [] - self.hints = [] - - def processoption(self, option): - if self._processopt: - if option.dest: - self._processopt(option) - - def addnote(self, note): - self._notes.append(note) - - def getgroup(self, name, description="", after=None): - """ get (or create) a named option Group. - - :name: unique name of the option group. - :description: long description for --help output. - :after: name of other group, used for ordering --help output. - """ - for group in self._groups: - if group.name == name: - return group - group = OptionGroup(name, description, parser=self) - i = 0 - for i, grp in enumerate(self._groups): - if grp.name == after: - break - self._groups.insert(i+1, group) - return group - - def addoption(self, *opts, **attrs): - """ add an optparse-style option. """ - self._anonymous.addoption(*opts, **attrs) - - def parse(self, args): - self.optparser = optparser = MyOptionParser(self) - groups = self._groups + [self._anonymous] - for group in groups: - if group.options: - desc = group.description or group.name - optgroup = py.std.optparse.OptionGroup(optparser, desc) - optgroup.add_options(group.options) - optparser.add_option_group(optgroup) - return self.optparser.parse_args([str(x) for x in args]) - - def parse_setoption(self, args, option): - parsedoption, args = self.parse(args) - for name, value in parsedoption.__dict__.items(): - setattr(option, name, value) - return args - - def addini(self, name, help, type=None, default=None): - """ add an ini-file option with the given name and description. """ - assert type in (None, "pathlist", "args", "linelist") - self._inidict[name] = (help, type, default) - self._ininames.append(name) - -class OptionGroup: - def __init__(self, name, description="", parser=None): - self.name = name - self.description = description - self.options = [] - self.parser = parser - - def addoption(self, *optnames, **attrs): - """ add an option to this group. """ - option = py.std.optparse.Option(*optnames, **attrs) - self._addoption_instance(option, shortupper=False) - - def _addoption(self, *optnames, **attrs): - option = py.std.optparse.Option(*optnames, **attrs) - self._addoption_instance(option, shortupper=True) - - def _addoption_instance(self, option, shortupper=False): - if not shortupper: - for opt in option._short_opts: - if opt[0] == '-' and opt[1].islower(): - raise ValueError("lowercase shortoptions reserved") - if self.parser: - self.parser.processoption(option) - self.options.append(option) - - -class MyOptionParser(py.std.optparse.OptionParser): - def __init__(self, parser): - self._parser = parser - py.std.optparse.OptionParser.__init__(self, usage=parser._usage, - add_help_option=False) - def format_epilog(self, formatter): - hints = self._parser.hints - if hints: - s = "\n".join(["hint: " + x for x in hints]) + "\n" - s = "\n" + s + "\n" - return s - return "" - -class Conftest(object): - """ the single place for accessing values and interacting - towards conftest modules from py.test objects. - """ - def __init__(self, onimport=None, confcutdir=None): - self._path2confmods = {} - self._onimport = onimport - self._conftestpath2mod = {} - self._confcutdir = confcutdir - self._md5cache = {} - - def setinitial(self, args): - """ try to find a first anchor path for looking up global values - from conftests. This function is usually called _before_ - argument parsing. conftest files may add command line options - and we thus have no completely safe way of determining - which parts of the arguments are actually related to options - and which are file system paths. We just try here to get - bootstrapped ... - """ - current = py.path.local() - opt = '--confcutdir' - for i in range(len(args)): - opt1 = str(args[i]) - if opt1.startswith(opt): - if opt1 == opt: - if len(args) > i: - p = current.join(args[i+1], abs=True) - elif opt1.startswith(opt + "="): - p = current.join(opt1[len(opt)+1:], abs=1) - self._confcutdir = p - break - for arg in args + [current]: - if hasattr(arg, 'startswith') and arg.startswith("--"): - continue - anchor = current.join(arg, abs=1) - if anchor.check(): # we found some file object - self._path2confmods[None] = self.getconftestmodules(anchor) - # let's also consider test* dirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self.getconftestmodules(x) - break - else: - assert 0, "no root of filesystem?" - - def getconftestmodules(self, path): - """ return a list of imported conftest modules for the given path. """ - try: - clist = self._path2confmods[path] - except KeyError: - if path is None: - raise ValueError("missing default confest.") - dp = path.dirpath() - clist = [] - if dp != path: - cutdir = self._confcutdir - if cutdir and path != cutdir and not path.relto(cutdir): - pass - else: - conftestpath = path.join("conftest.py") - if conftestpath.check(file=1): - key = conftestpath.computehash() - # XXX logging about conftest loading - if key not in self._md5cache: - clist.append(self.importconftest(conftestpath)) - self._md5cache[key] = conftestpath - else: - # use some kind of logging - print ("WARN: not loading %s" % conftestpath) - clist[:0] = self.getconftestmodules(dp) - self._path2confmods[path] = clist - # be defensive: avoid changes from caller side to - # affect us by always returning a copy of the actual list - return clist[:] - - def rget(self, name, path=None): - mod, value = self.rget_with_confmod(name, path) - return value - - def rget_with_confmod(self, name, path=None): - modules = self.getconftestmodules(path) - modules.reverse() - for mod in modules: - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def importconftest(self, conftestpath): - assert conftestpath.check(), conftestpath - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport() - 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._postimport(mod) - return mod - - def _postimport(self, mod): - if self._onimport: - self._onimport(mod) - return mod - -def _ensure_removed_sysmodule(modname): - try: - del sys.modules[modname] - except KeyError: - pass - -class CmdOptions(object): - """ holds cmdline options as attributes.""" - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - def __repr__(self): - return "" %(self.__dict__,) - -class Config(object): - """ access to configuration values, pluginmanager and plugin hooks. """ - basetemp = None - - def __init__(self, pluginmanager=None): - #: command line option values, usually added via parser.addoption(...) - #: or parser.getgroup(...).addoption(...) calls - self.option = CmdOptions() - self._parser = Parser( - usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", - processopt=self._processopt, - ) - #: a pluginmanager instance - self.pluginmanager = pluginmanager or PluginManager(load=True) - self.trace = self.pluginmanager.trace.root.get("config") - self._conftest = Conftest(onimport=self._onimportconftest) - self.hook = self.pluginmanager.hook - - def _onimportconftest(self, conftestmodule): - self.trace("loaded conftestmodule %r" %(conftestmodule,)) - self.pluginmanager.consider_conftest(conftestmodule) - - def _processopt(self, opt): - if hasattr(opt, 'default') and opt.dest: - if not hasattr(self.option, opt.dest): - setattr(self.option, opt.dest, opt.default) - - def _getmatchingplugins(self, fspath): - allconftests = self._conftest._conftestpath2mod.values() - plugins = [x for x in self.pluginmanager.getplugins() - if x not in allconftests] - plugins += self._conftest.getconftestmodules(fspath) - return plugins - - def _setinitialconftest(self, args): - # capture output during conftest init (#issue93) - name = hasattr(os, 'dup') and 'StdCaptureFD' or 'StdCapture' - cap = getattr(py.io, name)() - try: - try: - self._conftest.setinitial(args) - finally: - out, err = cap.reset() - except: - sys.stdout.write(out) - sys.stderr.write(err) - raise - - def _initini(self, args): - self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) - self._parser.addini('addopts', 'extra command line options', 'args') - self._parser.addini('minversion', 'minimally required pytest version') - - def _preparse(self, args, addopts=True): - self._initini(args) - if addopts: - args[:] = self.getini("addopts") + args - self._checkversion() - self.pluginmanager.consider_setuptools_entrypoints() - self.pluginmanager.consider_env() - self.pluginmanager.consider_preparse(args) - self._setinitialconftest(args) - self.pluginmanager.do_addoption(self._parser) - - def _checkversion(self): - minver = self.inicfg.get('minversion', None) - if minver: - ver = minver.split(".") - myver = pytest.__version__.split(".") - if myver < ver: - raise pytest.UsageError( - "%s:%d: requires pytest-%s, actual pytest-%s'" %( - self.inicfg.config.path, self.inicfg.lineof('minversion'), - minver, pytest.__version__)) - - def parse(self, args): - # parse given cmdline arguments into this config object. - # Note that this can only be called once per testing process. - assert not hasattr(self, 'args'), ( - "can only parse cmdline args at most once per Config object") - self._preparse(args) - self._parser.hints.extend(self.pluginmanager._hints) - args = self._parser.parse_setoption(args, self.option) - if not args: - args.append(py.std.os.getcwd()) - self.args = args - - def ensuretemp(self, string, dir=True): - return self.getbasetemp().ensure(string, dir=dir) - - def getbasetemp(self): - if self.basetemp is None: - basetemp = self.option.basetemp - if basetemp: - basetemp = py.path.local(basetemp) - if not basetemp.check(dir=1): - basetemp.mkdir() - else: - basetemp = py.path.local.make_numbered_dir(prefix='pytest-') - self.basetemp = basetemp - return self.basetemp - - def mktemp(self, basename, numbered=False): - basetemp = self.getbasetemp() - if not numbered: - return basetemp.mkdir(basename) - else: - return py.path.local.make_numbered_dir(prefix=basename, - keep=0, rootdir=basetemp, lock_timeout=None) - - def getini(self, name): - """ return configuration value from an ini file. If the - specified name hasn't been registered through a prior ``parse.addini`` - call (usually from a plugin), a ValueError is raised. """ - try: - description, type, default = self._parser._inidict[name] - except KeyError: - raise ValueError("unknown configuration value: %r" %(name,)) - try: - value = self.inicfg[name] - except KeyError: - if default is not None: - return default - if type is None: - return '' - return [] - if type == "pathlist": - dp = py.path.local(self.inicfg.config.path).dirpath() - l = [] - for relpath in py.std.shlex.split(value): - l.append(dp.join(relpath, abs=True)) - return l - elif type == "args": - return py.std.shlex.split(value) - elif type == "linelist": - return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] - else: - assert type is None - return value - - def _getconftest_pathlist(self, name, path=None): - try: - mod, relroots = self._conftest.rget_with_confmod(name, path) - except KeyError: - return None - modpath = py.path.local(mod.__file__).dirpath() - l = [] - for relroot in relroots: - if not isinstance(relroot, py.path.local): - relroot = relroot.replace("/", py.path.local.sep) - relroot = modpath.join(relroot, abs=True) - l.append(relroot) - return l - - def _getconftest(self, name, path=None, check=False): - if check: - self._checkconftest(name) - return self._conftest.rget(name, path) - - def getvalue(self, name, path=None): - """ return ``name`` value looked set from command line options. - - (deprecated) if we can't find the option also lookup - the name in a matching conftest file. - """ - try: - return getattr(self.option, name) - except AttributeError: - return self._getconftest(name, path, check=False) - - def getvalueorskip(self, name, path=None): - """ (deprecated) return getvalue(name) or call py.test.skip if no value exists. """ - try: - val = self.getvalue(name, path) - if val is None: - raise KeyError(name) - return val - except KeyError: - py.test.skip("no %r value found" %(name,)) - - -def getcfg(args, inibasenames): - args = [x for x in args if str(x)[0] != "-"] - if not args: - args = [py.path.local()] - for arg in args: - arg = py.path.local(arg) - if arg.check(): - for base in arg.parts(reverse=True): - for inibasename in inibasenames: - p = base.join(inibasename) - if p.check(): - iniconfig = py.iniconfig.IniConfig(p) - if 'pytest' in iniconfig.sections: - return iniconfig['pytest'] - return {} - -def findupwards(current, basename): - current = py.path.local(current) - while 1: - p = current.join(basename) - if p.check(): - return p - p = current.dirpath() - if p == current: - return - current = p - --- a/pytest/plugin/mark.py +++ /dev/null @@ -1,176 +0,0 @@ -""" generic mechanism for marking and selecting python functions. """ -import pytest, py - -def pytest_namespace(): - return {'mark': MarkGenerator()} - -def pytest_addoption(parser): - group = parser.getgroup("general") - group._addoption('-k', - action="store", dest="keyword", default='', metavar="KEYWORDEXPR", - help="only run tests which match given keyword expression. " - "An expression consists of space-separated terms. " - "Each term must match. Precede a term with '-' to negate. " - "Terminate expression with ':' to make the first match match " - "all subsequent tests (usually file-order). ") - -def pytest_collection_modifyitems(items, config): - keywordexpr = config.option.keyword - if not keywordexpr: - return - selectuntil = False - if keywordexpr[-1] == ":": - selectuntil = True - keywordexpr = keywordexpr[:-1] - - remaining = [] - deselected = [] - for colitem in items: - if keywordexpr and skipbykeyword(colitem, keywordexpr): - deselected.append(colitem) - else: - remaining.append(colitem) - if selectuntil: - keywordexpr = None - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - -def skipbykeyword(colitem, keywordexpr): - """ return True if they given keyword expression means to - skip this collector/item. - """ - if not keywordexpr: - return - - itemkeywords = getkeywords(colitem) - for key in filter(None, keywordexpr.split()): - eor = key[:1] == '-' - if eor: - key = key[1:] - if not (eor ^ matchonekeyword(key, itemkeywords)): - return True - -def getkeywords(node): - keywords = {} - while node is not None: - keywords.update(node.keywords) - node = node.parent - return keywords - - -def matchonekeyword(key, itemkeywords): - for elem in key.split("."): - for kw in itemkeywords: - if elem in kw: - break - else: - return False - return True - -class MarkGenerator: - """ Factory for :class:`MarkDecorator` objects - exposed as - a ``py.test.mark`` singleton instance. Example:: - - import py - @py.test.mark.slowtest - def test_function(): - pass - - will set a 'slowtest' :class:`MarkInfo` object - on the ``test_function`` object. """ - - def __getattr__(self, name): - if name[0] == "_": - raise AttributeError(name) - return MarkDecorator(name) - -class MarkDecorator: - """ A decorator for test functions and test classes. When applied - it will create :class:`MarkInfo` objects which may be - :ref:`retrieved by hooks as item keywords` MarkDecorator instances - are usually created by writing:: - - mark1 = py.test.mark.NAME # simple MarkDecorator - mark2 = py.test.mark.NAME(name1=value) # parametrized MarkDecorator - - and can then be applied as decorators to test functions:: - - @mark2 - def test_function(): - pass - """ - def __init__(self, name): - self.markname = name - self.kwargs = {} - self.args = [] - - def __repr__(self): - d = self.__dict__.copy() - name = d.pop('markname') - return "" %(name, d) - - def __call__(self, *args, **kwargs): - """ if passed a single callable argument: decorate it with mark info. - otherwise add *args/**kwargs in-place to mark information. """ - if args: - func = args[0] - if len(args) == 1 and hasattr(func, '__call__') or \ - hasattr(func, '__bases__'): - if hasattr(func, '__bases__'): - if hasattr(func, 'pytestmark'): - l = func.pytestmark - if not isinstance(l, list): - func.pytestmark = [l, self] - else: - l.append(self) - else: - func.pytestmark = [self] - else: - holder = getattr(func, self.markname, None) - if holder is None: - holder = MarkInfo(self.markname, self.args, self.kwargs) - setattr(func, self.markname, holder) - else: - holder.kwargs.update(self.kwargs) - holder.args.extend(self.args) - return func - else: - self.args.extend(args) - self.kwargs.update(kwargs) - return self - -class MarkInfo: - """ Marking object created by :class:`MarkDecorator` instances. """ - def __init__(self, name, args, kwargs): - #: name of attribute - self.name = name - #: positional argument list, empty if none specified - self.args = args - #: keyword argument dictionary, empty if nothing specified - self.kwargs = kwargs - - def __repr__(self): - return "" % ( - self._name, self.args, self.kwargs) - -def pytest_itemcollected(item): - if not isinstance(item, pytest.Function): - return - try: - func = item.obj.__func__ - except AttributeError: - func = getattr(item.obj, 'im_func', item.obj) - pyclasses = (pytest.Class, pytest.Module) - for node in item.listchain(): - if isinstance(node, pyclasses): - marker = getattr(node.obj, 'pytestmark', None) - if marker is not None: - if isinstance(marker, list): - for mark in marker: - mark(func) - else: - marker(func) - node = node.parent - item.keywords.update(py.builtin._getfuncdict(func)) --- a/pytest/plugin/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# --- /dev/null +++ b/_pytest/config.py @@ -0,0 +1,453 @@ +""" command line configuration, ini-file and conftest.py processing. """ + +import py +import sys, os +from _pytest.core import PluginManager +import pytest + +def pytest_cmdline_parse(pluginmanager, args): + config = Config(pluginmanager) + config.parse(args) + if config.option.debug: + config.trace.root.setwriter(sys.stderr.write) + return config + +class Parser: + """ Parser for command line arguments. """ + + def __init__(self, usage=None, processopt=None): + self._anonymous = OptionGroup("custom options", parser=self) + self._groups = [] + self._processopt = processopt + self._usage = usage + self._inidict = {} + self._ininames = [] + self.hints = [] + + def processoption(self, option): + if self._processopt: + if option.dest: + self._processopt(option) + + def addnote(self, note): + self._notes.append(note) + + def getgroup(self, name, description="", after=None): + """ get (or create) a named option Group. + + :name: unique name of the option group. + :description: long description for --help output. + :after: name of other group, used for ordering --help output. + """ + for group in self._groups: + if group.name == name: + return group + group = OptionGroup(name, description, parser=self) + i = 0 + for i, grp in enumerate(self._groups): + if grp.name == after: + break + self._groups.insert(i+1, group) + return group + + def addoption(self, *opts, **attrs): + """ add an optparse-style option. """ + self._anonymous.addoption(*opts, **attrs) + + def parse(self, args): + self.optparser = optparser = MyOptionParser(self) + groups = self._groups + [self._anonymous] + for group in groups: + if group.options: + desc = group.description or group.name + optgroup = py.std.optparse.OptionGroup(optparser, desc) + optgroup.add_options(group.options) + optparser.add_option_group(optgroup) + return self.optparser.parse_args([str(x) for x in args]) + + def parse_setoption(self, args, option): + parsedoption, args = self.parse(args) + for name, value in parsedoption.__dict__.items(): + setattr(option, name, value) + return args + + def addini(self, name, help, type=None, default=None): + """ add an ini-file option with the given name and description. """ + assert type in (None, "pathlist", "args", "linelist") + self._inidict[name] = (help, type, default) + self._ininames.append(name) + +class OptionGroup: + def __init__(self, name, description="", parser=None): + self.name = name + self.description = description + self.options = [] + self.parser = parser + + def addoption(self, *optnames, **attrs): + """ add an option to this group. """ + option = py.std.optparse.Option(*optnames, **attrs) + self._addoption_instance(option, shortupper=False) + + def _addoption(self, *optnames, **attrs): + option = py.std.optparse.Option(*optnames, **attrs) + self._addoption_instance(option, shortupper=True) + + def _addoption_instance(self, option, shortupper=False): + if not shortupper: + for opt in option._short_opts: + if opt[0] == '-' and opt[1].islower(): + raise ValueError("lowercase shortoptions reserved") + if self.parser: + self.parser.processoption(option) + self.options.append(option) + + +class MyOptionParser(py.std.optparse.OptionParser): + def __init__(self, parser): + self._parser = parser + py.std.optparse.OptionParser.__init__(self, usage=parser._usage, + add_help_option=False) + def format_epilog(self, formatter): + hints = self._parser.hints + if hints: + s = "\n".join(["hint: " + x for x in hints]) + "\n" + s = "\n" + s + "\n" + return s + return "" + +class Conftest(object): + """ the single place for accessing values and interacting + towards conftest modules from py.test objects. + """ + def __init__(self, onimport=None, confcutdir=None): + self._path2confmods = {} + self._onimport = onimport + self._conftestpath2mod = {} + self._confcutdir = confcutdir + self._md5cache = {} + + def setinitial(self, args): + """ try to find a first anchor path for looking up global values + from conftests. This function is usually called _before_ + argument parsing. conftest files may add command line options + and we thus have no completely safe way of determining + which parts of the arguments are actually related to options + and which are file system paths. We just try here to get + bootstrapped ... + """ + current = py.path.local() + opt = '--confcutdir' + for i in range(len(args)): + opt1 = str(args[i]) + if opt1.startswith(opt): + if opt1 == opt: + if len(args) > i: + p = current.join(args[i+1], abs=True) + elif opt1.startswith(opt + "="): + p = current.join(opt1[len(opt)+1:], abs=1) + self._confcutdir = p + break + for arg in args + [current]: + if hasattr(arg, 'startswith') and arg.startswith("--"): + continue + anchor = current.join(arg, abs=1) + if anchor.check(): # we found some file object + self._path2confmods[None] = self.getconftestmodules(anchor) + # let's also consider test* dirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): + self.getconftestmodules(x) + break + else: + assert 0, "no root of filesystem?" + + def getconftestmodules(self, path): + """ return a list of imported conftest modules for the given path. """ + try: + clist = self._path2confmods[path] + except KeyError: + if path is None: + raise ValueError("missing default confest.") + dp = path.dirpath() + clist = [] + if dp != path: + cutdir = self._confcutdir + if cutdir and path != cutdir and not path.relto(cutdir): + pass + else: + conftestpath = path.join("conftest.py") + if conftestpath.check(file=1): + key = conftestpath.computehash() + # XXX logging about conftest loading + if key not in self._md5cache: + clist.append(self.importconftest(conftestpath)) + self._md5cache[key] = conftestpath + else: + # use some kind of logging + print ("WARN: not loading %s" % conftestpath) + clist[:0] = self.getconftestmodules(dp) + self._path2confmods[path] = clist + # be defensive: avoid changes from caller side to + # affect us by always returning a copy of the actual list + return clist[:] + + def rget(self, name, path=None): + mod, value = self.rget_with_confmod(name, path) + return value + + def rget_with_confmod(self, name, path=None): + modules = self.getconftestmodules(path) + modules.reverse() + for mod in modules: + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def importconftest(self, conftestpath): + assert conftestpath.check(), conftestpath + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport() + 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._postimport(mod) + return mod + + def _postimport(self, mod): + if self._onimport: + self._onimport(mod) + return mod + +def _ensure_removed_sysmodule(modname): + try: + del sys.modules[modname] + except KeyError: + pass + +class CmdOptions(object): + """ holds cmdline options as attributes.""" + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + def __repr__(self): + return "" %(self.__dict__,) + +class Config(object): + """ access to configuration values, pluginmanager and plugin hooks. """ + basetemp = None + + def __init__(self, pluginmanager=None): + #: command line option values, usually added via parser.addoption(...) + #: or parser.getgroup(...).addoption(...) calls + self.option = CmdOptions() + self._parser = Parser( + usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", + processopt=self._processopt, + ) + #: a pluginmanager instance + self.pluginmanager = pluginmanager or PluginManager(load=True) + self.trace = self.pluginmanager.trace.root.get("config") + self._conftest = Co