retracesoftware-proxy 0.1.5__py3-none-any.whl → 0.2.4__py3-none-any.whl

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.
Files changed (35) hide show
  1. retracesoftware/__main__.py +285 -0
  2. retracesoftware/autoenable.py +53 -0
  3. retracesoftware/config.json +19 -233
  4. retracesoftware/config.yaml +0 -0
  5. retracesoftware/install/config.py +6 -0
  6. retracesoftware/install/edgecases.py +23 -1
  7. retracesoftware/install/patcher.py +98 -513
  8. retracesoftware/install/patchfindspec.py +117 -0
  9. retracesoftware/install/phases.py +338 -0
  10. retracesoftware/install/record.py +111 -40
  11. retracesoftware/install/replace.py +28 -0
  12. retracesoftware/install/replay.py +59 -11
  13. retracesoftware/install/tracer.py +171 -33
  14. retracesoftware/install/typeutils.py +20 -0
  15. retracesoftware/modules.toml +384 -0
  16. retracesoftware/preload.txt +216 -0
  17. retracesoftware/proxy/__init__.py +1 -1
  18. retracesoftware/proxy/globalref.py +31 -0
  19. retracesoftware/proxy/messagestream.py +204 -0
  20. retracesoftware/proxy/proxysystem.py +328 -71
  21. retracesoftware/proxy/proxytype.py +90 -38
  22. retracesoftware/proxy/record.py +109 -119
  23. retracesoftware/proxy/replay.py +94 -188
  24. retracesoftware/proxy/serializer.py +28 -0
  25. retracesoftware/proxy/startthread.py +40 -0
  26. retracesoftware/proxy/stubfactory.py +82 -27
  27. retracesoftware/proxy/thread.py +64 -4
  28. retracesoftware/replay.py +104 -0
  29. retracesoftware/run.py +378 -0
  30. retracesoftware/stackdifference.py +133 -0
  31. {retracesoftware_proxy-0.1.5.dist-info → retracesoftware_proxy-0.2.4.dist-info}/METADATA +2 -1
  32. retracesoftware_proxy-0.2.4.dist-info/RECORD +42 -0
  33. retracesoftware_proxy-0.1.5.dist-info/RECORD +0 -27
  34. {retracesoftware_proxy-0.1.5.dist-info → retracesoftware_proxy-0.2.4.dist-info}/WHEEL +0 -0
  35. {retracesoftware_proxy-0.1.5.dist-info → retracesoftware_proxy-0.2.4.dist-info}/top_level.txt +0 -0
@@ -1,537 +1,122 @@
1
- import inspect
2
- # from retrace_utils import _intercept
3
- # import _intercept
4
- import types
5
- import re
6
- import sys
7
- import pdb
8
- import builtins
9
- import types
10
- import functools
11
- import traceback
12
- import functools
13
- import importlib
14
- import gc
15
- import os
16
- import atexit
17
- import threading
18
-
1
+ import enum
19
2
  from retracesoftware.install.typeutils import modify
20
- # from retracesoftware.proxy.immutabletypes import ImmutableTypes
21
- from retracesoftware.install.record import record_system
22
- from retracesoftware.install.replay import replay_system
23
- from retracesoftware.install.config import load_config
24
-
25
- from functools import wraps
26
-
3
+ from retracesoftware.install.replace import update
4
+ import threading
5
+ import sys
6
+ import retracesoftware.utils as utils
27
7
  import retracesoftware.functional as functional
