Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
jantic
GitHub Repository: jantic/deoldify
Path: blob/master/fastai/gen_doc/doctest.py
781 views
1
import sys, re, json, pprint
2
from pathlib import Path
3
from collections import defaultdict
4
from inspect import currentframe, getframeinfo, ismodule
5
6
__all__ = ['this_tests']
7
8
DB_NAME = 'test_registry.json'
9
10
def _json_set_default(obj):
11
if isinstance(obj, set): return list(obj)
12
raise TypeError
13
14
class TestRegistry:
15
"Tests register which API they validate using this class."
16
registry = defaultdict(list)
17
this_tests_check = None
18
missing_this_tests = set()
19
20
# logic for checking whether each test calls `this_tests`:
21
# 1. `this_tests_check` is set to True during test's 'setup' stage if it wasn't skipped
22
# 2. if the test is dynamically skipped `this_tests_check` is set to False
23
# 3. `this_tests` sets this flag to False when it's successfully completes
24
# 4. if during the 'teardown' stage `this_tests_check` is still True then we
25
# know that this test needs `this_tests_check`
26
27
@staticmethod
28
def this_tests(*funcs):
29
prev_frame = currentframe().f_back.f_back
30
file_name, lineno, test_name, _, _ = getframeinfo(prev_frame)
31
parent_func_lineno, _ = get_parent_func(lineno, get_lines(file_name))
32
entry = {'file': relative_test_path(file_name), 'test': test_name , 'line': parent_func_lineno}
33
for func in funcs:
34
if func == 'na':
35
# special case when we can't find a function to declare, e.g.
36
# when attributes are tested
37
continue
38
try:
39
func_fq = get_func_fq_name(func)
40
except:
41
raise Exception(f"'{func}' is not a function") from None
42
if re.match(r'fastai\.', func_fq):
43
if entry not in TestRegistry.registry[func_fq]:
44
TestRegistry.registry[func_fq].append(entry)
45
else:
46
raise Exception(f"'{func}' is not in the fastai API") from None
47
TestRegistry.this_tests_check = False
48
49
def this_tests_check_on():
50
TestRegistry.this_tests_check = True
51
52
def this_tests_check_off():
53
TestRegistry.this_tests_check = False
54
55
def this_tests_check_run(file_name, test_name):
56
if TestRegistry.this_tests_check:
57
TestRegistry.missing_this_tests.add(f"{file_name}::{test_name}")
58
59
def registry_save():
60
if TestRegistry.registry:
61
path = Path(__file__).parent.parent.resolve()/DB_NAME
62
if path.exists():
63
#print("\n*** Merging with the existing test registry")
64
with open(path, 'r') as f: old_registry = json.load(f)
65
TestRegistry.registry = merge_registries(old_registry, TestRegistry.registry)
66
#print(f"\n*** Saving test registry @ {path}")
67
with open(path, 'w') as f:
68
json.dump(obj=TestRegistry.registry, fp=f, indent=4, sort_keys=True, default=_json_set_default)
69
70
def missing_this_tests_alert():
71
if TestRegistry.missing_this_tests:
72
tests = '\n '.join(sorted(TestRegistry.missing_this_tests))
73
print(f"""
74
*** Attention ***
75
Please include `this_tests` call in each of the following tests:
76
{tests}
77
For details see: https://docs.fast.ai/dev/test.html#test-registry""")
78
79
# merge_registries helpers
80
# merge dict of lists of dict
81
def a2k(a): return '::'.join([a['file'], a['test']]), a['line']
82
def k2a(k, v): f,t = k.split('::'); return {"file": f, "line": v, "test": t}
83
# merge by key that is a combination of 2 values: test, file
84
def merge_lists(a, b):
85
x = dict(map(a2k, [*a, *b])) # pack + merge
86
return [k2a(k, v) for k,v in x.items()] # unpack
87
def merge_registries(a, b):
88
for i in b: a[i] = merge_lists(a[i], b[i]) if i in a else b[i]
89
return a
90
91
def this_tests(*funcs): TestRegistry.this_tests(*funcs)
92
93
def str2func(name):
94
"Converts 'fastai.foo.bar' into an function 'object' if such exists"
95
if isinstance(name, str): subpaths = name.split('.')
96
else: return None
97
98
module = subpaths.pop(0)
99
if module in sys.modules: obj = sys.modules[module]
100
else: return None
101
102
for subpath in subpaths:
103
obj = getattr(obj, subpath, None)
104
if obj == None: return None
105
return obj
106
107
def get_func_fq_name(func):
108
if ismodule(func): return func.__name__
109
if isinstance(func, str): func = str2func(func)
110
name = None
111
if hasattr(func, '__qualname__'): name = func.__qualname__
112
elif hasattr(func, '__name__'): name = func.__name__
113
elif hasattr(func, '__wrapped__'): return get_func_fq_name(func.__wrapped__)
114
elif hasattr(func, '__class__'): name = func.__class__.__name__
115
else: raise Exception(f"'{func}' is not a func or class")
116
return f'{func.__module__}.{name}'
117
118
def get_parent_func(lineno, lines, ignore_missing=False):
119
"Find any lines where `elt` is called and return the parent test function"
120
for idx,l in enumerate(reversed(lines[:lineno])):
121
if re.match(f'\s*def test', l): return (lineno - idx), l # 1 based index for github
122
if re.match(f'\w+', l): break # top level indent - out of function scope
123
if ignore_missing: return None
124
raise LookupError('Could not find parent function for line:', lineno, lines[:lineno])
125
126
def relative_test_path(test_file:Path)->str:
127
"Path relative to the `fastai` parent directory"
128
test_file = Path(test_file)
129
testdir_idx = list(reversed(test_file.parts)).index('tests')
130
return '/'.join(test_file.parts[-(testdir_idx+1):])
131
132
def get_lines(file):
133
with open(file, 'r') as f: return f.readlines()
134
135