retracesoftware-proxy 0.1.22__py3-none-any.whl → 0.2.1__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.
@@ -0,0 +1,117 @@
1
+ from functools import wraps
2
+ import os
3
+ from pathlib import Path
4
+ import shutil
5
+ import sys
6
+
7
+ # Set to track copied modules
8
+ copied_modules = set()
9
+
10
+ def get_relative_path(module_path: Path, sys_path: list) -> Path:
11
+ """Compute the relative path of module_path relative to sys.path entries."""
12
+ module_path = module_path.resolve()
13
+ for base in sys_path:
14
+ base_path = Path(base).resolve()
15
+ try:
16
+ if module_path.is_relative_to(base_path):
17
+ return module_path.relative_to(base_path)
18
+ except ValueError:
19
+ continue
20
+ return Path(module_path.name)
21
+
22
+ class patch_find_spec:
23
+ def __init__(self, cwd, run_path, python_path):
24
+ self.cwd = cwd
25
+ self.run_path = run_path
26
+ self.python_path = python_path
27
+
28
+ def __call__(self, spec):
29
+ if spec is not None and spec.origin and spec.origin != "built-in" and os.path.isfile(spec.origin):
30
+ module_name = spec.name
31
+ if module_name not in copied_modules:
32
+ module_path = Path(spec.origin)
33
+
34
+ dest_path = None
35
+
36
+ if module_path.is_relative_to(self.cwd):
37
+ dest_path = self.run_path / module_path.relative_to(self.cwd)
38
+ elif self.python_path:
39
+ relative_path = get_relative_path(module_path, sys.path)
40
+ dest_path = self.python_path / relative_path
41
+
42
+ if dest_path:
43
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
44
+ shutil.copy2(module_path, dest_path)
45
+ copied_modules.add(module_name)
46
+
47
+ # def __call__(self, fullname, path, target=None):
48
+ # spec = self.original_find_spec(fullname, path, target)
49
+
50
+ # if spec is not None and spec.origin and spec.origin != "built-in" and os.path.isfile(spec.origin):
51
+ # module_name = spec.name
52
+ # if module_name not in copied_modules:
53
+ # module_path = Path(spec.origin)
54
+
55
+ # dest_path = None
56
+
57
+ # if module_path.is_relative_to(self.cwd):
58
+ # dest_path = self.run_path / module_path.relative_to(self.cwd)
59
+ # elif self.python_path:
60
+ # relative_path = get_relative_path(module_path, sys.path)
61
+ # dest_path = self.python_path / relative_path
62
+
63
+ # if dest_path:
64
+ # dest_path.parent.mkdir(parents=True, exist_ok=True)
65
+ # shutil.copy2(module_path, dest_path)
66
+ # copied_modules.add(module_name)
67
+
68
+ # return spec
69
+
70
+ # def patch_find_spec(cwd, run_path, python_path, original_find_spec):
71
+ # """Create a patched version of find_spec that copies .py files."""
72
+ # @wraps(original_find_spec)
73
+ # def patched_find_spec(fullname, path, target=None):
74
+ # spec = original_find_spec(fullname, path, target)
75
+
76
+ # if spec is not None and spec.origin and spec.origin != "built-in" and os.path.isfile(spec.origin):
77
+ # module_name = spec.name
78
+ # if module_name not in copied_modules:
79
+ # module_path = Path(spec.origin)
80
+
81
+ # dest_path = None
82
+
83
+ # if module_path.is_relative_to(cwd):
84
+ # dest_path = run_path / module_path.relative_to(cwd)
85
+ # elif python_path:
86
+ # relative_path = get_relative_path(module_path, sys.path)
87
+ # dest_path = python_path / relative_path
88
+
89
+ # if dest_path:
90
+ # dest_path.parent.mkdir(parents=True, exist_ok=True)
91
+ # shutil.copy2(module_path, dest_path)
92
+ # copied_modules.add(module_name)
93
+
94
+ # # elif python_path:
95
+ # # relative_path = get_relative_path(module_path, sys.path)
96
+ # # dest_path = python_path / relative_path
97
+ # # dest_path.parent.mkdir(parents=True, exist_ok=True)
98
+
99
+ # # shutil.copy2(module_path, dest_path)
100
+ # # copied_modules.add(module_name)
101
+
102
+ # # if module_path.suffix == '.py':
103
+ # # elif module_path.suffix == '.py':
104
+ # # relative_path = get_relative_path(module_path, sys.path)
105
+ # # dest_path = path / relative_path
106
+ # # dest_path.parent.mkdir(parents=True, exist_ok=True)
107
+ # # try:
108
+ # # shutil.copy2(module_path, dest_path)
109
+ # # print(f"Copied {module_path} to {dest_path} (relative: {relative_path})")
110
+ # # copied_modules.add(module_name)
111
+ # # except Exception as e:
112
+ # # print(f"Failed to copy {module_path}: {e}")
113
+ # # else:
114
+ # # print(f"Skipped copying {module_path} (not a .py file)")
115
+
116
+ # return spec
117
+ # return patched_find_spec
@@ -0,0 +1,338 @@
1
+ import retracesoftware.functional as functional
2
+ import retracesoftware.utils as utils
3
+ from retracesoftware.proxy.thread import start_new_thread_wrapper
4
+ import threading
5
+ import importlib
6
+ from retracesoftware.install.typeutils import modify, WithFlags, WithoutFlags
7
+ import enum
8
+
9
+ from retracesoftware.install.typeutils import modify
10
+ from abc import ABC, abstractmethod
11
+ from typing import List, Dict, Any, Callable
12
+ import functools
13
+
14
+ def simple_phase(names, func):
15
+ """
16
+ Simplest phase type
17
+ """
18
+ return {name: func for name in names}
19
+
20
+ def simple_patch_phase(names, func):
21
+ """
22
+ Simplest phase type
23
+ """
24
+ def side_effect(obj):
25
+ func(obj)
26
+ return obj
27
+
28
+ return {name: side_effect for name in names}
29
+
30
+ Transform = Callable[[Any], Any]
31
+ Config = Any
32
+
33
+ Updater = Dict[str, Transform]
34
+
35
+ Phase = Callable[[Config], Updater]
36
+
37
+ Patcher = Dict[str, Phase]
38
+
39
+ def update_class(cls : type, actions : Updater):
40
+ ...
41
+
42
+ def type_attributes(patcher, config):
43
+ result = {}
44
+
45
+ for typename, action_attrs in config.items():
46
+
47
+ def patch_type(cls):
48
+ with modify(cls):
49
+ for action, attrs in action_attrs.items():
50
+ update_class(cls, patcher[action](attrs))
51
+ return cls
52
+
53
+ result[typename] = patch_type
54
+
55
+ return result
56
+
57
+ def create_patcher(system) -> Patcher:
58
+ patcher = {}
59
+
60
+ def simple_patcher(func): lambda config: {name: func for name in config}
61
+
62
+ patcher.update({
63
+ 'type_attributes': functools.partial(type_attributes, patcher),
64
+ 'disable': simple_patcher(system.disable),
65
+ 'patch_types': simple_patcher(system.patch_type),
66
+
67
+ })
68
+ return patcher
69
+
70
+ class TypeAttributesPhase(Phase):
71
+
72
+ def __init__(self, patcher):
73
+ super().__init__('type_attributes')
74
+ self.patcher = patcher
75
+
76
+ def patch_with_config(self, config, cls):
77
+ # print(f'TypeAttributesPhase: {config} {cls}')
78
+ if not isinstance(cls, type):
79
+ raise Exception("TODO")
80
+
81
+ with modify(cls):
82
+ for phase_name,values in config.items():
83
+ for attribute_name,func in self.patcher.find_phase(phase_name)(values).items():
84
+ utils.update(cls, attribute_name, func)
85
+
86
+ return cls
87
+
88
+ class Phase:
89
+ def __init__(self, system):
90
+ self.system = system
91
+
92
+ def simple_phase(func): return lambda names: {name: func for name in names}
93
+
94
+ self.phases = {
95
+ 'disable': simple_phase(system.disable),
96
+ 'type_attributes': lambda config: simple_phase(config, system.disable)
97
+ }
98
+
99
+ def type_attributes(self, config, cls):
100
+ with modify(cls):
101
+ for action_name, attribute_names in config.items():
102
+ for attribute_name, func in self(action_name, attribute_names).items():
103
+ utils.update(cls, attribute_name, func)
104
+
105
+ return cls
106
+
107
+ def __call__(self, name, config):
108
+ match name:
109
+ case 'disable':
110
+ return {name: self.system.disable for name in config}
111
+ case 'type_attributes':
112
+ return {cls: partial(self.type_attributes, actions) for cls, actions in config.items()}
113
+
114
+
115
+ phases = {
116
+ 'disable': lambda system, config: simple_phase(config, system.disable),
117
+ 'type_attributes': lambda system, config: simple_phase(config, system.disable)
118
+ }
119
+
120
+ class Phase(ABC):
121
+ """
122
+ A phase is function when called with system and config yields map of name->func
123
+ A patch phase
124
+ A phase has... keys... and action
125
+ """
126
+ def __init__(self, name):
127
+ self.name = name
128
+
129
+ def patch(self, obj):
130
+ return obj
131
+
132
+ def patch_with_config(self, config, obj):
133
+ return obj
134
+
135
+ @abstractmethod
136
+ def apply(self, name : str, value : Any) -> Any:
137
+ pass
138
+
139
+ @abstractmethod
140
+ def __call__(self, namespace : Dict[str, Any]) -> None:
141
+ for name, value in namespace.items():
142
+ if
143
+
144
+ def __call__(self, config):
145
+ if isinstance(config, str):
146
+ return {config: self.patch}
147
+ elif isinstance(config, list):
148
+ return {key: self.patch for key in config}
149
+ elif isinstance(config, dict):
150
+ return {key: lambda obj: self.patch_with_config(value, obj) for key,value in config.items()}
151
+ else:
152
+ raise Exception(f'Unhandled config type: {config}')
153
+
154
+
155
+
156
+ class Proxy(Phase):
157
+
158
+ def proxy()
159
+ def __call__(self, name : str, value : Any) -> None:
160
+ if name in self.targets
161
+
162
+
163
+ class Patch(Phase):
164
+
165
+ def __init__(self, targets, action):
166
+ self.targets = targets
167
+
168
+ def select(self, name : str) -> bool:
169
+ pass
170
+
171
+ def __call__(self, name : str, value : Any) -> None:
172
+ if name in self.targets
173
+
174
+ class SimplePhase(Phase):
175
+ def __init__(self, name, patch):
176
+ super().__init__(name)
177
+ self.patch = patch
178
+
179
+ class DisablePhase(Phase):
180
+ def __init__(self, thread_state):
181
+ super().__init__('disable')
182
+ self.thread_state = thread_state
183
+
184
+ def patch(self, obj):
185
+ print(f'Disabling: {obj}')
186
+ return self.thread_state.wrap('disabled', obj)
187
+
188
+ class TryPatchPhase(Phase):
189
+ def __init__(self, patcher):
190
+ super().__init__('try_patch')
191
+ self.patcher = patcher
192
+
193
+ def patch(self, obj):
194
+ try:
195
+ return self.patcher(obj)
196
+ except:
197
+ return obj
198
+
199
+ class PatchTypesPhase(Phase):
200
+ def __init__(self, patcher):
201
+ super().__init__('patch_types')
202
+ self.patcher = patcher
203
+
204
+ def patch(self, obj):
205
+ if not isinstance(obj, type):
206
+ raise Exception("TODO")
207
+
208
+ self.patcher(obj)
209
+ return obj
210
+
211
+ class BindPhase(Phase):
212
+ def __init__(self, bind):
213
+ super().__init__('bind')
214
+ self.bind = bind
215
+
216
+ def patch(self, obj):
217
+ # print(f'In BindPhase: {obj} {isinstance(obj, type)}')
218
+ if issubclass(obj, enum.Enum):
219
+ for member in obj:
220
+ # print(f'binding member: {member}')
221
+ # utils.sigtrap(member)
222
+ self.bind(member)
223
+ else:
224
+ self.bind(obj)
225
+
226
+ return obj
227
+
228
+ class ImmutableTypePhase(Phase):
229
+
230
+ def __init__(self, types):
231
+ super().__init__('immutable')
232
+ self.types = types
233
+
234
+ def patch(self, obj):
235
+ if not isinstance(obj, type):
236
+ raise Exception("TODO")
237
+
238
+ self.types.add(obj)
239
+ return obj
240
+
241
+ class PatchThreadPhase(Phase):
242
+ def __init__(self, thread_state, on_exit):
243
+ super().__init__('patch_start_new_thread')
244
+ self.thread_state = thread_state
245
+ self.on_exit = on_exit
246
+
247
+ def patch(self, obj):
248
+ return start_new_thread_wrapper(thread_state = self.thread_state,
249
+ on_exit = self.on_exit,
250
+ start_new_thread = obj)
251
+
252
+ def resolve(path):
253
+ module, sep, name = path.rpartition('.')
254
+ if module == None: module = 'builtins'
255
+
256
+ return getattr(importlib.import_module(module), name)
257
+
258
+ class WrapPhase(Phase):
259
+ def __init__(self):
260
+ super().__init__('wrap')
261
+
262
+ def patch_with_config(self, wrapper, obj):
263
+ return resolve(wrapper)(obj)
264
+
265
+ class PatchClassPhase(Phase):
266
+ def __init__(self):
267
+ super().__init__('patch_class')
268
+
269
+ def patch_with_config(self, config, cls):
270
+
271
+ patchers = utils.map_values(resolve, config)
272
+
273
+ assert cls is not None
274
+
275
+ with WithoutFlags(cls, "Py_TPFLAGS_IMMUTABLETYPE"):
276
+ for name,func in patchers.items():
277
+ utils.update(cls, name, func)
278
+
279
+ return cls
280
+
281
+ class ProxyWrapPhase(Phase):
282
+ def __init__(self):
283
+ super().__init__('wrap_proxy')
284
+
285
+ def patch_with_config(self, config, cls):
286
+
287
+ patchers = utils.map_values(resolve, config)
288
+
289
+ def patch(proxytype):
290
+ for name,func in patchers.items():
291
+ utils.update(proxytype, name, func)
292
+
293
+ cls.__retrace_patch_proxy__ = patch
294
+
295
+ return cls
296
+ # return resolve(wrapper)(obj)
297
+
298
+ class TypeAttributesPhase(Phase):
299
+
300
+ def __init__(self, patcher):
301
+ super().__init__('type_attributes')
302
+ self.patcher = patcher
303
+
304
+ def patch_with_config(self, config, cls):
305
+ # print(f'TypeAttributesPhase: {config} {cls}')
306
+ if not isinstance(cls, type):
307
+ raise Exception("TODO")
308
+
309
+ with modify(cls):
310
+ for phase_name,values in config.items():
311
+ for attribute_name,func in self.patcher.find_phase(phase_name)(values).items():
312
+ utils.update(cls, attribute_name, func)
313
+
314
+ return cls
315
+
316
+ class PerThread(threading.local):
317
+ def __init__(self):
318
+ self.internal = utils.counter()
319
+ self.external = utils.counter()
320
+
321
+ class PatchHashPhase(Phase):
322
+
323
+ def __init__(self, thread_state):
324
+ super().__init__('patch_hash')
325
+
326
+ per_thread = PerThread()
327
+
328
+ self.hashfunc = thread_state.dispatch(
329
+ functional.constantly(None),
330
+ internal = functional.repeatedly(functional.partial(getattr, per_thread, 'internal')),
331
+ external = functional.repeatedly(functional.partial(getattr, per_thread, 'external')))
332
+
333
+ def patch(self, obj):
334
+ if not isinstance(obj, type):
335
+ raise Exception("TODO")
336
+
337
+ utils.patch_hash(cls = obj, hashfunc = self.hashfunc)
338
+ return obj
@@ -6,23 +6,34 @@ import retracesoftware.stream as stream
6
6
 