28
- import retracesoftware_utils as utils
29
-
30
- from retracesoftware.install import edgecases
31
- from functools import partial
32
- # from retrace_utils.intercept import proxy
33
-
34
- # from retrace_utils.intercept.typeutils import *
35
- # from retrace_utils.intercept.proxytype import DynamicProxyFactory, proxytype
36
-
37
- from retracesoftware.install.predicate import PredicateBuilder
38
-
39
- def find_attr(mro, name):
40
- for cls in mro:
41
- if name in cls.__dict__:
42
- return cls.__dict__[name]
43
-
44
- def is_descriptor(obj):
45
- return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')
46
-
47
- # default_exclude = ['__class__', '__getattribute__', '__init_subclass__', '__dict__', '__del__', '__new__']
48
-
49
- def is_function_type(cls):
50
- return issubclass(cls, types.BuiltinFunctionType) or issubclass(cls, types.FunctionType)
51
-
52
- def select_keys(keys, dict):
53
- return {key: dict[key] for key in keys if key in dict}
54
-
55
- def map_values(f, dict):
56
- return {key: f(value) for key,value in dict.items()}
57
-
58
- def common_keys(dict, *dicts):
59
- common_keys = utils.set(dict)
60
- for d in dicts:
61
- common_keys &= d.keys()
62
-
63
- assert isinstance(common_keys, utils.set)
64
-
65
- return common_keys
66
-
67
- def intersection(*dicts):
68
- return { key: tuple(d[key] for d in dicts) for key in common_keys(*dicts) }
69
-
70
- def intersection_apply(f, *dicts):
71
- return map_values(lambda vals: f(*vals), intersection(*dicts))
8
+ import importlib
72
9
 
73
10
  def resolve(path):
74
11
  module, sep, name = path.rpartition('.')
75
- if module == None: module = 'builtins'
76
-
77
- return getattr(importlib.import_module(module), name)
78
-
79
- # def sync(spec, module_dict):
80
- # for name,properties in spec.items():
81
-
82
- # cls = module_dict[name]
83
-
84
- # orig_init = cls.__init__
85
-
86
- # def __init__(inst, *args, **kwargs):
87
- # orig_init(inst, *args, **kwargs)
88
- # for prop in properties:
89
- # self.system.add_sync(getattr(inst, prop))
90
12
 
91
- def container_replace(container, old, new):
92
- if isinstance(container, dict):
93
- if old in container:
94
- elem = container.pop(old)
95
- container[new] = elem
96
- container_replace(container, old, new)
97
- else:
98
- for key,value in container.items():
99
- if key != '__retrace_unproxied__' and value is old:
100
- container[key] = new
101
- return True
102
- elif isinstance(container, list):
103
- for i,value in enumerate(container):
104
- if value is old:
105
- container[i] = new
106
- return True
107
- elif isinstance(container, set):
108
- container.remove(old)
109
- container.add(new)
110
- return True
111
- else:
112
- return False
113
-
114
- def phase(func):
115
- func.is_phase = True # add marker attribute
116
- return func
117
-
118
- def patch(func):
119
- @wraps(func)
120
- def wrapper(self, spec, mod_dict):
121
- if isinstance(spec, str):
122
- return wrapper(self, [spec], mod_dict)
123
- elif isinstance(spec, list):
124
- return {name: func(self, mod_dict[name]) for name in spec if name in mod_dict}
125
- elif isinstance(spec, dict):
126
- return {name: func(self, mod_dict[name], value) for name, value in spec.items() if name in mod_dict}
127
- else:
128
- raise Exception('TODO')
129
-
130
- wrapper.is_phase = True
131
- return wrapper
132
-
133
- def superdict(cls):
134
- result = {}
135
- for cls in list(reversed(cls.__mro__))[1:]:
136
- result.update(cls.__dict__)
13
+ if module is None:
14
+ module = 'builtins'
137
15
 
