codetac 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ """Detail on request (Phase 4 of the Node version, spec 3.8).
2
+
3
+ The panel writes .codetac/<recording>/detalhe.json: functions (file, line,
4
+ name) and whole files. For those functions only, and from their next call:
5
+
6
+ - the arguments, read from the frame when the call starts;
7
+ - the value returned, or the type of the exception;
8
+ - the lines that ran: LINE events are switched on for those code objects
9
+ only (sys.monitoring.set_local_events), so the rest of the app pays
10
+ nothing.
11
+
12
+ The `detail` event has the format of the Node runtime (src/runtime.mjs), so
13
+ the panel does not change. The file is read every 400 ms by the writer's
14
+ thread (no thread of its own: see writer.py on daemon threads at exit).
15
+
16
+ preview() never runs code of the app: no __repr__, __str__, properties or
17
+ __getattr__ of any type outside the standard library. Types are tested with
18
+ issubclass on the real type (isinstance may read an overridden __class__),
19
+ built-in containers are read through the methods of their base type, and
20
+ objects of the app through the C descriptor of their __dict__ only.
21
+ """
22
+ import datetime
23
+ import decimal
24
+ import enum
25
+ import json
26
+ import math
27
+ import os
28
+ import sys
29
+ import types
30
+ import uuid
31
+ from pathlib import PurePath
32
+
33
+ from .redact import MARKER, is_sensitive_name
34
+
35
+ MONITORING = sys.monitoring
36
+ LINE = MONITORING.events.LINE
37
+ CO_VARARGS = 0x0004
38
+ CO_VARKEYWORDS = 0x0008
39
+ LIMITS = {'depth': 4, 'items': 20, 'keys': 30, 'nodes': 300, 'text': 2000}
40
+
41
+
42
+ # preview ----------------------------------------------------------------------------
43
+
44
+ _TYPE_DICT = type.__dict__['__dict__'] # a class's own namespace, without its metaclass
45
+ _TYPE_QUALNAME = type.__dict__['__qualname__']
46
+ _TYPE_MODULE = type.__dict__['__module__']
47
+ _TYPE_MRO = type.__dict__['__mro__']
48
+ _GETSET = types.GetSetDescriptorType
49
+
50
+
51
+ def _class_name(cls):
52
+ try:
53
+ return _TYPE_QUALNAME.__get__(cls)
54
+ except Exception:
55
+ return '?'
56
+
57
+
58
+ def _class_module(cls):
59
+ try:
60
+ module = _TYPE_MODULE.__get__(cls)
61
+ return module if isinstance(module, str) else ''
62
+ except Exception:
63
+ return ''
64
+
65
+
66
+ def _instance_dict(item):
67
+ """The instance's __dict__, through the C descriptor of the class that has one; None when
68
+ the class defines __dict__ itself (a property: app code) or has none (slots, C types)."""
69
+ for cls in _TYPE_MRO.__get__(type(item)):
70
+ found = _TYPE_DICT.__get__(cls).get('__dict__')
71
+ if found is None:
72
+ continue
73
+ if type(found) is _GETSET:
74
+ try:
75
+ value = found.__get__(item)
76
+ except Exception:
77
+ return None
78
+ return value if type(value) is dict else None
79
+ return None
80
+ return None
81
+
82
+
83
+ def _query_keys(query):
84
+ keys = []
85
+ for part in query.split('&'):
86
+ key = part.split('=', 1)[0]
87
+ if key and key not in keys:
88
+ keys.append(key)
89
+ return keys
90
+
91
+
92
+ def _path(path, query):
93
+ keys = _query_keys(query) if query else []
94
+ return path + ('?' + '&'.join('%s=…' % key for key in keys) if keys else '')
95
+
96
+
97
+ def _http_summary(item, cls, fields):
98
+ """Requests and responses of Starlette, werkzeug and Flask, from their own fields:
99
+ method, path without query values, header names; never header values or bodies."""
100
+ module = _class_module(cls)
101
+ if not module.startswith(('starlette.', 'fastapi.', 'werkzeug.', 'flask.')):
102
+ return None
103
+ name = _class_name(cls)
104
+ scope = fields.get('scope')
105
+ if type(scope) is dict and scope.get('type') in ('http', 'websocket'):
106
+ query = scope.get('query_string') or b''
107
+ headers = [key.decode('latin-1') for key, _ in list(scope.get('headers') or [])[:40] if type(key) is bytes]
108
+ return {'$classe': name, 'metodo': scope.get('method'), 'caminho': _path(str(scope.get('path', '')), query.decode('latin-1')),
109
+ 'cabecalhos': headers}
110
+ environ = fields.get('environ')
111
+ if type(environ) is dict and 'REQUEST_METHOD' in environ:
112
+ headers = [key[5:].replace('_', '-').lower() for key in environ if type(key) is str and key.startswith('HTTP_')][:40]
113
+ return {'$classe': name, 'metodo': environ.get('REQUEST_METHOD'), 'caminho': _path(str(environ.get('PATH_INFO', '')),
114
+ str(environ.get('QUERY_STRING', ''))),
115
+ 'cabecalhos': headers}
116
+ if 'status_code' in fields and ('raw_headers' in fields or 'body' in fields):
117
+ raw = fields.get('raw_headers') or []
118
+ return {'$classe': name, 'estado': fields.get('status_code') if type(fields.get('status_code')) is int else None,
119
+ 'cabecalhos': [key.decode('latin-1') for key, _ in list(raw)[:40] if type(key) is bytes]}
120
+ if '_status_code' in fields or '_status' in fields:
121
+ code = fields.get('_status_code')
122
+ return {'$classe': name, 'estado': code if type(code) is int else None}
123
+ return {'$classe': name, '$resumido': True}
124
+
125
+
126
+ def preview(value):
127
+ """A readable copy for the detail view: plain JSON, bounded in depth, length and size.
128
+ Redaction happens when the event is emitted."""
129
+ nodes = [0]
130
+ seen = set()
131
+
132
+ def text(item):
133
+ return item if len(item) <= LIMITS['text'] else '%s[TRUNCATED %d]' % (item[:LIMITS['text']], len(item))
134
+
135
+ def walk(item, depth):
136
+ nodes[0] += 1
137
+ if nodes[0] > LIMITS['nodes']:
138
+ return '[…]'
139
+ cls = type(item)
140
+ if item is None or cls is bool:
141
+ return item
142
+ if cls is int:
143
+ return item if -2 ** 53 < item < 2 ** 53 else int.__repr__(item)
144
+ if cls is float:
145
+ return item if math.isfinite(item) else float.__repr__(item)
146
+ if cls is str:
147
+ return text(item)
148
+ if issubclass(cls, (bytes, bytearray, memoryview)):
149
+ return {'$tipo': 'bytes', 'bytes': len(item) if cls in (bytes, bytearray) else memoryview.__len__(item)}
150
+ if cls in (types.FunctionType, types.BuiltinFunctionType, types.MethodType):
151
+ return {'$tipo': 'função', 'nome': getattr(item, '__qualname__', None) if cls is not types.MethodType else '(método)'}
152
+ if issubclass(cls, type):
153
+ return {'$tipo': 'classe', 'nome': _class_name(item)}
154
+ if cls is types.ModuleType:
155
+ return {'$tipo': 'módulo'}
156
+ if cls in (types.GeneratorType, types.CoroutineType, types.AsyncGeneratorType):
157
+ return {'$tipo': {types.GeneratorType: 'gerador', types.CoroutineType: 'corrotina'}.get(cls, 'gerador assíncrono')}
158
+ if issubclass(cls, BaseException):
159
+ # The type only: the message comes from __str__, which may be the app's.
160
+ return {'$tipo': 'erro', 'nome': _class_name(cls)}
161
+ if cls in (datetime.datetime, datetime.date, datetime.time):
162
+ return {'$tipo': 'data', 'valor': cls.isoformat(item)}
163
+ if cls is decimal.Decimal:
164
+ return {'$tipo': 'decimal', 'valor': decimal.Decimal.__str__(item)}
165
+ if cls is uuid.UUID:
166
+ return {'$tipo': 'uuid', 'valor': uuid.UUID.__str__(item)}
167
+ if issubclass(cls, PurePath) and _class_module(cls).startswith('pathlib'):
168
+ return {'$tipo': 'caminho', 'valor': PurePath.__str__(item)}
169
+ key = id(item)
170
+ if key in seen:
171
+ return {'$tipo': 'circular'}
172
+ name = _class_name(cls)
173
+ if depth >= LIMITS['depth']:
174
+ return {'$tipo': name, '$resumido': True}
175
+ seen.add(key)
176
+ try:
177
+ if issubclass(cls, enum.Enum):
178
+ fields = _instance_dict(item) or {}
179
+ return {'$classe': name, 'nome': fields.get('_name_') if type(fields.get('_name_')) is str else None}
180
+ if issubclass(cls, dict):
181
+ out = {}
182
+ entries = list(dict.items(item))
183
+ for entry_key, entry in entries[:LIMITS['keys']]:
184
+ label = entry_key if type(entry_key) is str else walk(entry_key, depth + 1) if type(entry_key) in (int, float, bool) or entry_key is None else '[%s]' % _class_name(type(entry_key))
185
+ out[str(label)] = walk(entry, depth + 1)
186
+ if len(entries) > LIMITS['keys']:
187
+ out['$mais'] = len(entries) - LIMITS['keys']
188
+ if cls is not dict:
189
+ out = {'$classe': name, **out}
190
+ return out
191
+ if issubclass(cls, (list, tuple)):
192
+ base = list if issubclass(cls, list) else tuple
193
+ size = base.__len__(item)
194
+ out = [walk(base.__getitem__(item, index), depth + 1) for index in range(min(size, LIMITS['items']))]
195
+ if size > LIMITS['items']:
196
+ out.append({'$mais': size - LIMITS['items']})
197
+ return out
198
+ if issubclass(cls, (set, frozenset)):
199
+ base = set if issubclass(cls, set) else frozenset
200
+ values = []
201
+ for entry in base.__iter__(item):
202
+ if len(values) >= LIMITS['items']:
203
+ break
204
+ values.append(walk(entry, depth + 1))
205
+ return {'$tipo': 'conjunto', 'tamanho': base.__len__(item), 'valores': values}
206
+ fields = _instance_dict(item)
207
+ if fields is None:
208
+ # Slots, C types (connections, sockets, locks…): the class only.
209
+ return {'$classe': name, '$resumido': True}
210
+ summary = _http_summary(item, cls, fields)
211
+ if summary is not None:
212
+ return summary
213
+ out = {'$classe': name}
214
+ # Fields starting with "_" are internals (frameworks, ORMs), not the app's data.
215
+ keys = [field for field in fields if type(field) is str and not field.startswith('_')]
216
+ for field in keys[:LIMITS['keys']]:
217
+ out[field] = walk(fields[field], depth + 1)
218
+ if len(keys) > LIMITS['keys']:
219
+ out['$mais'] = len(keys) - LIMITS['keys']
220
+ return out
221
+ except Exception:
222
+ return {'$tipo': 'ilegível'}
223
+ finally:
224
+ seen.discard(key)
225
+
226
+ return walk(value, 0)
227
+
228
+
229
+ # Detail on request ------------------------------------------------------------------
230
+
231
+ def parameter_names(code):
232
+ """In the order of the signature: positional, *args, keyword-only, **kwargs
233
+ (co_varnames keeps *args after the keyword-only ones)."""
234
+ names = code.co_varnames
235
+ positional, keyword = code.co_argcount, code.co_kwonlyargcount
236
+ varargs = (names[positional + keyword],) if code.co_flags & CO_VARARGS else ()
237
+ extra = positional + keyword + len(varargs)
238
+ varkw = (names[extra],) if code.co_flags & CO_VARKEYWORDS else ()
239
+ return names[:positional] + varargs + names[positional:positional + keyword] + varkw
240
+
241
+
242
+ def template_line(table, line):
243
+ """The template line of a generated line (Template.get_corresponding_lineno)."""
244
+ for template, generated in reversed(table):
245
+ if generated <= line:
246
+ return template
247
+ return 1
248
+
249
+
250
+ class Detail(object):
251
+ def __init__(self, capture, writer, directory):
252
+ self.capture = capture
253
+ self.writer = writer
254
+ self.file = os.path.join(directory, 'detalhe.json')
255
+ self.stamp = None
256
+ self.functions = set() # "file:line:name"
257
+ self.files = set()
258
+ self.codes = {} # code object -> parameter names, for the functions with detail
259
+ self.calls = {} # id(frame) -> [args, lines, node, request literal, code]
260
+
261
+ # detalhe.json -----------------------------------------------------------------
262
+ def poll(self):
263
+ try:
264
+ stat = os.stat(self.file)
265
+ stamp = (stat.st_mtime_ns, stat.st_size)
266
+ except OSError:
267
+ stamp = None
268
+ if stamp == self.stamp:
269
+ return
270
+ self.stamp = stamp
271
+ spec = {}
272
+ if stamp is not None:
273
+ try:
274
+ with open(self.file, 'rb') as file:
275
+ spec = json.loads(file.read().decode('utf-8'))
276
+ except (OSError, ValueError):
277
+ spec = {}
278
+ functions = spec.get('functions') if isinstance(spec, dict) else None
279
+ files = spec.get('files') if isinstance(spec, dict) else None
280
+ self.functions = set('%s:%s:%s' % (item.get('file'), item.get('line'), item.get('function') or '')
281
+ for item in functions or [] if isinstance(item, dict))
282
+ self.files = set(item for item in files or [] if isinstance(item, str))
283
+ for code, place in list(self.capture.places_of.items()):
284
+ self.consider(code, place)
285
+
286
+ def wanted(self, place):
287
+ file, line, name = place
288
+ return file in self.files or '%s:%s:%s' % (file, line, name) in self.functions or '%s:%s:' % (file, line) in self.functions
289
+
290
+ def consider(self, code, place):
291
+ """Switches detail on or off for one code object of the project."""
292
+ wanted = self.wanted(place)
293
+ tool = self.capture.tool
294
+ if wanted and code not in self.codes:
295
+ self.codes[code] = parameter_names(code)
296
+ MONITORING.set_local_events(tool, code, LINE)
297
+ elif not wanted and code in self.codes:
298
+ del self.codes[code]
299
+ MONITORING.set_local_events(tool, code, 0)
300
+
301
+ # Calls ------------------------------------------------------------------------
302
+ def start(self, code, frame, node, request):
303
+ try:
304
+ names = self.codes.get(code)
305
+ if names is None:
306
+ return
307
+ local = frame.f_locals
308
+ args = [{'name': name, 'value': MARKER if is_sensitive_name(name) else preview(local.get(name))} for name in names]
309
+ except Exception:
310
+ args = []
311
+ self.calls[id(frame)] = [args, set(), node, request, code]
312
+
313
+ def line(self, frame, code, line):
314
+ call = self.calls.get(id(frame))
315
+ if call is not None and call[4] is code:
316
+ table = self.capture.template_tables.get(code)
317
+ call[1].add(template_line(table, line) if table is not None else line)
318
+
319
+ def finish(self, frame, code, returned=None, exception=None):
320
+ call = self.calls.pop(id(frame), None)
321
+ if call is None or call[4] is not code:
322
+ return
323
+ args, lines, node, request, _ = call
324
+ from .context import request_of
325
+ request_id, _ = request_of(request)
326
+ event = {'type': 'detail', 'id': '%s:%d' % (self.writer.process, node), 'requestId': request_id,
327
+ 'args': args, 'lines': sorted(lines)}
328
+ if exception is not None and type(exception) is not GeneratorExit:
329
+ event['threw'] = preview(exception)
330
+ else:
331
+ name = self.capture.places_of.get(code, (None, None, ''))[2]
332
+ event['returned'] = MARKER if is_sensitive_name(name.rsplit('.', 1)[-1]) else preview(returned)
333
+ self.writer.emit(event)
@@ -0,0 +1,135 @@
1
+ """File boundaries (stage 7), as installFiles in src/boundaries.mjs: only
2
+ operations made directly by the project's code during a request.
3
+
4
+ boundary {kind: 'ficheiros', provider: 'sistema de ficheiros', library: 'builtins' | 'os',
5
+ function, operation, path}
6
+
7
+ - open() in a mode that writes (w, a, x, +): 'escrita'. pathlib's
8
+ write_text, write_bytes and open go through io.open;
9
+ - os.remove/unlink/rmdir ('remoção'), os.rename/replace ('mudança de
10
+ nome'), os.mkdir ('criação de pasta'; os.makedirs and Path.mkdir use it).
11
+
12
+ The first caller frame outside the captor and the standard library decides:
13
+ a library (site-packages, a virtual environment) is not the project. Reads
14
+ are not recorded (M66). The captor's own files and CodeTAC's data folder are
15
+ never boundaries; the captor writes with os.open/os.write, which are not
16
+ observed.
17
+
18
+ Keep it importable on old Pythons (3.8+): the minimal mode records boundaries.
19
+ """
20
+ import builtins
21
+ import functools
22
+ import io
23
+ import os
24
+ import sys
25
+ import sysconfig
26
+
27
+ from .boundaries import _inside, start_boundary
28
+ from .context import NULL, current
29
+ from .project import CAPTOR, ProjectFiles
30
+ from .servers import safe_path
31
+
32
+ WRITES = set('wax+')
33
+ OPERATIONS = {'remove': 'remoção', 'unlink': 'remoção', 'rmdir': 'remoção', 'rename': 'mudança de nome',
34
+ 'replace': 'mudança de nome', 'mkdir': 'criação de pasta'}
35
+ DEPTH = 12
36
+ # The captor's code, by its real path and by the path it was imported from.
37
+ CODE = tuple(set(folder + os.sep for folder in (CAPTOR, os.path.dirname(os.path.abspath(__file__)))))
38
+
39
+
40
+ def _standard_library():
41
+ paths = sysconfig.get_paths()
42
+ folders = set()
43
+ for key in ('stdlib', 'platstdlib'):
44
+ if paths.get(key):
45
+ folders.add(os.path.realpath(paths[key]) + os.sep)
46
+ folders.add(os.path.abspath(paths[key]) + os.sep)
47
+ return tuple(folders)
48
+
49
+
50
+ class Files(object):
51
+ def __init__(self, root, data):
52
+ self.root = os.path.realpath(root)
53
+ self.project_file = ProjectFiles(self.root)
54
+ self.own = tuple(os.path.realpath(folder) + os.sep for folder in (CAPTOR, data))
55
+ self.stdlib = _standard_library()
56
+
57
+ def caller_is_project(self):
58
+ frame = sys._getframe(3)
59
+ for _ in range(DEPTH):
60
+ if frame is None:
61
+ return False
62
+ filename = frame.f_code.co_filename
63
+ if 'site-packages' in filename or 'dist-packages' in filename:
64
+ return False
65
+ if filename.startswith(CODE) or filename.startswith('<frozen') or filename.startswith(self.stdlib):
66
+ frame = frame.f_back
67
+ continue
68
+ return self.project_file(filename) is not None
69
+ return False
70
+
71
+ def describe(self, target):
72
+ if isinstance(target, int):
73
+ return '(descritor)'
74
+ path = os.fsdecode(os.fspath(target))
75
+ real = os.path.realpath(path)
76
+ if (real + os.sep).startswith(self.own):
77
+ return None
78
+ within = os.path.relpath(real, self.root)
79
+ if within != os.curdir and not within.startswith(os.pardir) and not os.path.isabs(within):
80
+ path = within
81
+ return safe_path(path)
82
+
83
+ def start(self, function, operation, target):
84
+ """end(extra), or None when this operation is not a boundary."""
85
+ if current.get()[1] == NULL or _inside.get() or not self.caller_is_project():
86
+ return None
87
+ try:
88
+ path = self.describe(target)
89
+ except (TypeError, ValueError, OSError):
90
+ return None
91
+ if path is None:
92
+ return None
93
+ return start_boundary({'kind': 'ficheiros', 'provider': 'sistema de ficheiros',
94
+ 'library': 'builtins' if function == 'open' else 'os',
95
+ 'function': function, 'operation': operation, 'path': path})
96
+
97
+
98
+ def _observe(files, function, operation, original):
99
+ @functools.wraps(original)
100
+ def wrapper(target, *args, **kwargs):
101
+ end = files.start(function, operation, target)
102
+ if end is None:
103
+ return original(target, *args, **kwargs)
104
+ try:
105
+ result = original(target, *args, **kwargs)
106
+ except BaseException:
107
+ end({'error': True})
108
+ raise
109
+ end()
110
+ return result
111
+ wrapper.__codetac__ = True
112
+ return wrapper
113
+
114
+
115
+ def install(root, data):
116
+ if getattr(builtins.open, '__codetac__', False):
117
+ return
118
+ files = Files(root, data)
119
+ original_open = builtins.open
120
+ writing = _observe(files, 'open', 'escrita', original_open)
121
+
122
+ @functools.wraps(original_open)
123
+ def codetac_open(file, mode='r', *args, **kwargs):
124
+ if isinstance(mode, str) and WRITES.intersection(mode):
125
+ return writing(file, mode, *args, **kwargs)
126
+ return original_open(file, mode, *args, **kwargs)
127
+
128
+ codetac_open.__codetac__ = True
129
+ builtins.open = codetac_open
130
+ if io.open is original_open:
131
+ io.open = codetac_open
132
+ for name, operation in OPERATIONS.items():
133
+ original = getattr(os, name, None)
134
+ if original is not None and not getattr(original, '__codetac__', False):
135
+ setattr(os, name, _observe(files, name, operation, original))
@@ -0,0 +1,72 @@
1
+ """What only a framework knows, when the dossier would otherwise have a gap.
2
+
3
+ - FastAPI: the body of a request is validated by pydantic-core, compiled
4
+ (Rust), where no Python function runs. The dossier shows an opaque step,
5
+ "validação do pedido (não observável)", around it: its time, whether it
6
+ failed (422), and any validators of the project as its children. The point
7
+ is fastapi.dependencies.utils.request_body_to_args, which solve_dependencies
8
+ looks up in its module at each call.
9
+
10
+ Keep it importable on old Pythons (3.8+); in the minimal mode, without
11
+ functions, nothing is recorded here.
12
+ """
13
+ import functools
14
+ import inspect
15
+
16
+ from .hooks import register
17
+
18
+ VALIDATION = 'validação do pedido (não observável)'
19
+
20
+
21
+ def _monitor():
22
+ import codetac_py
23
+ return codetac_py.monitor if codetac_py.role == 'app' else None
24
+
25
+
26
+ def _patch_fastapi_dependencies(module):
27
+ original = module.request_body_to_args
28
+ if getattr(original, '__codetac_wrapped__', False):
29
+ return
30
+
31
+ def failed(result):
32
+ # (values, errors): errors is the list of validation errors.
33
+ try:
34
+ return bool(result[1])
35
+ except (TypeError, IndexError, KeyError):
36
+ return False
37
+
38
+ if inspect.iscoroutinefunction(original):
39
+ @functools.wraps(original)
40
+ async def request_body_to_args(*args, **kwargs):
41
+ monitor = _monitor()
42
+ if monitor is None:
43
+ return await original(*args, **kwargs)
44
+ end = monitor.opaque(VALIDATION)
45
+ error = True
46
+ try:
47
+ result = await original(*args, **kwargs)
48
+ error = failed(result)
49
+ return result
50
+ finally:
51
+ end(error)
52
+ else:
53
+ @functools.wraps(original)
54
+ def request_body_to_args(*args, **kwargs):
55
+ monitor = _monitor()
56
+ if monitor is None:
57
+ return original(*args, **kwargs)
58
+ end = monitor.opaque(VALIDATION)
59
+ error = True
60
+ try:
61
+ result = original(*args, **kwargs)
62
+ error = failed(result)
63
+ return result
64
+ finally:
65
+ end(error)
66
+
67
+ request_body_to_args.__codetac_wrapped__ = True
68
+ module.request_body_to_args = request_body_to_args
69
+
70
+
71
+ def install():
72
+ register('fastapi.dependencies.utils', _patch_fastapi_dependencies)
@@ -0,0 +1,72 @@
1
+ """Post-import hooks: a module of a library is patched right after it is
2
+ imported, before the code that imported it can bind its names. Used by
3
+ servers.py (servers) and boundaries.py (databases, HTTP...).
4
+
5
+ Keep it importable on old Pythons (3.8+).
6
+ """
7
+ import sys
8
+
9
+ HOOKS = {}
10
+
11
+
12
+ def register(name, hook):
13
+ """Runs hook(module) when `name` is imported, or now if it already was."""
14
+ HOOKS.setdefault(name, []).append(hook)
15
+ module = sys.modules.get(name)
16
+ if module is not None: # imported before the capture started
17
+ _apply(hook, module)
18
+ _install()
19
+
20
+
21
+ def _apply(hook, module):
22
+ try:
23
+ hook(module)
24
+ except Exception as error: # a version we do not know: it runs unobserved
25
+ sys.stderr.write('codeTAC: %s não foi preparado (%s: %s)\n' % (module.__name__, type(error).__name__, error))
26
+
27
+
28
+ class _Loader(object):
29
+ """Runs the real loader, then the hooks. Everything else is the real loader's."""
30
+
31
+ def __init__(self, loader, hooks):
32
+ self._loader = loader
33
+ self._hooks = hooks
34
+
35
+ def __getattr__(self, name):
36
+ return getattr(self._loader, name)
37
+
38
+ def create_module(self, spec):
39
+ return self._loader.create_module(spec)
40
+
41
+ def exec_module(self, module):
42
+ # The module keeps the real loader: nothing else sees this wrapper.
43
+ module.__loader__ = self._loader
44
+ if getattr(module, '__spec__', None) is not None:
45
+ module.__spec__.loader = self._loader
46
+ self._loader.exec_module(module)
47
+ for hook in self._hooks:
48
+ _apply(hook, module)
49
+
50
+
51
+ class _Finder(object):
52
+ def find_spec(self, name, path=None, target=None):
53
+ hooks = HOOKS.get(name)
54
+ if not hooks:
55
+ return None
56
+ for finder in sys.meta_path:
57
+ if finder is self or not hasattr(finder, 'find_spec'):
58
+ continue
59
+ spec = finder.find_spec(name, path, target)
60
+ if spec is not None:
61
+ if spec.loader is not None and hasattr(spec.loader, 'exec_module'):
62
+ spec.loader = _Loader(spec.loader, hooks)
63
+ return spec
64
+ return None
65
+
66
+ def invalidate_caches(self):
67
+ pass
68
+
69
+
70
+ def _install():
71
+ if not any(isinstance(finder, _Finder) for finder in sys.meta_path):
72
+ sys.meta_path.insert(0, _Finder())