From 4e3a8077330a20488eb818a1f280c4bd5a37fc67 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 28 Sep 2015 13:34:28 +0200 Subject: [PATCH 1/4] fix issue #1073 -- shortcut plugin hook lookup if the attrname is not prefixed with pytest_. --- _pytest/config.py | 6 ++++++ testing/test_conftest.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/_pytest/config.py b/_pytest/config.py index def26a02b..c327436ad 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -174,6 +174,12 @@ class PytestPluginManager(PluginManager): if exclude_pytest_names(name): return None + # pytest hooks are always prefixed with pytest_ + # so we avoid accessing possibly non-readable attributes + # (see issue #1073) + if not name.startswith("pytest_"): + return + method = getattr(plugin, name) opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name) if opts is not None: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 6700502c4..a0b77cfa5 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -388,3 +388,19 @@ def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error): if error: match += '*%d error*' % error result.stdout.fnmatch_lines(match) + + +def test_issue1073_conftest_special_objects(testdir): + testdir.makeconftest(""" + class DontTouchMe: + def __getattr__(self, x): + raise Exception('cant touch me') + + x = DontTouchMe() + """) + testdir.makepyfile(""" + def test_some(): + pass + """) + res = testdir.runpytest() + assert res.ret == 0 From 1c0ffc5caf783fce6d3d89bdef1de97cea310a33 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 28 Sep 2015 14:02:30 +0200 Subject: [PATCH 2/4] seems like pypy's callable builtin calls __getattr__ so we do the check later. --- _pytest/python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 6548cdbf5..0ad18b3ef 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1918,14 +1918,14 @@ class FixtureManager: autousenames = [] for name in dir(holderobj): obj = getattr(holderobj, name, None) - if not callable(obj): - continue # fixture functions have a pytest_funcarg__ prefix (pre-2.3 style) # or are "@pytest.fixture" marked marker = getfixturemarker(obj) if marker is None: if not name.startswith(self._argprefix): continue + if not callable(obj): + continue marker = defaultfuncargprefixmarker name = name[len(self._argprefix):] elif not isinstance(marker, FixtureFunctionMarker): From 03aca9ea79897a5c5f4eccbf74faa0c8c575fd40 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 28 Sep 2015 14:17:58 +0200 Subject: [PATCH 3/4] add changelog entry for fix #1073 --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 864c211b0..771de3f4f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ 2.8.1.dev --------- +- fix issue #1073: avoid calling __getattr__ on potential plugin objects. + This fixes an incompatibility with pytest-django. Thanks Andreas Pelme, + Bruno Oliveira and Ronny Pfannschmidt for contributing and Holger Krekel + for the fix. + - Fix issue #704: handle versionconflict during plugin loading more gracefully. Thanks Bruno Oliveira for the PR. From 971ebcbd776a122d6785a73476e727c8eade0336 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 28 Sep 2015 15:43:55 +0200 Subject: [PATCH 4/4] simplify by removing the single-call "exclude_pytest_names" function --- _pytest/config.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index c327436ad..8be167a21 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -120,12 +120,6 @@ def _prepareconfig(args=None, plugins=None): raise -def exclude_pytest_names(name): - return not name.startswith(name) or name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") - - - class PytestPluginManager(PluginManager): """ Overwrites :py:class:`pluggy.PluginManager` to add pytest-specific @@ -171,14 +165,14 @@ class PytestPluginManager(PluginManager): return self.add_hookspecs(module_or_class) def parse_hookimpl_opts(self, plugin, name): - if exclude_pytest_names(name): - return None - # pytest hooks are always prefixed with pytest_ # so we avoid accessing possibly non-readable attributes # (see issue #1073) if not name.startswith("pytest_"): return + # ignore some historic special names which can not be hooks anyway + if name == "pytest_plugins" or name.startswith("pytest_funcarg__"): + return method = getattr(plugin, name) opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name)