-
Notifications
You must be signed in to change notification settings - Fork 18
/
plugin.py
145 lines (110 loc) · 4.51 KB
/
plugin.py
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
import sys
import types
from _pytest.python import PyCollector
def trace_function(funcobj, *args, **kwargs):
"""Call a function, and return its locals"""
funclocals = {}
def _tracefunc(frame, event, arg):
# Activate local trace for first call only
if frame.f_back.f_locals.get('_tracefunc') == _tracefunc:
if event == 'return':
funclocals.update(frame.f_locals)
sys.setprofile(_tracefunc)
try:
funcobj(*args, **kwargs)
finally:
sys.setprofile(None)
return funclocals
def make_module_from_function(funcobj):
"""Evaluates the local scope of a function, as if it was a module"""
module = types.ModuleType(funcobj.__name__)
# Import shared behaviors into the generated module. We do this before
# importing the direct children, so that fixtures in the block that's
# importing the behavior take precedence.
for shared_funcobj in getattr(funcobj, '_behaves_like', []):
module.__dict__.update(evaluate_shared_behavior(shared_funcobj))
# Import children
module.__dict__.update(trace_function(funcobj))
return module
def evaluate_shared_behavior(funcobj):
if not hasattr(funcobj, '_shared_functions'):
funcobj._shared_functions = {}
for name, obj in trace_function(funcobj).items():
# Only functions are relevant here
if not isinstance(obj, types.FunctionType):
continue
# Mangle names of imported functions, except fixtures because we
# want fixtures to be overridden in the block that's importing the
# behavior.
if not hasattr(obj, '_pytestfixturefunction'):
name = obj._mangled_name = f"{funcobj.__name__}::{name}"
funcobj._shared_functions[name] = obj
return funcobj._shared_functions
def copy_markinfo(module, funcobj):
for obj in module.__dict__.values():
if isinstance(obj, types.FunctionType):
merge_pytestmark(obj, funcobj)
def merge_pytestmark(obj, parentobj):
marks = {**pytestmark_dict(parentobj), **pytestmark_dict(obj)}
if marks:
obj.pytestmark = list(marks.values())
def pytestmark_name(mark):
name = mark.name
if name == 'parametrize':
argnames = mark.args[0]
if not isinstance(argnames, str):
argnames = ','.join(argnames)
name += '-' + argnames
return name
def pytestmark_dict(obj):
try:
marks = obj.pytestmark
if not isinstance(marks, list):
marks = [marks]
return {pytestmark_name(mark): mark for mark in marks}
except AttributeError:
return {}
class DescribeBlock(PyCollector):
"""Module-like object representing the scope of a describe block"""
@classmethod
def from_parent(cls, parent, obj):
name = obj.__name__
try:
from_parent_super = super().from_parent
except AttributeError: # PyTest < 5.4
self = cls(name, parent)
else:
self = from_parent_super(parent, name=name)
self._name = getattr(obj, '_mangled_name', name)
self.funcobj = obj
return self
def collect(self):
self.session._fixturemanager.parsefactories(self)
return super().collect()
def _getobj(self):
return self._importtestmodule()
def _makeid(self):
"""Magic that makes fixtures local to each scope"""
return f'{self.parent.nodeid}::{self._name}'
def _importtestmodule(self):
"""Import a describe block as if it was a module"""
module = make_module_from_function(self.funcobj)
copy_markinfo(module, self.funcobj)
merge_pytestmark(module, self.parent.obj)
return module
def funcnamefilter(self, name):
"""Treat all nested functions as tests, without requiring the 'test_' prefix"""
return not name.startswith('_')
def classnamefilter(self, name):
"""Don't allow test classes inside describe"""
return False
def __repr__(self):
return f"<{self.__class__.__name__} {self._name!r}>"
def pytest_pycollect_makeitem(collector, name, obj):
if isinstance(obj, types.FunctionType):
for prefix in collector.config.getini('describe_prefixes'):
if obj.__name__.startswith(prefix):
return DescribeBlock.from_parent(collector, obj)
def pytest_addoption(parser):
parser.addini("describe_prefixes", type="args", default=("describe",),
help="prefixes for Python describe function discovery")