138
- return result
139
-
140
- def is_method_descriptor(obj):
141
- return isinstance(obj, types.FunctionType) or \
142
- (isinstance(obj, (types.WrapperDescriptorType, types.MethodDescriptorType)) and obj.__objclass__ != object)
143
-
144
- def wrap_method_descriptors(wrapper, prefix, base):
145
- slots = {"__slots__": () }
146
-
147
- extended = type(f'{prefix}.{base.__module__}.{base.__name__}', (base,), {"__slots__": () })
148
-
149
- blacklist = ['__getattribute__', '__hash__', '__del__']
150
-
151
- for name,value in superdict(base).items():
152
- if name not in blacklist:
153
- if is_method_descriptor(value):
154
- setattr(extended, name, wrapper(value))
155
-
156
- return extended
157
-
158
- class Patcher:
159
-
160
- def __init__(self, thread_state, config, system,
161
- immutable_types,
162
- on_function_proxy = None,
163
- debug_level = 0,
164
- post_commit = None):
165
-
166
- # validate(config)
167
- # system.set_thread_id(0)
168
- # utils.set_thread_id(0)
169
- self.thread_counter = system.sync(utils.counter(1))
170
- # self.set_thread_number = set_thread_number
171
-
172
- self.thread_state = thread_state
173
- self.debug_level = debug_level
174
- self.on_function_proxy = on_function_proxy
175
- self.modules = config['modules']
176
- self.immutable_types_set = immutable_types
177
- self.predicate = PredicateBuilder()
178
- self.system = system
179
- self.type_attribute_filter = self.predicate(config['type_attribute_filter'])
180
- self.post_commit = post_commit
181
- self.exclude_paths = [re.compile(s) for s in config.get('exclude_paths', [])]
182
-
183
- def is_phase(name): return getattr(getattr(self, name, None), "is_phase", False)
184
-
185
- self.phases = [(name, getattr(self, name)) for name in Patcher.__dict__.keys() if is_phase(name)]
186
-
187
- def log(self, *args):
188
- self.system.tracer.log(*args)
189
-
190
- def path_predicate(self, path):
191
- for exclude in self.exclude_paths:
192
- if exclude.match(str(path)) is not None:
193
- # print(f'in path_predicate, excluding {path}')
194
- return False
195
- return True
196
-
197
- @property
198
- def disable(self):
199
- return self.thread_state.select('disabled')
200
-
201
- def proxyable(self, name, obj):
202
- if name.startswith('__') and name.endswith('__'):
203
- return False
204
-
205
- if isinstance(obj, (str, int, dict, list, tuple)):
206
- return False
207
-
208
- if isinstance(obj, type):
209
- return not issubclass(obj, BaseException) and obj not in self.immutable_types_set
210
- else:
211
- return type(obj) not in self.immutable_types_set
212
-
213
- @phase
214
- def immutable_types(self, spec, mod_dict):
215
- if isinstance(spec, str):
216
- return self.immutable_types([spec], mod_dict)
217
-
218
- for name in spec:
219
- if name in mod_dict:
220
- if isinstance(mod_dict[name], type):
221
- assert isinstance(mod_dict[name], type)
222
- self.immutable_types_set.add(mod_dict[name])
223
- else:
224
- raise Exception(f'Tried to add "{name}" - {mod_dict[name]} which isn\'t a type to immutable')
225
-
226
- @patch
227
- def proxy(self, value):
228
- return self.system(value)
229
-
230
- @phase
231
- def proxy_all_except(self, spec, mod_dict):
232
-
233
- all_except = set(spec)
234
-
235
- def proxyable(name, value):
236
- return name not in all_except and self.proxyable(name, value)
237
-
238
- return {key: self.system(value) for key,value in mod_dict.items() if proxyable(key, value)}
239
-
240
- @phase
241
- def proxy_type_attributes(self, spec, mod_dict):
242
- for classname, attributes in spec.items():
243
- if classname in mod_dict:
244
- cls = mod_dict[classname]
245
- if isinstance(cls, type):
246
- for name in attributes:
247
- attr = find_attr(cls.__mro__, name)
248
- if attr is not None and (callable(attr) or is_descriptor(attr)):
249
- proxied = self.system(attr)
250
- # proxied = self.proxy(attr)
251
-
252
- with modify(cls):
253
- setattr(cls, name, proxied)
254
- else:
255
- raise Exception(f"Cannot patch attributes for {cls.__module__}.{cls.__name__} as object is: {cls} and not a type")
256
-
257
- @phase
258
- def replace(self, spec, mod_dict):
259
- return {key: resolve(value) for key,value in spec.items()}
260
-
261
- @patch
262
- def patch_start_new_thread(self, value):
263
- def start_new_thread(self, function, *args):
264
- # synchronized, replay shoudl yield correct number
265
- thread_id = self.thread_counter()
266
-
267
- def threadrunner(*args, **kwargs):
268
- nonlocal thread_id
269
- self.system.set_thread_id(thread_id)
270
-
271
- with self.thread_state.select('internal'):
272
- # if self.tracing:
273
- # FrameTracer.install(self.thread_state.dispatch(noop, internal = self.checkpoint))
274
- return function(*args, **kwargs)
275
-
276
- return value(threadrunner, *args)
277
-
278
- return self.thread_state.dispatch(value, internal = start_new_thread)
279
-
280
- @phase
281
- def wrappers(self, spec, mod_dict):
282
- return intersection_apply(lambda path, value: resolve(path)(value), spec, mod_dict)
283
-
284
- @patch
285
- def patch_exec(self, exec):
286
- def is_module_exec(source):
287
- return isinstance(source, types.CodeType) and \
288
- source.co_name == '<module>' and inspect.getmodule(source)
289
-
290
- def exec_wrapper(source, *args,**kwargs):
291
- if self.thread_state.value != 'boostrap' and is_module_exec(source):
292
- with self.thread_state.select('disabled'):
293
- module = inspect.getmodule(source)
294
- with self.thread_state.select('internal'):
295
- result = exec(source, *args, **kwargs)
296
-
297
- self(module)
298
-
299
- return result
300
- else:
301
- return exec(source, *args,**kwargs)
302
-
303
- return exec_wrapper
304
-
305
- @patch
306
- def sync_types(self, value):
307
- return wrap_method_descriptors(self.system.sync, "retrace", value)
308
-
309
- @phase
310
- def with_state_recursive(self, spec, mod_dict):
311
-
312
- updates = {}
313
-
314
- for state,elems in spec.items():
16
+ return getattr(importlib.import_module(module), name)
315
17
 