7
7
  from retracesoftware.install.tracer import Tracer
8
8
  from retracesoftware.install import globals
9
+ from retracesoftware.install.config import env_truthy
10
+ from retracesoftware.install.patchfindspec import patch_find_spec
9
11
 
10
12
  import os
11
13
  import sys
12
14
  from datetime import datetime
13
15
  import json
14
16
  from pathlib import Path
17
+ import shutil
15
18
 
16
19
  # class ThreadSwitch:
17
20
  # def __init__(self, id):
18
21
  # self.id = id
19
-
22
+
20
23
  # def __repr__(self):
21
24
  # return f'ThreadSwitch<{self.id}>'
22
25
 
23
26
  # def __str__(self):
24
27
  # return f'ThreadSwitch<{self.id}>'
25
28
 
29
+ def code_workspace():
30
+ return {
31
+ 'folders': [
32
+ {'path': '../..', 'name': 'Application'},
33
+ {'path': '.', 'name': 'Recording'}
34
+ ]
35
+ }
36
+
26
37
  def write_files(recording_path):
27
38
  with open(recording_path / 'env', 'w') as f:
28
39
  json.dump(dict(os.environ), f, indent=2)
@@ -36,6 +47,9 @@ def write_files(recording_path):
36
47
  with open(recording_path / 'cmd', 'w') as f:
37
48
  json.dump(sys.orig_argv, f, indent=2)
38
49
 
50
+ with open(recording_path / 'replay.code-workspace', 'w') as f:
51
+ json.dump(code_workspace(), f, indent=2)
52
+
39
53
  def create_recording_path(path):
40
54
  expanded = datetime.now().strftime(path.format(pid = os.getpid()))
41
55
  os.environ['RETRACE_RECORDING_PATH'] = expanded
@@ -55,10 +69,14 @@ def merge_config(base, override):
55
69
  return override
56
70
 
57
71
 
72
+
73
+ def dump_as_json(path, obj):
74
+ with open(path, 'w') as f:
75
+ json.dump(obj, f, indent=2)
76
+
58
77
  def record_system(thread_state, immutable_types, config):
59
78
 
60
79
  recording_path = create_recording_path(config['recording_path'])
61
-
62
80
  recording_path.mkdir(parents=True, exist_ok=True)
63
81
 
64
82
  globals.recording_path = globals.RecordingPath(recording_path)
@@ -67,48 +85,90 @@ def record_system(thread_state, immutable_types, config):
67
85
 
68
86
  tracing_config = config['tracing_levels'].get(tracing_level(config), {})
69
87
 
70
- with open(recording_path / 'tracing_config.json', 'w') as f:
71
- json.dump(tracing_config, f, indent=2)
88
+ dump_as_json(path = recording_path / 'tracing_config.json', obj = tracing_config)
72
89
 
