From c7531705fcf2a1faa9dff9b8281eedf419889fb7 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 6 Dec 2010 16:54:42 +0100 Subject: [PATCH] refine plugin registration, allow new "-p no:NAME" way to prevent/undo plugin registration --- CHANGELOG | 6 ++- _pytest/config.py | 2 +- _pytest/core.py | 84 ++++++++++++++------------------- _pytest/helpconfig.py | 6 ++- _pytest/{session.py => main.py} | 4 +- _pytest/pytester.py | 2 +- _pytest/python.py | 2 +- _pytest/resultlog.py | 12 ++--- doc/links.inc | 1 - doc/plugins.txt | 66 ++++++++++++++++++++++---- testing/acceptance_test.py | 2 +- testing/test_collection.py | 2 +- testing/test_config.py | 15 +++++- testing/test_core.py | 57 +++++++++++++--------- testing/test_resultlog.py | 4 +- testing/test_session.py | 4 +- tox.ini | 2 +- 17 files changed, 169 insertions(+), 102 deletions(-) rename _pytest/{session.py => main.py} (99%) diff --git a/CHANGELOG b/CHANGELOG index 611ca649c..297b7f427 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ Changes between 2.0.0 and 2.0.1.dev1 ---------------------------------------------- -- refinements to terminal output +- refinements to "collecting" output on non-ttys +- refine internal plugin registration and --traceconfig output +- introduce a mechanism to prevent/unregister plugins from the + command line, see http://pytest.org/plugins.html#cmdunregister +- activate resultlog plugin by default Changes between 1.3.4 and 2.0.0 ---------------------------------------------- diff --git a/_pytest/config.py b/_pytest/config.py index fef1b5367..93fdeb393 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -300,9 +300,9 @@ class Config(object): if addopts: args[:] = self.getini("addopts") + args self._checkversion() + self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() - self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) self.pluginmanager.do_addoption(self._parser) diff --git a/_pytest/core.py b/_pytest/core.py index a13fd27dd..fcd7ea547 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -11,11 +11,9 @@ assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) default_plugins = ( - "config mark session terminal runner python pdb unittest capture skipping " + "config mark main terminal runner python pdb unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml doctest").split() - -IMPORTPREFIX = "pytest_" + "junitxml resultlog doctest").split() class TagTracer: def __init__(self, prefix="[pytest] "): @@ -79,20 +77,12 @@ class PluginManager(object): 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) + name = name or getattr(plugin, '__name__', str(id(plugin))) if name in self._name2plugin: return False + #self.trace("registering", name, plugin) self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) @@ -112,7 +102,7 @@ class PluginManager(object): del self._name2plugin[name] def isregistered(self, plugin, name=None): - if self._getpluginname(plugin, name) in self._name2plugin: + if self.getplugin(name) is not None: return True for val in self._name2plugin.values(): if plugin == val: @@ -136,11 +126,12 @@ class PluginManager(object): return False def getplugin(self, name): + if name is None: + return None try: return self._name2plugin[name] except KeyError: - impname = canonical_importname(name) - return self._name2plugin[impname] + return self._name2plugin.get("_pytest." + name, None) # API for bootstrapping # @@ -160,19 +151,28 @@ class PluginManager(object): except ImportError: return # XXX issue a warning for ep in iter_entry_points('pytest11'): - name = canonical_importname(ep.name) - if name in self._name2plugin: + if ep.name in self._name2plugin: continue try: plugin = ep.load() except DistributionNotFound: continue + name = ep.name + if name.startswith("pytest_"): + name = name[7:] 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) + if opt2.startswith("no:"): + name = opt2[3:] + if self.getplugin(name) is not None: + self.unregister(None, name=name) + self._name2plugin[name] = -1 + else: + if self.getplugin(opt2) is None: + self.import_plugin(opt2) def consider_conftest(self, conftestmodule): if self.register(conftestmodule, name=conftestmodule.__file__): @@ -186,15 +186,19 @@ class PluginManager(object): 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: + def import_plugin(self, modname): + assert isinstance(modname, str) + if self.getplugin(modname) is not None: return try: + #self.trace("importing", modname) mod = importplugin(modname) except KeyboardInterrupt: raise + except ImportError: + if modname.startswith("pytest_"): + return self.import_plugin(modname[7:]) + raise except: e = py.std.sys.exc_info()[1] if not hasattr(py.test, 'skip'): @@ -290,34 +294,18 @@ class PluginManager(object): 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 + name = importspec try: - return __import__(importspec, None, None, '__doc__') + mod = "_pytest." + name + return __import__(mod, 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__') - + #e = py.std.sys.exc_info()[1] + #if str(e).find(name) == -1: + # raise + pass # + return __import__(importspec, None, None, '__doc__') class MultiCall: """ execute a call into multiple python functions/methods. """ diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 548c6f411..6d0958420 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -80,7 +80,11 @@ def pytest_report_header(config): plugins = [] items = config.pluginmanager._name2plugin.items() for name, plugin in items: - lines.append(" %-20s: %s" %(name, repr(plugin))) + if hasattr(plugin, '__file__'): + r = plugin.__file__ + else: + r = repr(plugin) + lines.append(" %-20s: %s" %(name, r)) return lines diff --git a/_pytest/session.py b/_pytest/main.py similarity index 99% rename from _pytest/session.py rename to _pytest/main.py index 534a199f2..b07662118 100644 --- a/_pytest/session.py +++ b/_pytest/main.py @@ -40,7 +40,7 @@ def pytest_addoption(parser): help="only load conftest.py's relative to specified dir.") group = parser.getgroup("debugconfig", - "test process debugging and configuration") + "test session debugging and configuration") group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", help="base temporary directory for this test run.") @@ -336,7 +336,7 @@ class Session(FSCollector): 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) + assert self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") diff --git a/_pytest/pytester.py b/_pytest/pytester.py index fa52001b2..f9da97b18 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -6,7 +6,7 @@ import re import inspect import time from fnmatch import fnmatch -from _pytest.session import Session +from _pytest.main import Session from py.builtin import print_ from _pytest.core import HookRelay diff --git a/_pytest/python.py b/_pytest/python.py index 8e85934b3..64e48b763 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -730,7 +730,7 @@ class FuncargRequest: raise self.LookupError(msg) def showfuncargs(config): - from _pytest.session import Session + from _pytest.main import Session session = Session(config) session.perform_collect() if session.items: diff --git a/_pytest/resultlog.py b/_pytest/resultlog.py index fd9e324ae..7f879cce5 100644 --- a/_pytest/resultlog.py +++ b/_pytest/resultlog.py @@ -1,12 +1,12 @@ """ (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.") + group = parser.getgroup("terminal reporting", "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 @@ -52,9 +52,9 @@ class ResultLog(object): self.logfile = logfile # preferably line buffered def write_log_entry(self, testpath, lettercode, longrepr): - print_("%s %s" % (lettercode, testpath), file=self.logfile) + py.builtin.print_("%s %s" % (lettercode, testpath), file=self.logfile) for line in longrepr.splitlines(): - print_(" %s" % line, file=self.logfile) + py.builtin.print_(" %s" % line, file=self.logfile) def log_outcome(self, report, lettercode, longrepr): testpath = getattr(report, 'nodeid', None) diff --git a/doc/links.inc b/doc/links.inc index d519cbc7a..34a4114ab 100644 --- a/doc/links.inc +++ b/doc/links.inc @@ -10,7 +10,6 @@ .. _pytest: http://pypi.python.org/pypi/pytest .. _mercurial: http://mercurial.selenic.com/wiki/ .. _`setuptools`: http://pypi.python.org/pypi/setuptools - .. _`easy_install`: .. _`distribute docs`: .. _`distribute`: http://pypi.python.org/pypi/distribute diff --git a/doc/plugins.txt b/doc/plugins.txt index deeebd4f8..80a3d9e65 100644 --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -1,4 +1,4 @@ -Writing, managing and understanding plugins +Working with plugins and conftest files ============================================= .. _`local plugin`: @@ -11,6 +11,7 @@ py.test implements all aspects of configuration, collection, running and reporti .. _`pytest/plugin`: http://bitbucket.org/hpk42/pytest/src/tip/pytest/plugin/ .. _`conftest.py plugins`: +.. _`conftest.py`: conftest.py: local per-directory plugins -------------------------------------------------------------- @@ -53,6 +54,7 @@ earlier than further away ones. conftest.py file. .. _`external plugins`: +.. _`extplugins`: Installing External Plugins / Searching ------------------------------------------------------ @@ -64,9 +66,26 @@ tool, for example:: 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 available plugins through a -`pytest- pypi.python.org search`_. +there is no need to activate it. Here is a list of known plugins: + +* `pytest-capturelog `_: + to capture and assert about messages from the logging module + +* `pytest-xdist `_: + to distribute tests to CPUs and remote hosts, looponfailing mode, + see also :ref:`xdist` + +* `pytest-cov `_: + coverage reporting, compatible with distributed testing + +* `pytest-pep8 `_: + a ``--pep8`` option to enable PEP8 compliancy checking. + +* `oejskit `_: + a plugin to run javascript unittests in life browsers + (**version 0.8.9 not compatible with pytest-2.0**) + +You may discover more 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 @@ -170,12 +189,42 @@ the plugin manager like this: If you want to look at the names of existing plugins, use the ``--traceconfig`` option. +.. _`findpluginname`: + +Finding out which plugins are active +---------------------------------------------------------------------------- + +If you want to find out which plugins are active in your +environment you can type:: + + py.test --traceconfig + +and will get an extended test header which shows activated plugins +and their names. It will also print local plugins aka +:ref:`conftest.py ` files when they are loaded. + +.. _`cmdunregister`: + +deactivate / unregister a plugin by name +---------------------------------------------------------------------------- + +You can prevent plugins from loading or unregister them:: + + py.test -p no:NAME + +This means that any subsequent try to activate/load the named +plugin will it already existing. See :ref:`findpluginname` for +how to obtain the name of a plugin. .. _`builtin plugins`: py.test default plugin reference ==================================== + +You can find the source code for the following plugins +in the `pytest repository `_. + .. autosummary:: _pytest.assertion @@ -195,7 +244,7 @@ py.test default plugin reference _pytest.recwarn _pytest.resultlog _pytest.runner - _pytest.session + _pytest.main _pytest.skipping _pytest.terminal _pytest.tmpdir @@ -288,14 +337,14 @@ Reference of important objects involved in hooks .. autoclass:: _pytest.config.Parser :members: -.. autoclass:: _pytest.session.Node(name, parent) +.. autoclass:: _pytest.main.Node(name, parent) :members: .. - .. autoclass:: _pytest.session.File(fspath, parent) + .. autoclass:: _pytest.main.File(fspath, parent) :members: - .. autoclass:: _pytest.session.Item(name, parent) + .. autoclass:: _pytest.main.Item(name, parent) :members: .. autoclass:: _pytest.python.Module(name, parent) @@ -313,4 +362,3 @@ Reference of important objects involved in hooks .. autoclass:: _pytest.runner.TestReport :members: - diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 42c133f98..d05d9e189 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -27,7 +27,7 @@ class TestGeneralUsage: def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) - result = testdir.runpytest("-p", "xyz", "--xyz=123") + result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123") assert result.ret == 0 result.stdout.fnmatch_lines([ '*1 passed*', diff --git a/testing/test_collection.py b/testing/test_collection.py index b5b15d466..71c58e128 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import pytest, py -from _pytest.session import Session +from _pytest.main import Session class TestCollector: def test_collect_versus_item(self): diff --git a/testing/test_config.py b/testing/test_config.py index a15e6001a..954909a81 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -219,7 +219,7 @@ def test_options_on_small_file_do_not_blow_up(testdir): ['--traceconfig'], ['-v'], ['-v', '-v']): runfiletest(opts + [path]) -def test_preparse_ordering(testdir, monkeypatch): +def test_preparse_ordering_with_setuptools(testdir, monkeypatch): pkg_resources = py.test.importorskip("pkg_resources") def my_iter(name): assert name == "pytest11" @@ -239,3 +239,16 @@ def test_preparse_ordering(testdir, monkeypatch): plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 +def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + def load(self): + assert 0, "should not arrive here" + return iter([EntryPoint()]) + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + config = testdir.parseconfig("-p", "no:mytestplugin") + plugin = config.pluginmanager.getplugin("mytestplugin") + assert plugin == -1 diff --git a/testing/test_core.py b/testing/test_core.py index 55a6d2dc0..3bcf681af 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,5 +1,5 @@ import pytest, py, os -from _pytest.core import PluginManager, canonical_importname +from _pytest.core import PluginManager from _pytest.core import MultiCall, HookRelay, varnames @@ -15,30 +15,47 @@ class TestBootstrapping: pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) """) + def test_plugin_prevent_register(self): + pluginmanager = PluginManager() + pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) + l1 = pluginmanager.getplugins() + pluginmanager.register(42, name="abc") + l2 = pluginmanager.getplugins() + assert len(l2) == len(l1) + + def test_plugin_prevent_register_unregistered_alredy_registered(self): + pluginmanager = PluginManager() + pluginmanager.register(42, name="abc") + l1 = pluginmanager.getplugins() + assert 42 in l1 + pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) + l2 = pluginmanager.getplugins() + assert 42 not in l2 + def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(pytest_skipping1=""" + p = testdir.makepyfile(skipping1=""" import pytest pytest.skip("hello") """) - p.copy(p.dirpath("pytest_skipping2.py")) + p.copy(p.dirpath("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*", + "*hint*skipping2*hello*", ]) def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): pluginmanager = PluginManager() testdir.syspathinsert() - testdir.makepyfile(pytest_xy123="#") + testdir.makepyfile(xy123="#") monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') l1 = len(pluginmanager.getplugins()) pluginmanager.consider_env() l2 = len(pluginmanager.getplugins()) assert l2 == l1 + 1 - assert pluginmanager.getplugin('pytest_xy123') + assert pluginmanager.getplugin('xy123') pluginmanager.consider_env() l3 = len(pluginmanager.getplugins()) assert l2 == l3 @@ -48,7 +65,7 @@ class TestBootstrapping: def my_iter(name): assert name == "pytest11" class EntryPoint: - name = "mytestplugin" + name = "pytest_mytestplugin" def load(self): class PseudoPlugin: x = 42 @@ -60,8 +77,6 @@ class TestBootstrapping: 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', @@ -75,7 +90,7 @@ class TestBootstrapping: p = testdir.makepyfile(""" import pytest def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('x500') + plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") @@ -91,14 +106,14 @@ class TestBootstrapping: reset = testdir.syspathinsert() pluginname = "pytest_hello" testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("hello") + pluginmanager.import_plugin("pytest_hello") len1 = len(pluginmanager.getplugins()) pluginmanager.import_plugin("pytest_hello") len2 = len(pluginmanager.getplugins()) assert len1 == len2 plugin1 = pluginmanager.getplugin("pytest_hello") assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("hello") + plugin2 = pluginmanager.getplugin("pytest_hello") assert plugin2 is plugin1 def test_import_plugin_dotted_name(self, testdir): @@ -116,13 +131,13 @@ class TestBootstrapping: def test_consider_module(self, testdir): pluginmanager = PluginManager() testdir.syspathinsert() - testdir.makepyfile(pytest_plug1="#") - testdir.makepyfile(pytest_plug2="#") + testdir.makepyfile(pytest_p1="#") + testdir.makepyfile(pytest_p2="#") mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + mod.pytest_plugins = ["pytest_p1", "pytest_p2"] pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" - assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" + assert pluginmanager.getplugin("pytest_p1").__name__ == "pytest_p1" + assert pluginmanager.getplugin("pytest_p2").__name__ == "pytest_p2" def test_consider_module_import_module(self, testdir): mod = py.std.types.ModuleType("x") @@ -198,8 +213,7 @@ class TestBootstrapping: 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 + pp.import_plugin('pytest_xyz') assert pp.getplugin('pytest_xyz') == mod assert pp.isregistered(mod) @@ -217,9 +231,6 @@ class TestBootstrapping: pass excinfo = pytest.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() @@ -400,7 +411,7 @@ class TestPytestPluginInteractions: 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 + config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 973e1277b..476a76b54 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -2,10 +2,10 @@ import py, pytest import os from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure -from _pytest.session import Node, Item, FSCollector +from _pytest.main import Node, Item, FSCollector def test_generic_path(testdir): - from _pytest.session import Session + from _pytest.main import Session config = testdir.parseconfig() session = Session(config) p1 = Node('a', config=config, session=session) diff --git a/testing/test_session.py b/testing/test_session.py index 90c950b7c..f80d3f3a2 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -212,8 +212,8 @@ def test_plugin_specify(testdir): #) def test_plugin_already_exists(testdir): - config = testdir.parseconfig("-p", "session") - assert config.option.plugins == ['session'] + config = testdir.parseconfig("-p", "terminal") + assert config.option.plugins == ['terminal'] config.pluginmanager.do_configure(config) def test_exclude(testdir): diff --git a/tox.ini b/tox.ini index 3854d398b..db04fc62d 100644 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,7 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts= -rxf --pyargs --doctest-modules --ignore=.tox +#addopts= -rxf --pyargs --doctest-modules --ignore=.tox rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py python_classes=Test Acceptance