316
- def wrap(obj):
317
- return functional.recursive_wrap(
318
- callable,
319
- lambda f: self.thread_state.wrap(state, f),
320
- obj)
321
-
322
- updates.update(map_values(wrap, select_keys(elems, mod_dict)))
18
+ def replace(replacements, coll):
19
+ return map(lambda x: replacements.get(x, x), coll)
323
20
 
324
- return updates
325
-
326
- @phase
327
- def with_state(self, spec, mod_dict):
21
+ def patch_class(transforms, cls):
22
+ with modify(cls):
23
+ for attr,transform in transforms.items():
24
+ utils.update(cls, attr, resolve(transform))
328
25
 
329
- updates = {}
26
+ return cls
330
27
 
331
- for state,elems in spec.items():
28
+ class PerThread(threading.local):
29
+ def __init__(self):
30
+ self.internal = utils.counter()
31
+ self.external = utils.counter()
332
32
 
333
- def wrap(obj): return self.thread_state.wrap(desired_state = state, function = obj)
33
+ def create_patcher(system):
334
34
 
335
- updates.update(map_values(wrap, select_keys(elems, mod_dict)))
336
-
337
- return updates
35
+ patcher = {}
338
36
 
339
- @patch
340
- def patch_extension_exec(self, exec):
341
-
342
- def wrapper(module):
343
- with self.thread_state.select('internal'):
344
- res = exec(module)
37
+ def foreach(func): return lambda config: {name: func for name in config}
38
+ def selector(func): return lambda config: {name: functional.partial(func, value) for name, value in config.items()}
345
39
 
346
- self(module)
347
- return res
40
+ def simple_patcher(func): return foreach(functional.side_effect(func))
348
41
 
349
- return wrapper
42
+ def type_attributes(transforms, cls):
43
+ with modify(cls):
44
+ for action, attrs in transforms.items():
45
+ for attr,func in patcher[action](attrs).items():
46
+ utils.update(cls, attr, func)
47
+ return cls
350
48
 
351
- @patch
352
- def path_predicates(self, func, param):
353
- signature = inspect.signature(func).parameters
49
+ def bind(obj):
50
+ if issubclass(obj, enum.Enum):
51
+ for member in obj:
52
+ system.bind(member)
53
+ else:
54
+ system.bind(obj)
354
55
 
355
- try:
356
- index = list(signature.keys()).index(param)
357
- except ValueError:
358
- print(f'parameter {param} not in: {signature.keys()} {type(func)} {func}')
359
- raise
360
-
361
- param = functional.param(name = param, index = index)
56
+ def add_immutable_type(obj):
57
+ if not isinstance(obj, type):
58
+ raise Exception("TODO")
59
+ system.immutable_types.add(obj)
60
+ return obj
362
61
 