73
90
  def write_main_path(path):
74
91
  with open(recording_path / 'mainscript', 'w') as f:
75
92
  f.write(path)
76
93
 
77
- # writer = stream.writer(path = recording_path / 'trace.bin')
78
-
79
- # os.register_at_fork(
80
- # # before = self.thread_state.wrap('disabled', self.before_fork),
81
- # before = before,
82
- # after_in_parent = self.thread_state.wrap('disabled', self.after_fork_in_parent),
83
- # after_in_child = self.thread_state.wrap('disabled', self.after_fork_in_child))
84
-
85
- # self.writer = thread_state.wrap(
86
- # desired_state = 'disabled',
87
- # sticky = True,
88
- # function = VerboseWriter(writer)) if verbose else writer
89
-
90
- # def gc_start(self):
91
- # self.before_gc = self.thread_state.value
92
- # self.thread_state.value = 'external'
93
-
94
- # def gc_end(self):
95
- # self.thread_state.value = self.before_gc
96
- # del self.before_gc
97
-
98
- # def gc_hook(self, phase, info):
99
- # if phase == 'start':
100
- # self.gc_start()
101
-
102
- # elif phase == 'stop':
103
- # self.gc_end()
104
- # gc.callbacks.append(self.gc_hook)
105
-
106
- # print(f'Tracing config: {tracing_config(config)}')
107
-
108
- # tracer = Tracer(config = tracing_config(config), writer = writer.handle('TRACE'))
94
+ run_path = recording_path / 'run'
95
+ run_path.mkdir(parents=True, exist_ok=False)
96
+
97
+ shutil.copy2(sys.argv[0], run_path)
98
+
99
+ python_path = recording_path / 'pythonpath'
100
+ python_path.mkdir(parents=True, exist_ok=False)
101
+
102
+ vscode = recording_path / '.vscode'
103
+ vscode.mkdir(parents=True, exist_ok=False)
104
+
105
+ # launch_json = {
106
+ # # // Use IntelliSense to learn about possible attributes.
107
+ # # // Hover to view descriptions of existing attributes.
108
+ # # // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
109
+ # "version": "0.2.0",
110
+ # "configurations": [
111
+ # {
112
+ # "name": "Python Debugger: Python File",
113
+ # "type": "debugpy",
114
+ # "request": "launch",
115
+ # "cwd": "${workspaceFolder}/run",
116
+ # "env": {
117
+ # "RETRACE_MODE": "replay"
118
+ # },
119
+ # "python": sys.executable,
120
+ # "program": sys.argv[0]
121
+ # }
122
+ # ]
123
+ # }
124
+
125
+ # dump_as_json(vscode / 'launch.json', launch_json)
126
+
127
+ workspace = {
128
+ "folders": [{ 'path': '.' }],
129
+ "settings": {
130
+ # This is the one that works reliably in .code-workspace files
131
+ # "python.defaultInterpreterPath":
132
+ "python.defaultInterpreterPath": sys.executable,
133
+ # "python.interpreterInfo": {
134
+ # "run": {"path": sys.executable}
135
+ # }
136
+ },
137
+ "launch": {
138
+ "version": "0.2.0",
139
+ "configurations": [
140
+ {
141
+ "justMyCode": False,
142
+ "name": "Python Debugger: Python File",
143
+ "type": "debugpy",
144
+ "request": "launch",
145
+ "cwd": "${workspaceFolder}/run",
146
+ "env": {
147
+ "RETRACE_RECORDING_PATH": "..",
148
+ "RETRACE_MODE": "replay"
149
+ },
150
+ # "python": ["${command:python.interpreterPath}"],
151
+ "python": sys.executable,
152
+ # "python": sys.executable,
153
+ "program": sys.argv[0]
154
+ }
155
+ ]
156
+ },
157
+ }
158
+
159
+ dump_as_json(recording_path / 'replay.code-workspace', workspace)
160
+
161
+ copy_source = thread_state.wrap('disabled', patch_find_spec(cwd = Path(os.getcwd()), run_path = run_path, python_path = python_path))
162
+
163
+ for finder in sys.meta_path:
164
+ finder.find_spec = functional.sequence(finder.find_spec, functional.side_effect(copy_source))
109
165
 
