diff --git a/_pytest/config.py b/_pytest/config.py index 095bf1595..df93eabb8 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -98,6 +98,12 @@ class PytestPluginManager(PluginManager): self._warnings = [] self._plugin_distinfo = [] self._globalplugins = [] + + # state related to local conftest plugins + self._path2confmods = {} + self._conftestpath2mod = {} + self._confcutdir = None + self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): @@ -140,6 +146,89 @@ class PytestPluginManager(PluginManager): for warning in self._warnings: config.warn(code="I1", message=warning) + # + # internal API for local conftest plugin handling + # + def _set_initial_conftests(self, namespace): + """ load initial conftest files given a preparsed "namespace". + As conftest files may add their own command line options + which have arguments ('--my-opt somepath') we might get some + false positives. All builtin and 3rd party plugins will have + been loaded, however, so common options will not confuse our logic + here. + """ + current = py.path.local() + self._confcutdir = current.join(namespace.confcutdir, abs=True) \ + if namespace.confcutdir else None + testpaths = namespace.file_or_dir + foundanchor = False + for path in testpaths: + path = str(path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = current.join(path, abs=1) + if exists(anchor): # we found some file object + self._try_load_conftest(anchor) + foundanchor = True + if not foundanchor: + self._try_load_conftest(current) + + def _try_load_conftest(self, anchor): + self._getconftestmodules(anchor) + # let's also consider test* subdirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): + self._getconftestmodules(x) + + def _getconftestmodules(self, path): + try: + return self._path2confmods[path] + except KeyError: + clist = [] + for parent in path.parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.check(file=1): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._path2confmods[path] = clist + return clist + + def _rget_with_confmod(self, name, path): + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest(self, conftestpath): + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + try: + mod = conftestpath.pyimport() + except Exception: + raise ConftestImportFailure(conftestpath, sys.exc_info()) + self._conftestpath2mod[conftestpath] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._path2confmods: + for path, mods in self._path2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace("loaded conftestmodule %r" %(mod)) + self.consider_conftest(mod) + return mod + # # API for bootstrapping plugin loading # @@ -572,96 +661,6 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): return action._formatted_action_invocation -class Conftest(object): - """ the single place for accessing values and interacting - towards conftest modules from pytest objects. - """ - def __init__(self, onimport=None): - self._path2confmods = {} - self._onimport = onimport - self._conftestpath2mod = {} - self._confcutdir = None - - def setinitial(self, namespace): - """ load initial conftest files given a preparsed "namespace". - As conftest files may add their own command line options - which have arguments ('--my-opt somepath') we might get some - false positives. All builtin and 3rd party plugins will have - been loaded, however, so common options will not confuse our logic - here. - """ - current = py.path.local() - self._confcutdir = current.join(namespace.confcutdir, abs=True) \ - if namespace.confcutdir else None - testpaths = namespace.file_or_dir - foundanchor = False - for path in testpaths: - path = str(path) - # remove node-id syntax - i = path.find("::") - if i != -1: - path = path[:i] - anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object - self._try_load_conftest(anchor) - foundanchor = True - if not foundanchor: - self._try_load_conftest(current) - - def _try_load_conftest(self, anchor): - self.getconftestmodules(anchor) - # let's also consider test* subdirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self.getconftestmodules(x) - - def getconftestmodules(self, path): - try: - return self._path2confmods[path] - except KeyError: - clist = [] - for parent in path.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.check(file=1): - mod = self.importconftest(conftestpath) - clist.append(mod) - self._path2confmods[path] = clist - return clist - - def rget_with_confmod(self, name, path): - modules = self.getconftestmodules(path) - for mod in reversed(modules): - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def importconftest(self, conftestpath): - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - try: - mod = conftestpath.pyimport() - except Exception: - raise ConftestImportFailure(conftestpath, sys.exc_info()) - self._conftestpath2mod[conftestpath] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._path2confmods: - for path, mods in self._path2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - if self._onimport: - self._onimport(mod) - return mod - def _ensure_removed_sysmodule(modname): try: @@ -697,7 +696,6 @@ class Config(object): #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") - self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook self._inicache = {} self._opt2dest = {} @@ -783,10 +781,6 @@ class Config(object): config.pluginmanager.consider_pluginarg(x) return config - def _onimportconftest(self, conftestmodule): - self.trace("loaded conftestmodule %r" %(conftestmodule,)) - self.pluginmanager.consider_conftest(conftestmodule) - def _processopt(self, opt): for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest @@ -797,10 +791,10 @@ class Config(object): def _getmatchingplugins(self, fspath): return self.pluginmanager._globalplugins + \ - self._conftest.getconftestmodules(fspath) + self.pluginmanager._getconftestmodules(fspath) def pytest_load_initial_conftests(self, early_config): - self._conftest.setinitial(early_config.known_args_namespace) + self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) pytest_load_initial_conftests.trylast = True def _initini(self, args): @@ -907,7 +901,7 @@ class Config(object): def _getconftest_pathlist(self, name, path): try: - mod, relroots = self._conftest.rget_with_confmod(name, path) + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 74dab333b..7c8190139 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -132,7 +132,7 @@ class DoctestModule(pytest.File): def collect(self): import doctest if self.fspath.basename == "conftest.py": - module = self.config._conftest.importconftest(self.fspath) + module = self.config._conftest._importconftest(self.fspath) else: try: module = self.fspath.pyimport() diff --git a/testing/python/fixture.py b/testing/python/fixture.py index ef43744d5..274f80965 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1487,7 +1487,7 @@ class TestAutouseManagement: reprec = testdir.inline_run("-v","-s") reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config - l = config._conftest.getconftestmodules(p)[0].l + l = config.pluginmanager._getconftestmodules(p)[0].l assert l == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 def test_scope_ordering(self, testdir): diff --git a/testing/test_conftest.py b/testing/test_conftest.py index bff68e6d2..cc2c63ae0 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,7 +1,6 @@ from textwrap import dedent import py, pytest -from _pytest.config import Conftest - +from _pytest.config import PytestPluginManager @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -16,7 +15,7 @@ def basedir(request): return tmpdir def ConftestWithSetinitial(path): - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) return conftest @@ -25,51 +24,41 @@ def conftest_setinitial(conftest, args, confcutdir=None): def __init__(self): self.file_or_dir = args self.confcutdir = str(confcutdir) - conftest.setinitial(Namespace()) + conftest._set_initial_conftests(Namespace()) class TestConftestValueAccessGlobal: def test_basic_init(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() p = basedir.join("adir") - assert conftest.rget_with_confmod("a", p)[1] == 1 - - def test_onimport(self, basedir): - l = [] - conftest = Conftest(onimport=l.append) - adir = basedir.join("adir") - conftest_setinitial(conftest, [adir], confcutdir=basedir) - assert len(l) == 1 - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("b", adir.join("b"))[1] == 2 - assert len(l) == 2 + assert conftest._rget_with_confmod("a", p)[1] == 1 def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): - conftest = Conftest() + conftest = PytestPluginManager() len(conftest._path2confmods) - conftest.getconftestmodules(basedir) + conftest._getconftestmodules(basedir) snap1 = len(conftest._path2confmods) #assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('adir')) + conftest._getconftestmodules(basedir.join('adir')) assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(basedir.join('b')) + conftest._getconftestmodules(basedir.join('b')) assert len(conftest._path2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): - conftest.rget_with_confmod('a', basedir) + conftest._rget_with_confmod('a', basedir) def test_value_access_by_path(self, basedir): conftest = ConftestWithSetinitial(basedir) adir = basedir.join("adir") - assert conftest.rget_with_confmod("a", adir)[1] == 1 - assert conftest.rget_with_confmod("a", adir.join("b"))[1] == 1.5 + assert conftest._rget_with_confmod("a", adir)[1] == 1 + assert conftest._rget_with_confmod("a", adir.join("b"))[1] == 1.5 def test_value_access_with_confmod(self, basedir): startdir = basedir.join("adir", "b") startdir.ensure("xx", dir=True) conftest = ConftestWithSetinitial(startdir) - mod, value = conftest.rget_with_confmod("a", startdir) + mod, value = conftest._rget_with_confmod("a", startdir) assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") @@ -85,9 +74,9 @@ def test_conftest_in_nonpkg_with_init(tmpdir): def test_doubledash_considered(testdir): conf = testdir.mkdir("--option") conf.join("conftest.py").ensure() - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.basename, conf.basename]) - l = conftest.getconftestmodules(conf) + l = conftest._getconftestmodules(conf) assert len(l) == 1 def test_issue151_load_all_conftests(testdir): @@ -96,7 +85,7 @@ def test_issue151_load_all_conftests(testdir): p = testdir.mkdir(name) p.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, names) d = list(conftest._conftestpath2mod.values()) assert len(d) == len(names) @@ -105,15 +94,15 @@ def test_conftest_global_import(testdir): testdir.makeconftest("x=3") p = testdir.makepyfile(""" import py, pytest - from _pytest.config import Conftest - conf = Conftest() - mod = conf.importconftest(py.path.local("conftest.py")) + from _pytest.config import PytestPluginManager + conf = PytestPluginManager() + mod = conf._importconftest(py.path.local("conftest.py")) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) subconf = py.path.local().ensure("sub", "conftest.py") subconf.write("y=4") - mod2 = conf.importconftest(subconf) + mod2 = conf._importconftest(subconf) assert mod != mod2 assert mod2.y == 4 import conftest @@ -125,27 +114,27 @@ def test_conftest_global_import(testdir): def test_conftestcutdir(testdir): conf = testdir.makeconftest("") p = testdir.mkdir("x") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p) - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 0 - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 0 assert conf not in conftest._conftestpath2mod # but we can still import a conftest directly - conftest.importconftest(conf) - l = conftest.getconftestmodules(conf.dirpath()) + conftest._importconftest(conf) + l = conftest._getconftestmodules(conf.dirpath()) assert l[0].__file__.startswith(str(conf)) # and all sub paths get updated properly - l = conftest.getconftestmodules(p) + l = conftest._getconftestmodules(p) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) def test_conftestcutdir_inplace_considered(testdir): conf = testdir.makeconftest("") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) - l = conftest.getconftestmodules(conf.dirpath()) + l = conftest._getconftestmodules(conf.dirpath()) assert len(l) == 1 assert l[0].__file__.startswith(str(conf)) @@ -153,7 +142,7 @@ def test_conftestcutdir_inplace_considered(testdir): def test_setinitial_conftest_subdirs(testdir, name): sub = testdir.mkdir(name) subconftest = sub.ensure("conftest.py") - conftest = Conftest() + conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ('whatever', '.dotdir'): assert subconftest in conftest._conftestpath2mod @@ -199,9 +188,9 @@ def test_conftest_import_order(testdir, monkeypatch): ct2.write("") def impct(p): return p - conftest = Conftest() - monkeypatch.setattr(conftest, 'importconftest', impct) - assert conftest.getconftestmodules(sub) == [ct1, ct2] + conftest = PytestPluginManager() + monkeypatch.setattr(conftest, '_importconftest', impct) + assert conftest._getconftestmodules(sub) == [ct1, ct2] def test_fixture_dependency(testdir, monkeypatch): diff --git a/testing/test_core.py b/testing/test_core.py index 6323db435..14631a48c 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -94,7 +94,7 @@ class TestPytestPluginInteractions: return xyz + 1 """) config = get_plugin_manager().config - config._conftest.importconftest(conf) + config.pluginmanager._importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] @@ -143,7 +143,7 @@ class TestPytestPluginInteractions: parser.addoption('--test123', action="store_true", default=True) """) - config._conftest.importconftest(p) + config.pluginmanager._importconftest(p) assert config.option.test123 def test_configure(self, testdir): @@ -849,10 +849,6 @@ class TestPytestPluginManager: mod = pytestpm.getplugin("pkg.plug") assert mod.x == 3 - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - def test_consider_conftest_deps(self, testdir, pytestpm): mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() with pytest.raises(ImportError):