363
- assert callable(param)
62
+ per_thread = PerThread()
364
63
 
365
- return functional.if_then_else(
366
- test = functional.sequence(param, self.path_predicate),
367
- then = func,
368
- otherwise = self.thread_state.wrap('disabled', func))
369
-
370
- @phase
371
- def wrap(self, spec, mod_dict):
372
- updates = {}
373
-
374
- for path, wrapper_name in spec.items():
375
-
376
- parts = path.split('.')
377
- name = parts[0]
378
- if name in mod_dict:
379
- value = mod_dict[name]
380
- assert not isinstance(value, utils.wrapped_function), f"value for key: {name} is already wrapped"
381
- if len(parts) == 1:
382
- updates[name] = resolve(wrapper_name)(value)
383
- elif len(parts) == 2:
384
- member = getattr(value, parts[1], None)
385
- if member:
386
- new_value = resolve(wrapper_name)(member)
387
- setattr(value, parts[1], new_value)
388
- else:
389
- raise Exception('TODO')
390
-
391
- return updates
392
-
393
- def updates(self, spec, mod_dict):
394
- updates = {}
395
-
396
- for phase,func in self.phases:
397
- if phase in spec:
398
- self.log('install.module.phase', phase)
399
-
400
- phase_updates = func(spec[phase], mod_dict | updates)
401
-
402
- if phase_updates:
403
- self.log('install.module.phase.results', list(phase_updates.keys()))
404
- updates |= phase_updates
405
-
406
- return updates
407
-
408
- def configs(self, module):
409
- for name,value in sys.modules.items():
410
- if value is module and name in self.modules:
411
- yield name
412
-
413
- def patch_module_with_name(self, mod_name, module):
414
- with self.disable:
415
- self.log('install.module', mod_name)
416
-
417
- # self.system.log(f'patching module: {mod_name}')
418
-
419
- spec = self.modules.get(mod_name)
420
-
421
- updates = self.updates(spec = spec, mod_dict = module.__dict__)
422
-
423
- originals = select_keys(updates.keys(), module.__dict__)
424
-
425
- module.__dict__.update(updates)
426
-
427
- for name, value in originals.items():
428
- for ref in gc.get_referrers(value):
429
- if ref is not originals:
430
- container_replace(container = ref, old = value, new = updates[name])
64
+ hashfunc = system.thread_state.dispatch(
65
+ functional.constantly(None),
66
+ internal = functional.repeatedly(functional.partial(getattr, per_thread, 'internal')),
67
+ external = functional.repeatedly(functional.partial(getattr, per_thread, 'external')))
431
68
 
432
- module.__retrace__ = originals
69
+ def patch_hash(obj):
70
+ if not isinstance(obj, type):
71
+ raise Exception("TODO")
433
72
 
434
- if self.post_commit:
435
- self.post_commit(mod_name, updates)
436
-
437
- def __call__(self, module):
438
-
439
- if not hasattr(module, '__retrace__'):
440
- configs = list(self.configs(module))
441
-
442
- if len(configs) > 0:
443
- if len(configs) > 1:
444
- raise Exception(f'TODO')
445
- else:
446
- try:
447
- self.patch_module_with_name(configs[0], module)
448
- except Exception as error:
449
- raise Exception(f'Error patching module: {configs[0]}') from error
450
-
451
- return module
452
-
453
- def env_truthy(key, default=False):
454
- value = os.getenv(key)
455
- if value is None:
456
- return default
457
- return value.strip().lower() in ("1", "true", "yes", "on")
458
-
459
- class ImmutableTypes(set):
460
- def __init__(self, *args, **kwargs):
461
- super().__init__(*args, **kwargs)
462
-
463
- def __contains__(self, item):
464
- assert isinstance(item, type)
465
-
466
- if super().__contains__(item):
467
- return True
468
-
469
- for elem in self:
470
- if issubclass(item, elem):
471
- self.add(item)
472
- return True
473
-
474
- return False
475
-
476
- def install(mode):
477
-
478
- create_system = None
479
-
480
- if mode == 'record':
481
- create_system = record_system
482
- elif mode == 'replay':
483
- create_system = replay_system
484
- else:
485
- raise Exception(f'mode: {mode} unsupported')
73
+ utils.patch_hash(cls = obj, hashfunc = hashfunc)
74
+ return obj
486
75
 