110
166
  return RecordProxySystem(thread_state = thread_state,
111
167
  immutable_types = immutable_types,
112
168
  tracing_config = tracing_config,
113
169
  write_main_path = write_main_path,
114
- path = recording_path / 'trace.bin')
170
+ path = recording_path / 'trace.bin',
171
+ tracecalls = env_truthy('RETRACE_ALL', False),
172
+ verbose = env_truthy('RETRACE_VERBOSE', False),
173
+ stacktraces = env_truthy('RETRACE_STACKTRACES', False),
174
+ magic_markers = env_truthy('RETRACE_MAGIC_MARKERS', False))
@@ -0,0 +1,28 @@
1
+ import gc
2
+
3
+ def container_replace(container, old, new):
4
+ if isinstance(container, dict):
5
+ if old in container:
6
+ elem = container.pop(old)
7
+ container[new] = elem
8
+ container_replace(container, old, new)
9
+ else:
10
+ for key,value in container.items():
11
+ if key != '__retrace_unproxied__' and value is old:
12
+ container[key] = new
13
+ return True
14
+ elif isinstance(container, list):
15
+ for i,value in enumerate(container):
16
+ if value is old:
17
+ container[i] = new
18
+ return True
19
+ elif isinstance(container, set):
20
+ container.remove(old)
21
+ container.add(new)
22
+ return True
23
+ else:
24
+ return False
25
+
26
+ def update(old, new):
27
+ for ref in gc.get_referrers(old):
28
+ container_replace(container = ref, old = old, new = new)