-
Notifications
You must be signed in to change notification settings - Fork 266
/
drainage.py
157 lines (119 loc) · 4.75 KB
/
drainage.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
146
147
148
149
150
151
152
153
154
155
156
157
import contextlib
import functools
import sys
from collections import defaultdict
from typing import DefaultDict
if sys.version_info >= (3, 8):
from typing import Final, Literal, _TypedDictMeta # type: ignore[attr-defined] # noqa: F401
else:
from typing_extensions import ( # type: ignore[attr-defined] # noqa: F401
Final, Literal, _TypedDictMeta,
)
class GeneratorStats:
_warn_cache: DefaultDict[str, int] = defaultdict(int)
_error_cache: DefaultDict[str, int] = defaultdict(int)
def __getattr__(self, name):
if not self.__dict__:
from drf_spectacular.settings import spectacular_settings
self.silent = spectacular_settings.DISABLE_ERRORS_AND_WARNINGS
return getattr(self, name)
def __bool__(self):
return bool(self._warn_cache or self._error_cache)
@contextlib.contextmanager
def silence(self):
self.silent, tmp = True, self.silent
try:
yield
finally:
self.silent = tmp
def reset(self):
self._warn_cache.clear()
self._error_cache.clear()
def emit(self, msg, severity):
assert severity in ['warning', 'error']
msg = _get_current_trace() + str(msg)
cache = self._warn_cache if severity == 'warning' else self._error_cache
if not self.silent and msg not in cache:
print(f'{severity.capitalize()} #{len(cache)}: {msg}', file=sys.stderr)
cache[msg] += 1
def emit_summary(self):
if not self.silent and (self._warn_cache or self._error_cache):
print(
f'\nSchema generation summary:\n'
f'Warnings: {sum(self._warn_cache.values())} ({len(self._warn_cache)} unique)\n'
f'Errors: {sum(self._error_cache.values())} ({len(self._error_cache)} unique)\n',
file=sys.stderr
)
GENERATOR_STATS = GeneratorStats()
def warn(msg):
GENERATOR_STATS.emit(msg, 'warning')
def error(msg):
GENERATOR_STATS.emit(msg, 'error')
def reset_generator_stats():
GENERATOR_STATS.reset()
_TRACES = []
@contextlib.contextmanager
def add_trace_message(trace_message):
"""
Adds a message to be used as a prefix when emitting warnings and errors.
"""
_TRACES.append(trace_message)
yield
_TRACES.pop()
def _get_current_trace():
return ''.join(f"{trace}: " for trace in _TRACES if trace)
def has_override(obj, prop):
if isinstance(obj, functools.partial):
obj = obj.func
if not hasattr(obj, '_spectacular_annotation'):
return False
if prop not in obj._spectacular_annotation:
return False
return True
def get_override(obj, prop, default=None):
if isinstance(obj, functools.partial):
obj = obj.func
if not has_override(obj, prop):
return default
return obj._spectacular_annotation[prop]
def set_override(obj, prop, value):
if not hasattr(obj, '_spectacular_annotation'):
obj._spectacular_annotation = {}
elif '_spectacular_annotation' not in obj.__dict__:
obj._spectacular_annotation = obj._spectacular_annotation.copy()
obj._spectacular_annotation[prop] = value
return obj
def get_view_method_names(view, schema=None):
schema = schema or view.schema
return [
item for item in dir(view) if callable(getattr(view, item)) and (
item in view.http_method_names
or item in schema.method_mapping.values()
or item == 'list'
or hasattr(getattr(view, item), 'mapping')
)
]
def isolate_view_method(view, method_name):
"""
Prevent modifying a view method which is derived from other views. Changes to
a derived method would leak into the view where the method originated from.
Break derivation by wrapping the method and explicitly setting it on the view.
"""
method = getattr(view, method_name)
# no isolation is required if the view method is not derived.
# @api_view is a special case that also breaks isolation. It proxies all view
# methods through a single handler function, which then also requires isolation.
if method_name in view.__dict__ and method.__name__ != 'handler':
return method
@functools.wraps(method)
def wrapped_method(self, request, *args, **kwargs):
return method(self, request, *args, **kwargs)
# wraps() will only create a shallow copy of method.__dict__. Updates to "kwargs"
# via @extend_schema would leak to the original method. Isolate by creating a copy.
if hasattr(method, 'kwargs'):
wrapped_method.kwargs = method.kwargs.copy()
setattr(view, method_name, wrapped_method)
return wrapped_method
def cache(user_function):
""" simple polyfill for python < 3.9 """
return functools.lru_cache(maxsize=None)(user_function)