From f5ea19858c05e7434008f754b2e45c7693216a3c Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 30 Dec 2009 16:18:59 +0100 Subject: [PATCH] deprecate direct definition of Directory, Module, ... in conftest.py's, add some pytest collect related tests + some refinements. --HG-- branch : trunk --- CHANGELOG | 5 + py/impl/test/collect.py | 8 +- py/impl/test/config.py | 12 ++- py/impl/test/pycollect.py | 4 +- py/plugin/hookspec.py | 1 + testing/plugin/test_pytest_resultlog.py | 7 +- testing/pytest/test_collect.py | 45 ++++----- testing/pytest/test_conftesthandle.py | 6 -- testing/pytest/test_deprecated_api.py | 120 +++++++++++++++++++++++- testing/pytest/test_pycollect.py | 70 ++++---------- 10 files changed, 177 insertions(+), 101 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 886922c4f..8471aef39 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,11 +14,16 @@ Changes between 1.X and 1.1.1 - allow pytest_generate_tests to be defined in classes as well - deprecate usage of 'disabled' attribute in favour of pytestmark +- deprecate definition of Directory, Module, Class and Function nodes + in conftest.py files. Use pytest collect hooks instead. - collection/item node specific runtest/collect hooks are only called exactly on matching conftest.py files, i.e. ones which are exactly below the filesystem path of an item +- change: the first pytest_collect_directory hook to return something + will now prevent further hooks to be called. + - robustify capturing to survive if custom pytest_runtest_setup code failed and prevented the capturing setup code from running. diff --git a/py/impl/test/collect.py b/py/impl/test/collect.py index e4a38e4be..9f66caf2c 100644 --- a/py/impl/test/collect.py +++ b/py/impl/test/collect.py @@ -36,10 +36,10 @@ class Node(object): - configuration/options for setup/teardown stdout/stderr capturing and execution of test items """ - def __init__(self, name, parent=None): + def __init__(self, name, parent=None, config=None): self.name = name self.parent = parent - self.config = getattr(parent, 'config', None) + self.config = config or parent.config self.fspath = getattr(parent, 'fspath', None) self.ihook = HookProxy(self) @@ -353,9 +353,9 @@ class Collector(Node): return traceback class FSCollector(Collector): - def __init__(self, fspath, parent=None): + def __init__(self, fspath, parent=None, config=None): fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, parent) + super(FSCollector, self).__init__(fspath.basename, parent, config=config) self.fspath = fspath def __getstate__(self): diff --git a/py/impl/test/config.py b/py/impl/test/config.py index 0e4dea5a2..a434d304b 100644 --- a/py/impl/test/config.py +++ b/py/impl/test/config.py @@ -156,16 +156,20 @@ class Config(object): pkgpath = path.pypkgpath() if pkgpath is None: pkgpath = path.check(file=1) and path.dirpath() or path - Dir = self._getcollectclass("Directory", pkgpath) - col = Dir(pkgpath) - col.config = self + tmpcol = py.test.collect.Directory(pkgpath, config=self) + col = tmpcol.ihook.pytest_collect_directory(path=pkgpath, parent=tmpcol) + col.parent = None return col._getfsnode(path) def _getcollectclass(self, name, path): try: - return self.getvalue(name, path) + cls = self.getvalue(name, path) except KeyError: return getattr(py.test.collect, name) + else: + py.log._apiwarn(">1.1", "%r was found in a conftest.py file, " + "use pytest_collect hooks instead." % (cls,)) + return cls def getconftest_pathlist(self, name, path=None): """ return a matching value, which needs to be sequence diff --git a/py/impl/test/pycollect.py b/py/impl/test/pycollect.py index 784831144..15fb376cc 100644 --- a/py/impl/test/pycollect.py +++ b/py/impl/test/pycollect.py @@ -315,9 +315,9 @@ class Function(FunctionMixin, py.test.collect.Item): and executing a Python callable test object. """ _genid = None - def __init__(self, name, parent=None, args=None, + def __init__(self, name, parent=None, args=None, config=None, callspec=None, callobj=_dummy): - super(Function, self).__init__(name, parent) + super(Function, self).__init__(name, parent, config=config) self._args = args if self._isyieldedfunction(): assert not callspec, "yielded functions (deprecated) cannot have funcargs" diff --git a/py/plugin/hookspec.py b/py/plugin/hookspec.py index bb3352f7d..73a4e4126 100644 --- a/py/plugin/hookspec.py +++ b/py/plugin/hookspec.py @@ -26,6 +26,7 @@ def pytest_unconfigure(config): def pytest_collect_directory(path, parent): """ return Collection node or None for the given path. """ +pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): """ return Collection node or None for the given path. """ diff --git a/testing/plugin/test_pytest_resultlog.py b/testing/plugin/test_pytest_resultlog.py index d721b2e3b..a0681283a 100644 --- a/testing/plugin/test_pytest_resultlog.py +++ b/testing/plugin/test_pytest_resultlog.py @@ -3,8 +3,9 @@ import os from py.plugin.pytest_resultlog import generic_path, ResultLog from py.impl.test.collect import Node, Item, FSCollector -def test_generic_path(): - p1 = Node('a') +def test_generic_path(testdir): + config = testdir.Config() + p1 = Node('a', config=config) assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -13,7 +14,7 @@ def test_generic_path(): res = generic_path(item) assert res == 'a.B().c' - p0 = FSCollector('proj/test') + p0 = FSCollector('proj/test', config=config) p1 = FSCollector('proj/test/a', parent=p0) p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) diff --git a/testing/pytest/test_collect.py b/testing/pytest/test_collect.py index 5ca9df6d3..0c6511c42 100644 --- a/testing/pytest/test_collect.py +++ b/testing/pytest/test_collect.py @@ -193,31 +193,6 @@ class TestPrunetraceback: ]) class TestCustomConftests: - def test_non_python_files(self, testdir): - testdir.makepyfile(conftest=""" - import py - class CustomItem(py.test.collect.Item): - def run(self): - pass - class Directory(py.test.collect.Directory): - def consider_file(self, fspath): - if fspath.ext == ".xxx": - return CustomItem(fspath.basename, parent=self) - """) - checkfile = testdir.makefile(ext="xxx", hello="world") - testdir.makepyfile(x="") - testdir.maketxtfile(x="") - config = testdir.parseconfig() - dircol = config.getfsnode(checkfile.dirpath()) - colitems = dircol.collect() - assert len(colitems) == 1 - assert colitems[0].name == "hello.xxx" - assert colitems[0].__class__.__name__ == "CustomItem" - - item = config.getfsnode(checkfile) - assert item.name == "hello.xxx" - assert item.__class__.__name__ == "CustomItem" - def test_collectignore_exclude_on_option(self, testdir): testdir.makeconftest(""" collect_ignore = ['hello', 'test_world.py'] @@ -237,3 +212,23 @@ class TestCustomConftests: names = [rep.collector.name 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): + 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) + """) + testdir.makepyfile("def test_x(): pass") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyDirectory*", + "*MyModule*", + "*test_x*" + ]) diff --git a/testing/pytest/test_conftesthandle.py b/testing/pytest/test_conftesthandle.py index 3663b1811..58415e9c3 100644 --- a/testing/pytest/test_conftesthandle.py +++ b/testing/pytest/test_conftesthandle.py @@ -48,12 +48,6 @@ class TestConftestValueAccessGlobal: conftest.getconftestmodules(basedir.join('b')) assert len(conftest._path2confmods) == snap1 + 2 - def test_default_Module_setting_is_visible_always(self, basedir, testdir): - basedir.copy(testdir.tmpdir) - config = testdir.Config() - colclass = config._getcollectclass("Module", testdir.tmpdir) - assert colclass == py.test.collect.Module - def test_default_has_lower_prio(self, basedir): conftest = ConftestWithSetinitial(basedir.join("adir")) assert conftest.rget('Directory') == 3 diff --git a/testing/pytest/test_deprecated_api.py b/testing/pytest/test_deprecated_api.py index 556c7dea0..d8869dab7 100644 --- a/testing/pytest/test_deprecated_api.py +++ b/testing/pytest/test_deprecated_api.py @@ -37,15 +37,19 @@ class TestCollectDeprecated: def join(self, name): if name == "somefile.py": return self.Module(self.fspath.join(name), parent=self) - Directory = MyDirectory + + def pytest_collect_directory(path, parent): + return MyDirectory(path, parent) """) - p = testdir.makepyfile(somefile=""" + subconf = testdir.mkpydir("subconf") + somefile = subconf.join("somefile.py") + somefile.write(py.code.Source(""" def check(): pass class Cls: def check2(self): pass - """) - config = testdir.parseconfig() - dirnode = config.getfsnode(p.dirpath()) + """)) + config = testdir.parseconfig(somefile) + dirnode = config.getfsnode(somefile.dirpath()) colitems = dirnode.collect() w = recwarn.pop(DeprecationWarning) assert w.filename.find("conftest.py") != -1 @@ -120,6 +124,7 @@ class TestCollectDeprecated: """) modcol = testdir.getmodulecol("def test_func2(): pass") funcitem = modcol.collect()[0] + w = recwarn.pop(DeprecationWarning) # for defining conftest.Function assert funcitem.name == 'test_func2' funcitem._deprecated_testexecution() w = recwarn.pop(DeprecationWarning) @@ -136,6 +141,8 @@ class TestCollectDeprecated: """) modcol = testdir.getmodulecol("def test_some2(): pass") funcitem = modcol.collect()[0] + w = recwarn.pop(DeprecationWarning) + assert "conftest.py" in str(w.message) recwarn.clear() funcitem._deprecated_testexecution() @@ -169,6 +176,7 @@ class TestCollectDeprecated: col = config.getfsnode(testme) assert col.collect() == [] + class TestDisabled: def test_disabled_module(self, recwarn, testdir): @@ -211,6 +219,31 @@ class TestDisabled: """) reprec.assertoutcome(skipped=2) + @py.test.mark.multi(name="Directory Module Class Function".split()) + def test_function_deprecated_run_execute(self, name, testdir, recwarn): + testdir.makeconftest(""" + import py + class %s(py.test.collect.%s): + pass + """ % (name, name)) + p = testdir.makepyfile(""" + class TestClass: + def test_method(self): + pass + def test_function(): + pass + """) + config = testdir.parseconfig() + if name == "Directory": + config.getfsnode(testdir.tmpdir) + elif name in ("Module", "File"): + config.getfsnode(p) + else: + fnode = config.getfsnode(p) + recwarn.clear() + fnode.collect() + w = recwarn.pop(DeprecationWarning) + assert "conftest.py" in str(w.message) def test_config_cmdline_options(recwarn, testdir): testdir.makepyfile(conftest=""" @@ -267,3 +300,80 @@ def test_dist_conftest_options(testdir): "*1 passed*", ]) +def test_conftest_non_python_items(recwarn, testdir): + testdir.makepyfile(conftest=""" + import py + class CustomItem(py.test.collect.Item): + def run(self): + pass + class Directory(py.test.collect.Directory): + def consider_file(self, fspath): + if fspath.ext == ".xxx": + return CustomItem(fspath.basename, parent=self) + """) + checkfile = testdir.makefile(ext="xxx", hello="world") + testdir.makepyfile(x="") + testdir.maketxtfile(x="") + config = testdir.parseconfig() + recwarn.clear() + dircol = config.getfsnode(checkfile.dirpath()) + w = recwarn.pop(DeprecationWarning) + assert str(w.message).find("conftest.py") != -1 + colitems = dircol.collect() + assert len(colitems) == 1 + assert colitems[0].name == "hello.xxx" + assert colitems[0].__class__.__name__ == "CustomItem" + + item = config.getfsnode(checkfile) + assert item.name == "hello.xxx" + assert item.__class__.__name__ == "CustomItem" + +def test_extra_python_files_and_functions(testdir): + testdir.makepyfile(conftest=""" + import py + class MyFunction(py.test.collect.Function): + pass + class Directory(py.test.collect.Directory): + def consider_file(self, path): + if path.check(fnmatch="check_*.py"): + return self.Module(path, parent=self) + return super(Directory, self).consider_file(path) + class myfuncmixin: + Function = MyFunction + def funcnamefilter(self, name): + return name.startswith('check_') + class Module(myfuncmixin, py.test.collect.Module): + def classnamefilter(self, name): + return name.startswith('CustomTestClass') + class Instance(myfuncmixin, py.test.collect.Instance): + pass + """) + checkfile = testdir.makepyfile(check_file=""" + def check_func(): + assert 42 == 42 + class CustomTestClass: + def check_method(self): + assert 23 == 23 + """) + # check that directory collects "check_" files + config = testdir.parseconfig() + col = config.getfsnode(checkfile.dirpath()) + colitems = col.collect() + assert len(colitems) == 1 + assert isinstance(colitems[0], py.test.collect.Module) + + # check that module collects "check_" functions and methods + config = testdir.parseconfig(checkfile) + col = config.getfsnode(checkfile) + assert isinstance(col, py.test.collect.Module) + colitems = col.collect() + assert len(colitems) == 2 + funccol = colitems[0] + assert isinstance(funccol, py.test.collect.Function) + assert funccol.name == "check_func" + clscol = colitems[1] + assert isinstance(clscol, py.test.collect.Class) + colitems = clscol.collect()[0].collect() + assert len(colitems) == 1 + assert colitems[0].name == "check_method" + diff --git a/testing/pytest/test_pycollect.py b/testing/pytest/test_pycollect.py index 231d1519a..412391c8e 100644 --- a/testing/pytest/test_pycollect.py +++ b/testing/pytest/test_pycollect.py @@ -4,7 +4,7 @@ class TestModule: def test_module_file_not_found(self, testdir): tmpdir = testdir.tmpdir fn = tmpdir.join('nada','no') - col = py.test.collect.Module(fn) + col = py.test.collect.Module(fn, config=testdir.Config()) col.config = testdir.parseconfig(tmpdir) py.test.raises(py.error.ENOENT, col.collect) @@ -213,13 +213,13 @@ class TestFunction: def test_function_equality(self, testdir, tmpdir): config = testdir.reparseconfig() - f1 = py.test.collect.Function(name="name", + f1 = py.test.collect.Function(name="name", config=config, args=(1,), callobj=isinstance) - f2 = py.test.collect.Function(name="name", + f2 = py.test.collect.Function(name="name",config=config, args=(1,), callobj=py.builtin.callable) assert not f1 == f2 assert f1 != f2 - f3 = py.test.collect.Function(name="name", + f3 = py.test.collect.Function(name="name", config=config, args=(1,2), callobj=py.builtin.callable) assert not f3 == f2 assert f3 != f2 @@ -227,7 +227,7 @@ class TestFunction: assert not f3 == f1 assert f3 != f1 - f1_b = py.test.collect.Function(name="name", + f1_b = py.test.collect.Function(name="name", config=config, args=(1,), callobj=isinstance) assert f1 == f1_b assert not f1 != f1_b @@ -242,9 +242,9 @@ class TestFunction: param = 1 funcargs = {} id = "world" - f5 = py.test.collect.Function(name="name", + f5 = py.test.collect.Function(name="name", config=config, callspec=callspec1, callobj=isinstance) - f5b = py.test.collect.Function(name="name", + f5b = py.test.collect.Function(name="name", config=config, callspec=callspec2, callobj=isinstance) assert f5 != f5b assert not (f5 == f5b) @@ -313,55 +313,21 @@ class TestSorting: class TestConftestCustomization: - def test_extra_python_files_and_functions(self, testdir): - testdir.makepyfile(conftest=""" + def test_pytest_pycollect_makeitem(self, testdir): + testdir.makeconftest(""" import py class MyFunction(py.test.collect.Function): pass - class Directory(py.test.collect.Directory): - def consider_file(self, path): - if path.check(fnmatch="check_*.py"): - return self.Module(path, parent=self) - return super(Directory, self).consider_file(path) - class myfuncmixin: - Function = MyFunction - def funcnamefilter(self, name): - return name.startswith('check_') - class Module(myfuncmixin, py.test.collect.Module): - def classnamefilter(self, name): - return name.startswith('CustomTestClass') - class Instance(myfuncmixin, py.test.collect.Instance): - pass + def pytest_pycollect_makeitem(collector, name, obj): + if name == "some": + return MyFunction(name, collector) """) - checkfile = testdir.makepyfile(check_file=""" - def check_func(): - assert 42 == 42 - class CustomTestClass: - def check_method(self): - assert 23 == 23 - """) - # check that directory collects "check_" files - config = testdir.parseconfig() - col = config.getfsnode(checkfile.dirpath()) - colitems = col.collect() - assert len(colitems) == 1 - assert isinstance(colitems[0], py.test.collect.Module) - - # check that module collects "check_" functions and methods - config = testdir.parseconfig(checkfile) - col = config.getfsnode(checkfile) - assert isinstance(col, py.test.collect.Module) - colitems = col.collect() - assert len(colitems) == 2 - funccol = colitems[0] - assert isinstance(funccol, py.test.collect.Function) - assert funccol.name == "check_func" - clscol = colitems[1] - assert isinstance(clscol, py.test.collect.Class) - colitems = clscol.collect()[0].collect() - assert len(colitems) == 1 - assert colitems[0].name == "check_method" - + testdir.makepyfile("def some(): pass") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyFunction*some*", + ]) + def test_makeitem_non_underscore(self, testdir, monkeypatch): modcol = testdir.getmodulecol("def _hello(): pass") l = []