ato 2.0.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.
ato/scope.py ADDED
@@ -0,0 +1,492 @@
1
+ import argparse
2
+ import hashlib
3
+ import inspect
4
+ import pickle
5
+ import sys
6
+ import uuid
7
+ import warnings
8
+ from contextlib import contextmanager
9
+ from functools import wraps
10
+
11
+ from ato.adict import ADict
12
+ from inspect import currentframe, getframeinfo
13
+
14
+ from ato.parser import parse_command
15
+
16
+
17
+ # safe compile
18
+ def exec_with_no_permissions(code, __locals):
19
+ exec(compile(code, '<string>', 'single'), {'__builtins__': None}, __locals)
20
+
21
+
22
+ def parse_args_pythonic():
23
+ if Scope.stored_arguments is not None:
24
+ _pythonic_vars = Scope.stored_arguments
25
+ else:
26
+ _pythonic_vars = sys.argv[1:]
27
+ joined = ' '.join(_pythonic_vars)
28
+ _pythonic_vars = parse_command(joined)
29
+ pythonic_vars = []
30
+ for literal in _pythonic_vars:
31
+ if '=' not in literal:
32
+ pythonic_vars.extend(literal.split())
33
+ else:
34
+ name, *values = literal.split('=')
35
+ value = '='.join(values)
36
+ if value.startswith('%') and value.endswith('%'):
37
+ value = f'"{value[1:-1]}"'.replace('%', '\"')
38
+ pythonic_vars.append(f'{name}={value}')
39
+ default_prefix = ''
40
+ if len(Scope.registry) == 1:
41
+ scope_name = list(Scope.registry.keys())[0]
42
+ default_prefix += f'{scope_name}.'
43
+ for literal in pythonic_vars:
44
+ literal = default_prefix+literal
45
+ scope_name = literal.split('.')[0]
46
+ scope = Scope.registry.get(scope_name)
47
+ if scope is not None:
48
+ literal = literal[len(f'{scope_name}.'):]
49
+ if literal in scope.views or '=' in literal:
50
+ scope.assign(literal)
51
+ Scope.parsed = True
52
+
53
+
54
+ def _add_func_to_scope(scope, func, field=None, priority=0, lazy=False, default=False, chain_with=None):
55
+ field = field or func.__name__
56
+ chain_with = chain_with or []
57
+ if isinstance(chain_with, str):
58
+ chain_with = [chain_with]
59
+ scope.views[field] = {
60
+ "view_type": 'function',
61
+ "priority": priority,
62
+ "lazy": lazy,
63
+ "fn": func,
64
+ "chain_with": chain_with,
65
+ "default": default
66
+ }
67
+ if default:
68
+ scope.assign(field)
69
+
70
+
71
+ def add_func_to_scope(scope, field=None, priority=0, lazy=False, default=False, chain_with=None):
72
+ def decorator(func):
73
+ _add_func_to_scope(scope, func, field, priority, lazy, default, chain_with)
74
+ return func
75
+ return decorator
76
+
77
+
78
+ def add_config_to_scope(scope, field=None, config=None, priority=0, lazy=False, default=False, chain_with=None):
79
+ if field is None:
80
+ raise ValueError(f'A name of config must be specified to assign config directly.')
81
+ config = config or ADict()
82
+ view = ADict()
83
+ view.view_type = 'config'
84
+ view.config = config
85
+ view.priority = priority
86
+ view.lazy = lazy
87
+ chain_with = chain_with or []
88
+ if isinstance(chain_with, str):
89
+ chain_with = [chain_with]
90
+ view.chain_with = chain_with
91
+ view.default = default
92
+ scope.views[field] = view
93
+ if default:
94
+ scope.assign(field)
95
+
96
+
97
+ def add_func_to_multi_scope(scopes, field=None, priority=0, lazy=False, default=False, chain_with=None):
98
+ def decorator(func):
99
+ for scope in scopes:
100
+ _add_func_to_scope(scope, func, field, priority, lazy, default, chain_with)
101
+ return func
102
+ return decorator
103
+
104
+
105
+ def add_config_to_multi_scope(scopes, field=None, config=None, priority=0, lazy=False, default=False, chain_with=None):
106
+ for scope in scopes:
107
+ add_config_to_scope(scope, field, config, priority, lazy, default, chain_with)
108
+
109
+
110
+ def patch_parsing_method(func, unknown_external_literals='error'):
111
+ modes = {'merge', 'ignore', 'error'}
112
+ if unknown_external_literals not in {'merge', 'ignore', 'error'}:
113
+ raise ValueError(
114
+ f'Unexpected unknown_external_literals="{unknown_external_literals}"; '
115
+ f'Choose the one from {modes}.'
116
+ )
117
+
118
+ @wraps(func)
119
+ def capture(*args, **kwargs):
120
+ parser = args[0]
121
+ if len(sys.argv) >= 2:
122
+ if sys.argv[1] in ('--help', '-h'):
123
+ warnings.warn(f'Scope does not support "--help" option. Use "manual" instead.', DeprecationWarning)
124
+ if sys.argv[1] in ('--help', '-h', 'manual'):
125
+ print('[External Parser]')
126
+ parser.print_help()
127
+ Scope.logging_manual()
128
+ sys.exit(0)
129
+ known, unknown = func(*args, **kwargs)
130
+ defaults = vars(func(args[0], [])[0])
131
+ non_defaults = ADict(vars(known)).clone()
132
+ non_defaults.filter(lambda k, v: k not in defaults or v != defaults[k])
133
+ stored_arguments = []
134
+ while unknown:
135
+ literal = unknown.pop(0)
136
+ if literal.startswith('--'):
137
+ if unknown_external_literals == 'error':
138
+ raise RuntimeError(f'Unexpected external literal: {literal}')
139
+ value = unknown.pop(0)
140
+ key = literal[2:].replace('-', '_')
141
+ if unknown_external_literals == 'merge':
142
+ exec_with_no_permissions(
143
+ compile(f'non_defaults["{key}"] = {value}', '<string>', 'single'),
144
+ __locals={'non_defaults': non_defaults}
145
+ )
146
+ else:
147
+ stored_arguments.append(literal)
148
+ Scope.stored_arguments = stored_arguments
149
+ for scope in Scope.registry.values():
150
+ if scope.use_external_parser:
151
+ scope.observe('_argparse', defaults, scope.external_priority, lazy=False)
152
+ scope.assign('_argparse')
153
+ scope.observe('_argparse_literals', non_defaults, scope.external_priority, lazy=True)
154
+ scope.assign('_argparse_literals')
155
+ parse_args_pythonic()
156
+ release()
157
+ return known, unknown
158
+
159
+ return capture
160
+
161
+
162
+ def release():
163
+ if hasattr(_parser, 'stored_methods'):
164
+ _parser.parse_args, _parser.parse_known_args = _parser.stored_methods
165
+ del _parser.stored_methods
166
+
167
+
168
+ _parser = argparse.ArgumentParser
169
+
170
+
171
+ def _print_config(config):
172
+ print(config.to_xyz())
173
+ sys.exit(0)
174
+
175
+
176
+ def _get_func_trace_id(func):
177
+ return f'{func.__module__}.{func.__qualname__}'
178
+
179
+
180
+ def _generate_func_fingerprint(func, trace_id=None):
181
+ code_obj = func.__code__
182
+ code_info = (
183
+ code_obj.co_code,
184
+ code_obj.co_consts,
185
+ code_obj.co_names,
186
+ code_obj.co_varnames,
187
+ code_obj.co_filename,
188
+ code_obj.co_name,
189
+ code_obj.co_firstlineno
190
+ )
191
+ trace_id = _get_func_trace_id(func) if trace_id is None else trace_id
192
+ code_hash = hashlib.sha256(repr(code_info).encode()).hexdigest()
193
+ return ADict(**{trace_id: code_hash})
194
+
195
+
196
+ class Scope:
197
+ registry = ADict()
198
+ parsed = False
199
+ stored_arguments = None
200
+ current_scope = None
201
+ unknown_external_literals = 'ignore'
202
+
203
+ def __init__(
204
+ self,
205
+ config=None,
206
+ name='config',
207
+ use_external_parser=False,
208
+ external_priority=-2,
209
+ enable_override=False
210
+ ):
211
+ self.config = ADict() if config is None else config
212
+ self.name = name
213
+ self.use_external_parser = use_external_parser
214
+ self.enable_override = enable_override
215
+ self.register()
216
+ self.views = ADict()
217
+ self.manuals = ADict()
218
+ self.observe('_default', config, priority=-1, lazy=False)
219
+ add_func_to_scope(self, 'print', priority=1280, lazy=True, default=False)(_print_config)
220
+ self.screen = ADict(views=[], literals=[], lazy_views=[])
221
+ self.external_priority = external_priority
222
+ self.compute = False
223
+ self.config_in_compute = None
224
+ self.mode = 'ON'
225
+ self.is_applied = False
226
+ self._traced_data = ADict(fingerprints=ADict())
227
+
228
+ def activate(self):
229
+ self.mode = 'ON'
230
+
231
+ def deactivate(self):
232
+ self.mode = 'OFF'
233
+
234
+ @contextmanager
235
+ def pause(self):
236
+ self.deactivate()
237
+ yield
238
+ self.activate()
239
+
240
+ def trace(self, trace_id=None):
241
+ def decorator(func):
242
+ self._traced_data.fingerprints.update(_generate_func_fingerprint(func, trace_id=trace_id))
243
+ return self(func)
244
+ return decorator
245
+
246
+ def runtime_trace(self, init_fn=None, inspect_fn=None, trace_id=None):
247
+ def decorator(func):
248
+ def inner(*args, **kwargs):
249
+ nonlocal trace_id
250
+ if init_fn is not None:
251
+ init_fn()
252
+ results = inspect_results = self(func)(*args, **kwargs)
253
+ if inspect_fn is not None:
254
+ inspect_results = inspect_fn(results)
255
+ trace_id = _get_func_trace_id(func) if trace_id is not None else trace_id
256
+ inspect_hash = hashlib.sha256(pickle.dumps(inspect_results)).hexdigest()
257
+ self._traced_data.fingerprints.update({trace_id: inspect_hash})
258
+ return inner
259
+ return decorator
260
+
261
+ def register(self):
262
+ registry = self.__class__.registry
263
+ if len(registry) == 0:
264
+ _parser.stored_methods = [_parser.parse_args, _parser.parse_known_args]
265
+ _parser.parse_args = _parser.parse_known_args = patch_parsing_method(
266
+ _parser.parse_known_args,
267
+ self.__class__.unknown_external_literals
268
+ )
269
+ if self.name in registry and not self.enable_override:
270
+ raise ValueError(
271
+ f'{self.name} is already used by another scope. '
272
+ f'The name of scope must not be duplicated.'
273
+ )
274
+ self.__class__.registry[self.name] = self
275
+
276
+ @classmethod
277
+ def initialize_registry(cls):
278
+ cls.registry.clear()
279
+
280
+ @classmethod
281
+ def override(cls, scope):
282
+ cls.registry[scope.name] = scope
283
+
284
+ def add_to_screen(self, field=None, config=None, priority=0, lazy=False, default=False, chain_with=None):
285
+ if config is not None:
286
+ add_config_to_scope(self, field, config, priority, lazy, default, chain_with)
287
+ else:
288
+ return add_func_to_scope(self, field, priority, lazy, default, chain_with)
289
+
290
+ def observe(self, field=None, config=None, priority=0, lazy=False, default=False, chain_with=None):
291
+ # to enable replace from external codes
292
+ return self.__class__.registry[self.name].add_to_screen(field, config, priority, lazy, default, chain_with)
293
+
294
+ def manual(self, field=None, manual=None):
295
+ if manual is not None:
296
+ self.manuals.update(manual)
297
+ else:
298
+ field(self.manuals)
299
+ return field
300
+
301
+ @classmethod
302
+ def logging_manual(cls):
303
+ for scope_name, scope in cls.registry.items():
304
+ print('-'*50)
305
+ print(f'[Scope "{scope_name}"]')
306
+ print('(The Applying Order of Views)')
307
+ view_names = list([key for key, view in scope.views.items() if not view.lazy])
308
+ lazy_view_names = list([key for key, view in scope.views.items() if view.lazy])
309
+ view_names.sort(key=lambda x: scope.views[x].priority)
310
+ lazy_view_names.sort(key=lambda x: scope.views[x].priority)
311
+ print(' → '.join(view_names+['(CLI Inputs)']+lazy_view_names))
312
+ print('(User Manuals)')
313
+ manuals = scope.manuals
314
+ if len(cls.registry) > 1:
315
+ manuals = manuals.clone()
316
+ for key, value in scope.manuals.items():
317
+ manuals[f'{scope.name}.{key}'] = manuals.pop(key)
318
+ print(manuals.to_xyz())
319
+ print('-'*50)
320
+ sys.exit(0)
321
+
322
+ def get_assigned_views(self):
323
+ default_views = [view for view in self.screen.views if self.views[view].default]
324
+ views = [view for view in self.screen.views if view not in default_views]
325
+ default_lazy_views = [lazy_view for lazy_view in self.screen.lazy_views if self.views[lazy_view].default]
326
+ lazy_views = [lazy_view for lazy_view in self.screen.lazy_views if lazy_view not in default_lazy_views]
327
+ return ADict(
328
+ default_views=default_views,
329
+ default_lazy_views=default_lazy_views,
330
+ views=views,
331
+ lazy_views=lazy_views,
332
+ literals=self.screen.literals
333
+ )
334
+
335
+ def assign(self, literals):
336
+ if not isinstance(literals, (list, tuple)) or isinstance(literals, str):
337
+ literals = [literals]
338
+ for literal in literals:
339
+ if literal in self.views:
340
+ view_info = self.views[literal]
341
+ if view_info.lazy and literal not in self.screen.lazy_views:
342
+ if view_info.chain_with is not None:
343
+ self.assign(view_info.chain_with)
344
+ self.screen.lazy_views.append(literal)
345
+ elif not view_info.lazy and literal not in self.screen.views:
346
+ if view_info.chain_with is not None:
347
+ self.assign(view_info.chain_with)
348
+ self.screen.views.append(literal)
349
+ else:
350
+ self.screen.literals.append(literal)
351
+
352
+ def apply(self):
353
+ if len(sys.argv) >= 2:
354
+ if sys.argv[1] in ('--help', '-h'):
355
+ warnings.warn(f'Scope does not support "--help" option. Use "manual" instead.', DeprecationWarning)
356
+ if sys.argv[1] in ('--help', '-h', 'manual'):
357
+ Scope.logging_manual()
358
+ self.__class__.current_scope = self
359
+ self.screen.views.sort(key=lambda x: self.views[x].priority)
360
+ self.screen.lazy_views.sort(key=lambda x: self.views[x].priority)
361
+ for field in self.screen.views:
362
+ view = self.views[field]
363
+ if view.view_type == 'config':
364
+ self.config.update(view.config)
365
+ else:
366
+ view.fn(self.config)
367
+ for literal in self.screen.literals:
368
+ if isinstance(literal, str):
369
+ exec_with_no_permissions(f'config.{literal}', __locals={'config': self.config})
370
+ else:
371
+ self.config.update(literal)
372
+ self.compute = True
373
+ self.config.freeze()
374
+ for field in self.screen.views:
375
+ view = self.views[field]
376
+ if view.view_type != 'config':
377
+ view.fn(self.config)
378
+ self.compute = False
379
+ self.config.defrost()
380
+ for field in self.screen.lazy_views:
381
+ view = self.views[field]
382
+ if view.view_type == 'config':
383
+ self.config.update(view.config)
384
+ else:
385
+ view.fn(self.config)
386
+ self.is_applied = True
387
+
388
+ def __enter__(self):
389
+ if not Scope.parsed:
390
+ parse_args_pythonic()
391
+ if not self.is_applied:
392
+ self.apply()
393
+
394
+ def __exit__(self, exc_type, exc_val, exc_tb):
395
+ pass
396
+
397
+ def _recreate_context(self):
398
+ return self
399
+
400
+ def _is_config_at_positional(self, func, *args, **kwargs):
401
+ if self.name in kwargs:
402
+ return True
403
+ full_arguments = inspect.getfullargspec(func)[0]
404
+ index = full_arguments.index(self.name)
405
+ return {'positional': index+1 < len(args) or len(full_arguments) > len(args), 'index': index}
406
+
407
+ def get_config_updated_arguments(self, func, *args, **kwargs):
408
+ pos_info = self._is_config_at_positional(func, *args, **kwargs)
409
+ if pos_info['positional']:
410
+ args = list(args)
411
+ args.insert(pos_info['index'], self.config)
412
+ else:
413
+ kwargs.update({self.name: self.config})
414
+ return args, kwargs
415
+
416
+ def exec(self, func):
417
+ @wraps(func)
418
+ def inner(*args, **kwargs):
419
+ with self._recreate_context():
420
+ if self.mode == 'ON':
421
+ args, kwargs = self.get_config_updated_arguments(func, *args, **kwargs)
422
+ return func(*args, **kwargs)
423
+ return inner
424
+
425
+ def __call__(self, func):
426
+ return self.exec(func)
427
+
428
+ def reset_user_inputs(self):
429
+ self.screen = ADict(views=[], literals=[], lazy_views=[])
430
+
431
+ def convert_argparse_to_scope(self):
432
+ args = self.views['_argparse'].config
433
+ code = f"def argparse({self.name}):\n"
434
+ code += '\n'.join([
435
+ f' {self.name}.{key} = '+(f"'{value}'" if isinstance(value, str) else f'{value}')
436
+ for key, value in args.items()
437
+ ])
438
+ return code
439
+
440
+ @classmethod
441
+ @contextmanager
442
+ def lazy(cls, with_compile=False, priority=1):
443
+ scope = cls.current_scope
444
+ if scope.compute and not with_compile:
445
+ scope.config.defrost()
446
+ yield
447
+ scope.config.freeze()
448
+ else:
449
+ scope.config.freeze()
450
+ try:
451
+ yield
452
+ except (KeyError, AttributeError):
453
+ pass
454
+ scope.config.defrost()
455
+ if with_compile and not scope.compute:
456
+ frame = currentframe().f_back.f_back
457
+ frame_info = getframeinfo(frame)
458
+ file_name = frame_info.filename
459
+ start = frame_info.positions.lineno
460
+ end = frame_info.positions.end_lineno
461
+ with open(file_name, 'r') as f:
462
+ inner_ctx_lines = list(f.readlines())[start:end]
463
+ ctx_name = f"_lazy_context_{str(uuid.uuid4()).replace('-', '_')}"
464
+ inner_ctx_lines = [f'def {ctx_name}({scope.name}):']+inner_ctx_lines
465
+ global_vars = frame.f_globals
466
+ local_vars = frame.f_locals
467
+ exec(compile('\n'.join(inner_ctx_lines), '<string>', 'exec'), global_vars, local_vars)
468
+ scope.observe(default=True, lazy=True, priority=priority)(local_vars[ctx_name])
469
+
470
+
471
+ class MultiScope:
472
+ def __init__(self, *scopes):
473
+ self.scopes = scopes
474
+ self.register_all()
475
+
476
+ def register_all(self):
477
+ Scope.registry.clear()
478
+ for scope in self.scopes:
479
+ Scope.registry[scope.name] = scope
480
+
481
+ def __call__(self, func):
482
+ def decorator(*args, **kwargs):
483
+ # if not Scope.parsed:
484
+ parse_args_pythonic()
485
+ arguments = inspect.getfullargspec(func)
486
+ name_spaces = set(arguments.args+arguments.kwonlyargs)
487
+ for scope in self.scopes:
488
+ if scope.name in name_spaces or arguments.varkw is not None:
489
+ args, kwargs = scope.get_config_updated_arguments(func, *args, **kwargs)
490
+ scope.apply()
491
+ return func(*args, **kwargs)
492
+ return decorator
ato/utils.py ADDED
@@ -0,0 +1,55 @@
1
+ from typing import Iterable, Union, Sequence, Optional, Dict
2
+
3
+
4
+ def is_seq(x):
5
+ return isinstance(x, Iterable) and not isinstance(x, str)
6
+
7
+
8
+ def get_all(iterable, key):
9
+ return map(lambda x: x[key], iterable)
10
+
11
+
12
+ def _to_ord(s):
13
+ if s is not None and len(s) >= 1:
14
+ return ord(s)
15
+
16
+
17
+ def replace_all(
18
+ string: str,
19
+ sources: Union[Sequence[str], str],
20
+ mappings: Optional[Union[Dict[str, str], Sequence[str], str]] = None
21
+ ):
22
+ if mappings is None:
23
+ mappings = {}
24
+ elif not isinstance(mappings, dict):
25
+ assert len(sources) == len(mappings), 'sources and mappings must have same length.'
26
+ mappings = {source: target for source, target in zip(sources, mappings)}
27
+ mappings = {ord(source): _to_ord(mappings.get(source, None)) for source in sources}
28
+ return string.translate(mappings)
29
+
30
+
31
+ def remove_all(string, targets):
32
+ for target in targets:
33
+ string = string.replace(target, '')
34
+ return string
35
+
36
+
37
+ def convert_string_to_value(value):
38
+ if not isinstance(value, str):
39
+ return value
40
+ if value.lower() == 'none':
41
+ return None
42
+ elif value.lower() == 'true':
43
+ return True
44
+ elif value.lower() == 'false':
45
+ return False
46
+ elif value == '[Empty Sequence]':
47
+ return []
48
+ elif value == '[Empty Mapping]':
49
+ return dict()
50
+ elif value.isdecimal():
51
+ return int(value)
52
+ elif remove_all(value.lower(), ('.', 'e+', 'e-')).isnumeric():
53
+ return float(value)
54
+ else:
55
+ return value