1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
import inspect
import logging
import types
from pytest import Class, Module
# NOTE: don't see any other way to get access to pytest innards besides using
# the underscored name :(
from _pytest.fixtures import getfixturemarker
from _pytest.python import PyCollector
log = logging.getLogger("relaxed")
# NOTE: these are defined here for reuse by both pytest's own machinery and our
# internal bits.
def istestclass(name):
return not name.startswith("_")
# NOTE: this is defined at top level due to a couple spots of reuse outside of
# the mixin class itself.
def istestfunction(obj, name):
is_hidden_name = name.startswith("_") or name in (
"setup",
"setup_method",
"teardown",
"teardown_method",
)
is_fixture = getfixturemarker(obj) is not None
return not (is_hidden_name or is_fixture)
# All other classes in here currently inherit from PyCollector, and it is what
# defines the default istestfunction/istestclass, so makes sense to inherit
# from it for our mixin. (PyobjMixin, another commonly found class, offers
# nothing of interest to us however.)
class RelaxedMixin(PyCollector):
"""
A mixin applying collection rules to both modules and inner/nested classes.
"""
# TODO:
# - worth calling super() in these? Difficult to know what to do with it;
# it would say "no" to lots of stuff we want to say "yes" to.
# - are there other tests to apply to 'obj' in a vacuum? so far only thing
# we test 'obj' for is its membership in a module, which must happen inside
# SpecModule's override.
def istestclass(self, obj, name):
return istestclass(name)
def istestfunction(self, obj, name):
return istestfunction(obj, name)
class SpecModule(RelaxedMixin, Module):
def _is_test_obj(self, test_func, obj, name):
# First run our super() test, which should be RelaxedMixin's.
good_name = getattr(super(), test_func)(obj, name)
# If RelaxedMixin said no, we can't really say yes, as the name itself
# was bad - private, other non test name like setup(), etc
if not good_name:
return False
# Here, we dig further based on our own wrapped module obj, by
# rejecting anything not defined locally.
if inspect.getmodule(obj) is not self.obj:
return False
# No other complaints -> it's probably good
return True
def istestfunction(self, obj, name):
return self._is_test_obj("istestfunction", obj, name)
def istestclass(self, obj, name):
return self._is_test_obj("istestclass", obj, name)
def collect(self):
# Given we've overridden naming constraints etc above, just use
# superclass' collection logic for the rest of the necessary behavior.
items = super().collect()
collected = []
for item in items:
# Replace Class objects with recursive SpecClasses
# NOTE: we could explicitly skip unittest objects here (we'd want
# them to be handled by pytest's own unittest support) but since
# those are almost always in test_prefixed_filenames anyways...meh
if isinstance(item, Class):
item = SpecClass.from_parent(item.parent, name=item.name)
collected.append(item)
return collected
class SpecClass(RelaxedMixin, Class):
def _getobj(self):
# Regular object-making first
obj = super()._getobj()
# Short circuit if this obj isn't a nested class (aka child):
# - no parent attr: implies module-level obj definition
# - parent attr, but isn't a class: implies method
if not hasattr(self, "parent") or not isinstance(
self.parent, SpecClass
):
return obj
# Then decorate it with our parent's extra attributes, allowing nested
# test classes to appear as an aggregate of parents' "scopes".
parent_obj = self.parent.obj
# Obtain parent attributes, etc not found on our obj (serves as both a
# useful identifier of "stuff added to an outer class" and a way of
# ensuring that we can override such attrs), and set them on obj
delta = set(dir(parent_obj)).difference(set(dir(obj)))
for name in delta:
value = getattr(parent_obj, name)
# Pytest's pytestmark attributes always get skipped, we don't want
# to spread that around where it's not wanted. (Besides, it can
# cause a lot of collection level warnings.)
if name == "pytestmark":
continue
# Classes get skipped; they'd always just be other 'inner' classes
# that we don't want to copy elsewhere.
if isinstance(value, type):
continue
# Functions (methods) may get skipped, or not, depending:
# NOTE: as of pytest 7, for some reason the value appears as a
# function and not a method (???) so covering both bases...
if isinstance(value, (types.MethodType, types.FunctionType)):
# If they look like tests, they get skipped; don't want to copy
# tests around!
if istestfunction(obj, name):
continue
# Non-test == they're probably lifecycle methods
# (setup/teardown) or helpers (_do_thing). Rebind them to the
# target instance, otherwise the 'self' in the setup/helper is
# not the same 'self' as that in the actual test method it runs
# around or within!
setattr(obj, name, value)
# Anything else should be some data-type attribute, which is copied
# verbatim / by-value.
else:
setattr(obj, name, value)
return obj
def collect(self):
ret = []
for item in super().collect():
# More pytestmark skipping.
if item.name == "pytestmark":
continue
if isinstance(item, Class):
item = SpecClass.from_parent(
parent=item.parent, name=item.name, obj=item.obj
)
ret.append(item)
return ret
|