Various cleanups in src/_pytest/python.py (#5599)

Various cleanups in src/_pytest/python.py
This commit is contained in:
Bruno Oliveira 2019-07-14 19:00:35 -03:00 committed by GitHub
commit 499fda2349
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 54 additions and 67 deletions

View File

@ -1,11 +1,12 @@
""" Python test discovery, setup and run of test functions. """ """ Python test discovery, setup and run of test functions. """
import collections
import enum import enum
import fnmatch import fnmatch
import inspect import inspect
import os import os
import sys import sys
import warnings import warnings
from collections import Counter
from collections.abc import Sequence
from functools import partial from functools import partial
from textwrap import dedent from textwrap import dedent
@ -240,9 +241,6 @@ class PyobjContext:
class PyobjMixin(PyobjContext): class PyobjMixin(PyobjContext):
_ALLOW_MARKERS = True _ALLOW_MARKERS = True
def __init__(self, *k, **kw):
super().__init__(*k, **kw)
@property @property
def obj(self): def obj(self):
"""Underlying Python object.""" """Underlying Python object."""
@ -394,12 +392,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
methods.append(module.pytest_generate_tests) methods.append(module.pytest_generate_tests)
if hasattr(cls, "pytest_generate_tests"): if hasattr(cls, "pytest_generate_tests"):
methods.append(cls().pytest_generate_tests) methods.append(cls().pytest_generate_tests)
if methods:
self.ihook.pytest_generate_tests.call_extra( self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
methods, dict(metafunc=metafunc)
)
else:
self.ihook.pytest_generate_tests(metafunc=metafunc)
if not metafunc._calls: if not metafunc._calls:
yield Function(name, parent=self, fixtureinfo=fixtureinfo) yield Function(name, parent=self, fixtureinfo=fixtureinfo)
@ -444,13 +438,12 @@ class Module(nodes.File, PyCollector):
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517). other fixtures (#517).
""" """
setup_module = _get_non_fixture_func(self.obj, "setUpModule") setup_module = _get_first_non_fixture_func(
if setup_module is None: self.obj, ("setUpModule", "setup_module")
setup_module = _get_non_fixture_func(self.obj, "setup_module") )
teardown_module = _get_first_non_fixture_func(
teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") self.obj, ("tearDownModule", "teardown_module")
if teardown_module is None: )
teardown_module = _get_non_fixture_func(self.obj, "teardown_module")
if setup_module is None and teardown_module is None: if setup_module is None and teardown_module is None:
return return
@ -472,8 +465,10 @@ class Module(nodes.File, PyCollector):
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517). other fixtures (#517).
""" """
setup_function = _get_non_fixture_func(self.obj, "setup_function") setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",))
teardown_function = _get_non_fixture_func(self.obj, "teardown_function") teardown_function = _get_first_non_fixture_func(
self.obj, ("teardown_function",)
)
if setup_function is None and teardown_function is None: if setup_function is None and teardown_function is None:
return return
@ -557,15 +552,15 @@ class Package(Module):
def setup(self): def setup(self):
# not using fixtures to call setup_module here because autouse fixtures # not using fixtures to call setup_module here because autouse fixtures
# from packages are not called automatically (#4085) # from packages are not called automatically (#4085)
setup_module = _get_non_fixture_func(self.obj, "setUpModule") setup_module = _get_first_non_fixture_func(
if setup_module is None: self.obj, ("setUpModule", "setup_module")
setup_module = _get_non_fixture_func(self.obj, "setup_module") )
if setup_module is not None: if setup_module is not None:
_call_with_optional_argument(setup_module, self.obj) _call_with_optional_argument(setup_module, self.obj)
teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") teardown_module = _get_first_non_fixture_func(
if teardown_module is None: self.obj, ("tearDownModule", "teardown_module")
teardown_module = _get_non_fixture_func(self.obj, "teardown_module") )
if teardown_module is not None: if teardown_module is not None:
func = partial(_call_with_optional_argument, teardown_module, self.obj) func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func) self.addfinalizer(func)
@ -656,27 +651,6 @@ class Package(Module):
pkg_prefixes.add(path) pkg_prefixes.add(path)
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
"""
Return a callable to perform xunit-style setup or teardown if
the function exists in the ``holder`` object.
The ``param_obj`` parameter is the parameter which will be passed to the function
when the callable is called without arguments, defaults to the ``holder`` object.
Return ``None`` if a suitable callable is not found.
"""
# TODO: only needed because of Package!
param_obj = param_obj if param_obj is not None else holder
result = _get_non_fixture_func(holder, attr_name)
if result is not None:
arg_count = result.__code__.co_argcount
if inspect.ismethod(result):
arg_count -= 1
if arg_count:
return lambda: result(param_obj)
else:
return result
def _call_with_optional_argument(func, arg): def _call_with_optional_argument(func, arg):
"""Call the given function with the given argument if func accepts one argument, otherwise """Call the given function with the given argument if func accepts one argument, otherwise
calls func without arguments""" calls func without arguments"""
@ -689,14 +663,15 @@ def _call_with_optional_argument(func, arg):
func() func()
def _get_non_fixture_func(obj, name): def _get_first_non_fixture_func(obj, names):
"""Return the attribute from the given object to be used as a setup/teardown """Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to xunit-style function, but only if not marked as a fixture to
avoid calling it twice. avoid calling it twice.
""" """
meth = getattr(obj, name, None) for name in names:
if fixtures.getfixturemarker(meth) is None: meth = getattr(obj, name, None)
return meth if meth is not None and fixtures.getfixturemarker(meth) is None:
return meth
class Class(PyCollector): class Class(PyCollector):
@ -736,7 +711,7 @@ class Class(PyCollector):
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517). other fixtures (#517).
""" """
setup_class = _get_non_fixture_func(self.obj, "setup_class") setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
teardown_class = getattr(self.obj, "teardown_class", None) teardown_class = getattr(self.obj, "teardown_class", None)
if setup_class is None and teardown_class is None: if setup_class is None and teardown_class is None:
return return
@ -760,7 +735,7 @@ class Class(PyCollector):
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517). other fixtures (#517).
""" """
setup_method = _get_non_fixture_func(self.obj, "setup_method") setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",))
teardown_method = getattr(self.obj, "teardown_method", None) teardown_method = getattr(self.obj, "teardown_method", None)
if setup_method is None and teardown_method is None: if setup_method is None and teardown_method is None:
return return
@ -1070,12 +1045,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
* "params" if the argname should be the parameter of a fixture of the same name. * "params" if the argname should be the parameter of a fixture of the same name.
* "funcargs" if the argname should be a parameter to the parametrized test function. * "funcargs" if the argname should be a parameter to the parametrized test function.
""" """
valtypes = {} if isinstance(indirect, bool):
if indirect is True: valtypes = dict.fromkeys(argnames, "params" if indirect else "funcargs")
valtypes = dict.fromkeys(argnames, "params") elif isinstance(indirect, Sequence):
elif indirect is False:
valtypes = dict.fromkeys(argnames, "funcargs")
elif isinstance(indirect, (tuple, list)):
valtypes = dict.fromkeys(argnames, "funcargs") valtypes = dict.fromkeys(argnames, "funcargs")
for arg in indirect: for arg in indirect:
if arg not in argnames: if arg not in argnames:
@ -1086,6 +1058,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
pytrace=False, pytrace=False,
) )
valtypes[arg] = "params" valtypes[arg] = "params"
else:
fail(
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
type=type(indirect).__name__, func=self.function.__name__
),
pytrace=False,
)
return valtypes return valtypes
def _validate_if_using_arg_names(self, argnames, indirect): def _validate_if_using_arg_names(self, argnames, indirect):
@ -1213,7 +1192,7 @@ def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None
if len(set(ids)) != len(ids): if len(set(ids)) != len(ids):
# The ids are not unique # The ids are not unique
duplicates = [testid for testid in ids if ids.count(testid) > 1] duplicates = [testid for testid in ids if ids.count(testid) > 1]
counters = collections.defaultdict(lambda: 0) counters = Counter()
for index, testid in enumerate(ids): for index, testid in enumerate(ids):
if testid in duplicates: if testid in duplicates:
ids[index] = testid + str(counters[testid]) ids[index] = testid + str(counters[testid])
@ -1402,14 +1381,11 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
# https://github.com/pytest-dev/pytest/issues/4569 # https://github.com/pytest-dev/pytest/issues/4569
self.keywords.update( self.keywords.update(
dict.fromkeys( {
[ mark.name: True
mark.name for mark in self.iter_markers()
for mark in self.iter_markers() if mark.name not in self.keywords
if mark.name not in self.keywords }
],
True,
)
) )
if fixtureinfo is None: if fixtureinfo is None:

View File

@ -599,6 +599,17 @@ class TestMetafunc:
assert metafunc._calls[0].funcargs == dict(x="a", y="b") assert metafunc._calls[0].funcargs == dict(x="a", y="b")
assert metafunc._calls[0].params == {} assert metafunc._calls[0].params == {}
def test_parametrize_indirect_wrong_type(self):
def func(x, y):
pass
metafunc = self.Metafunc(func)
with pytest.raises(
pytest.fail.Exception,
match="In func: expected Sequence or boolean for indirect, got dict",
):
metafunc.parametrize("x, y", [("a", "b")], indirect={})
def test_parametrize_indirect_list_functional(self, testdir): def test_parametrize_indirect_list_functional(self, testdir):
""" """
#714 #714