487
- config = load_config('config.json')
488
-
489
- states = [x for x in config['states'] if isinstance(x, str)]
490
-
491
- thread_state = utils.ThreadState(*states)
492
-
493
- immutable_types = ImmutableTypes()
494
-
495
- if 'RETRACE_RECORDING_PATH' in os.environ:
496
- config['recording_path'] = os.environ['RETRACE_RECORDING_PATH']
497
-
498
- # immutable_types = ImmutableTypes(config)
499
- config['verbose'] = env_truthy('RETRACE_VERBOSE')
500
-
501
- system = create_system(thread_state = thread_state,
502
- immutable_types = immutable_types,
503
- config = config)
504
-
505
- os.register_at_fork(before = system.before_fork,
506
- after_in_parent = system.after_fork_in_parent,
507
- after_in_child = system.after_fork_in_child)
76
+ patcher.update({
77
+ 'type_attributes': selector(type_attributes),
78
+ 'patch_class': selector(patch_class),
79
+ 'disable': foreach(system.disable_for),
80
+ 'patch_types': simple_patcher(system.patch_type),
81
+ 'proxy': foreach(system),
82
+ 'bind': simple_patcher(bind),
83
+ 'wrap': lambda config: {name: resolve(action) for name,action in config.items() },
84
+ 'immutable': simple_patcher(add_immutable_type),
85
+ 'patch_hash': simple_patcher(patch_hash),
86
+ })
87
+ return patcher
88
+
89
+ def patch_namespace(patcher, config, namespace, update_refs):
90
+ for phase_name, phase_config in config.items():
91
+ if phase_name in patcher:
92
+ for name,func in patcher[phase_name](phase_config).items():
93
+ if name in namespace:
94
+ # print(f"patching: {name}")
95
+
96
+ value = namespace[name]
97
+ new_value = func(value)
98
+
99
+ if value is not new_value:
100
+ namespace[name] = new_value
101
+
102
+ if update_refs:
103
+ update(value, new_value)
104
+ else:
105
+ print(phase_name)
106
+ utils.sigtrap('FOO1')
508
107
 
509
- patcher = Patcher(thread_state = thread_state,
510
- config = config,
511
- system = system,
512
- post_commit = getattr(system, 'on_patched', None),
513
- immutable_types = immutable_types)
514
-
515
- def at_exit(): thread_state.value = 'disabled'
516
-
517
- with thread_state.select('internal'):
518
-
519
- atexit.register(lambda: at_exit)
520
-
521
- for module in sys.modules.values():
522
- patcher(module)
523
-
524
- for library in config.get('preload', []):
525
- importlib.import_module(library)
526
-
527
- # # if env_truthy('RETRACE_TRACE_CALLS'):
528
- # # trace_calls(system = retracesystem)
529
-
530
- thread_state.value = 'internal'
531
-
532
- # import threading
533
- # threading.current_thread().retrace = system
534
-
535
- threading.current_thread().__retrace__ = system
536
-
537
- return system
108
+ def patch_module(patcher, config, namespace, update_refs):
109
+ if '__name__' in namespace:
110
+ name = namespace['__name__']
111
+ if name in config:
112
+ patch_namespace(patcher, config = config[name], namespace=namespace, update_refs=update_refs)
113
+
114
+ def patch_imported_module(patcher, checkpoint, config, namespace, update_refs):
115
+ if '__name__' in namespace:
116
+ name = namespace['__name__']
117
+ checkpoint(f"importing module: {name}")
118
+
119
+ if name in config:
120
+ # print(f"Patching imported module: {name}")
121
+ # checkpoint(f"Patching imported module: {name}")
122
+ patch_namespace(patcher, config = config[name], namespace=namespace, update_refs=update_refs)