mapres 3.dev20__tar.gz → 3.dev21__tar.gz
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.
- {mapres-3.dev20/src/mapres.egg-info → mapres-3.dev21}/PKG-INFO +1 -1
- {mapres-3.dev20 → mapres-3.dev21}/pyproject.toml +1 -1
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/datamap.py +5 -11
- mapres-3.dev21/src/mapres/evaluator.py +123 -0
- {mapres-3.dev20 → mapres-3.dev21/src/mapres.egg-info}/PKG-INFO +1 -1
- mapres-3.dev20/src/mapres/evaluator.py +0 -221
- {mapres-3.dev20 → mapres-3.dev21}/LICENSE +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/README.md +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/setup.cfg +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/__init__.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/ast.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/cache.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/exceptions.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/layers.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/maps/__init__.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/maps/color.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/maps/time.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/parser.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/resolver.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres/tokenizer.py +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres.egg-info/SOURCES.txt +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres.egg-info/requires.txt +0 -0
- {mapres-3.dev20 → mapres-3.dev21}/src/mapres.egg-info/top_level.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from dataclasses import dataclass, fields, is_dataclass
|
|
3
2
|
|
|
4
3
|
# syntax identifiers
|
|
@@ -30,15 +29,13 @@ class DataMap:
|
|
|
30
29
|
|
|
31
30
|
Mode metadata (__mode__) controls behavior:
|
|
32
31
|
- None: plain static map
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
32
|
+
- 'dynamic': values may be provided by callables/providers
|
|
33
|
+
- 'config': config-style maps (no dynamic evaluation)
|
|
34
|
+
- 'callable': map exposes callable methods for nested calls
|
|
36
35
|
'''
|
|
37
36
|
__syntax__: str = None
|
|
38
|
-
__mode__: str | None = None
|
|
39
|
-
__recursive__: bool = False
|
|
37
|
+
__mode__: str | None = None # 'dynamic', 'config', 'callable', or None
|
|
40
38
|
__missing_key__: str = 'error' # 'error', 'silent', 'placeholder'
|
|
41
|
-
__ignore_delimiters__: bool = False
|
|
42
39
|
|
|
43
40
|
def as_map(self):
|
|
44
41
|
'''
|
|
@@ -95,14 +92,11 @@ class DataMap:
|
|
|
95
92
|
return out
|
|
96
93
|
|
|
97
94
|
|
|
98
|
-
def datamap(_cls=None, *, syntax=None, mode=None,
|
|
99
|
-
|
|
95
|
+
def datamap(_cls=None, *, syntax=None, mode=None, missing_key=None):
|
|
100
96
|
def wrap(cls):
|
|
101
97
|
cls.__syntax__ = syntax
|
|
102
98
|
cls.__mode__ = mode
|
|
103
|
-
cls.__recursive__ = recursive
|
|
104
99
|
cls.__missing_key__ = missing_key or 'error'
|
|
105
|
-
cls.__ignore_delimiters__ = ignore_delimiters or False
|
|
106
100
|
|
|
107
101
|
namespace = dict(cls.__dict__)
|
|
108
102
|
namespace['__dict__'] = {}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from .ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
2
|
+
from .tokenizer import Tokenizer
|
|
3
|
+
from .parser import Parser
|
|
4
|
+
from .exceptions import MapResError, MissingKeyError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EvaluationError(MapResError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Evaluator:
|
|
12
|
+
def __init__(self, layerstack, ctx=None):
|
|
13
|
+
self.layerstack = layerstack
|
|
14
|
+
self.ctx = ctx or {}
|
|
15
|
+
self._seen = set() # cycle detection
|
|
16
|
+
|
|
17
|
+
# public api
|
|
18
|
+
def evaluate(self, node):
|
|
19
|
+
return self._eval(node)
|
|
20
|
+
|
|
21
|
+
# internal dispatch
|
|
22
|
+
def _eval(self, node):
|
|
23
|
+
if isinstance(node, TemplateNode):
|
|
24
|
+
return self._eval_template(node)
|
|
25
|
+
|
|
26
|
+
if isinstance(node, TextNode):
|
|
27
|
+
return node.text
|
|
28
|
+
|
|
29
|
+
if isinstance(node, IdentNode):
|
|
30
|
+
return self._eval_ident(node)
|
|
31
|
+
|
|
32
|
+
if isinstance(node, CallNode):
|
|
33
|
+
return self._eval_call(node)
|
|
34
|
+
|
|
35
|
+
raise EvaluationError(f'Unknown AST node: {node}')
|
|
36
|
+
|
|
37
|
+
# template
|
|
38
|
+
def _eval_template(self, node: TemplateNode):
|
|
39
|
+
parts = []
|
|
40
|
+
for child in node.children:
|
|
41
|
+
parts.append(self._eval(child))
|
|
42
|
+
out = ''.join(parts)
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
# identifier
|
|
46
|
+
def _eval_ident(self, node: IdentNode):
|
|
47
|
+
name = node.name
|
|
48
|
+
|
|
49
|
+
# cycle detection
|
|
50
|
+
if name in self._seen:
|
|
51
|
+
raise EvaluationError(f"Cycle detected in identifier '{name}'")
|
|
52
|
+
self._seen.add(name)
|
|
53
|
+
|
|
54
|
+
parts = name.split('.')
|
|
55
|
+
|
|
56
|
+
# 1. ctx first
|
|
57
|
+
val = self._lookup_hierarchical(self.ctx, parts)
|
|
58
|
+
if val is not None:
|
|
59
|
+
self._seen.remove(name)
|
|
60
|
+
return str(val)
|
|
61
|
+
|
|
62
|
+
# 2. syntax-bound maps
|
|
63
|
+
for m in self.layerstack.all_maps():
|
|
64
|
+
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
d = self._map_to_dict(m)
|
|
68
|
+
val = self._lookup_hierarchical(d, parts)
|
|
69
|
+
if val is not None:
|
|
70
|
+
self._seen.remove(name)
|
|
71
|
+
return str(val)
|
|
72
|
+
|
|
73
|
+
self._seen.remove(name)
|
|
74
|
+
raise MissingKeyError(name)
|
|
75
|
+
|
|
76
|
+
# call (nested token)
|
|
77
|
+
def _eval_call(self, node: CallNode):
|
|
78
|
+
outer = node.name
|
|
79
|
+
|
|
80
|
+
if outer in self._seen:
|
|
81
|
+
raise EvaluationError(f"Cycle detected in call '{outer}'")
|
|
82
|
+
self._seen.add(outer)
|
|
83
|
+
|
|
84
|
+
arg_value = self._eval(node.arg)
|
|
85
|
+
parts = [outer, arg_value]
|
|
86
|
+
|
|
87
|
+
# ctx first
|
|
88
|
+
val = self._lookup_hierarchical(self.ctx, parts)
|
|
89
|
+
if val is not None:
|
|
90
|
+
self._seen.remove(outer)
|
|
91
|
+
return str(val)
|
|
92
|
+
|
|
93
|
+
# syntax-bound maps
|
|
94
|
+
for m in self.layerstack.all_maps():
|
|
95
|
+
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
d = self._map_to_dict(m)
|
|
99
|
+
val = self._lookup_hierarchical(d, parts)
|
|
100
|
+
if val is not None:
|
|
101
|
+
self._seen.remove(outer)
|
|
102
|
+
return str(val)
|
|
103
|
+
|
|
104
|
+
self._seen.remove(outer)
|
|
105
|
+
raise MissingKeyError(f'{outer}({arg_value})')
|
|
106
|
+
|
|
107
|
+
# helpers
|
|
108
|
+
def _map_to_dict(self, m):
|
|
109
|
+
if hasattr(m, 'as_map'):
|
|
110
|
+
return m.as_map()
|
|
111
|
+
if isinstance(m, dict):
|
|
112
|
+
return m
|
|
113
|
+
raise EvaluationError(f'Invalid map object: {m}')
|
|
114
|
+
|
|
115
|
+
def _lookup_hierarchical(self, root, parts):
|
|
116
|
+
cur = root
|
|
117
|
+
for p in parts:
|
|
118
|
+
if not isinstance(cur, dict):
|
|
119
|
+
return None
|
|
120
|
+
if p not in cur:
|
|
121
|
+
return None
|
|
122
|
+
cur = cur[p]
|
|
123
|
+
return cur
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
from .ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
2
|
-
from .tokenizer import Tokenizer
|
|
3
|
-
from .parser import Parser
|
|
4
|
-
from .exceptions import MapResError, MissingKeyError
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class EvaluationError(MapResError):
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Evaluator:
|
|
12
|
-
def __init__(self, layerstack, ctx=None):
|
|
13
|
-
self.layerstack = layerstack
|
|
14
|
-
self.ctx = ctx or {}
|
|
15
|
-
self._seen = set() # cycle detection
|
|
16
|
-
|
|
17
|
-
# public api
|
|
18
|
-
def evaluate(self, node):
|
|
19
|
-
return self._eval(node)
|
|
20
|
-
|
|
21
|
-
# internal dispatch
|
|
22
|
-
def _eval(self, node):
|
|
23
|
-
if isinstance(node, TemplateNode):
|
|
24
|
-
return self._eval_template(node)
|
|
25
|
-
|
|
26
|
-
if isinstance(node, TextNode):
|
|
27
|
-
return node.text
|
|
28
|
-
|
|
29
|
-
if isinstance(node, IdentNode):
|
|
30
|
-
return self._eval_ident(node)
|
|
31
|
-
|
|
32
|
-
if isinstance(node, CallNode):
|
|
33
|
-
return self._eval_call(node)
|
|
34
|
-
|
|
35
|
-
raise EvaluationError(f'Unknown AST node: {node}')
|
|
36
|
-
|
|
37
|
-
# template
|
|
38
|
-
def _eval_template(self, node: TemplateNode):
|
|
39
|
-
parts = []
|
|
40
|
-
for child in node.children:
|
|
41
|
-
parts.append(self._eval(child))
|
|
42
|
-
out = ''.join(parts)
|
|
43
|
-
|
|
44
|
-
# recursion over full template output
|
|
45
|
-
if self._has_recursive_maps():
|
|
46
|
-
seen = set()
|
|
47
|
-
depth = 0
|
|
48
|
-
max_depth = self._get_max_depth()
|
|
49
|
-
|
|
50
|
-
while depth < max_depth:
|
|
51
|
-
new_out = self._re_resolve(out)
|
|
52
|
-
if new_out == out:
|
|
53
|
-
break
|
|
54
|
-
if new_out in seen:
|
|
55
|
-
raise EvaluationError('Cycle detected in template recursion')
|
|
56
|
-
seen.add(new_out)
|
|
57
|
-
out = new_out
|
|
58
|
-
depth += 1
|
|
59
|
-
|
|
60
|
-
return out
|
|
61
|
-
|
|
62
|
-
# identifier
|
|
63
|
-
def _eval_ident(self, node: IdentNode):
|
|
64
|
-
name = node.name
|
|
65
|
-
|
|
66
|
-
# ignore_delimiters: treat token as literal for this syntax
|
|
67
|
-
if self._syntax_ignores_delimiters(node.syntax):
|
|
68
|
-
return self._literal_from_syntax(name, node.syntax)
|
|
69
|
-
|
|
70
|
-
# cycle detection
|
|
71
|
-
if name in self._seen:
|
|
72
|
-
raise EvaluationError(f"Cycle detected in identifier '{name}'")
|
|
73
|
-
self._seen.add(name)
|
|
74
|
-
|
|
75
|
-
parts = name.split('.')
|
|
76
|
-
|
|
77
|
-
# 1. ctx first
|
|
78
|
-
val = self._lookup_hierarchical(self.ctx, parts)
|
|
79
|
-
if val is not None:
|
|
80
|
-
self._seen.remove(name)
|
|
81
|
-
return str(val)
|
|
82
|
-
|
|
83
|
-
# 2. syntax-bound maps
|
|
84
|
-
for m in self.layerstack.all_maps():
|
|
85
|
-
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
86
|
-
continue
|
|
87
|
-
|
|
88
|
-
d = self._map_to_dict(m)
|
|
89
|
-
val = self._lookup_hierarchical(d, parts)
|
|
90
|
-
if val is not None:
|
|
91
|
-
self._seen.remove(name)
|
|
92
|
-
return str(val)
|
|
93
|
-
|
|
94
|
-
self._seen.remove(name)
|
|
95
|
-
raise MissingKeyError(name)
|
|
96
|
-
|
|
97
|
-
# call (nested token)
|
|
98
|
-
def _eval_call(self, node: CallNode):
|
|
99
|
-
outer = node.name
|
|
100
|
-
|
|
101
|
-
# ignore_delimiters: treat call token as literal
|
|
102
|
-
if self._syntax_ignores_delimiters(node.syntax):
|
|
103
|
-
arg_text = self._eval(node.arg)
|
|
104
|
-
return self._literal_from_syntax(outer, node.syntax, is_call=True, arg_text=arg_text)
|
|
105
|
-
|
|
106
|
-
if outer in self._seen:
|
|
107
|
-
raise EvaluationError(f"Cycle detected in call '{outer}'")
|
|
108
|
-
self._seen.add(outer)
|
|
109
|
-
|
|
110
|
-
arg_value = self._eval(node.arg)
|
|
111
|
-
parts = [outer, arg_value]
|
|
112
|
-
|
|
113
|
-
# ctx first
|
|
114
|
-
val = self._lookup_hierarchical(self.ctx, parts)
|
|
115
|
-
if val is not None:
|
|
116
|
-
self._seen.remove(outer)
|
|
117
|
-
return str(val)
|
|
118
|
-
|
|
119
|
-
# syntax-bound maps
|
|
120
|
-
for m in self.layerstack.all_maps():
|
|
121
|
-
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
d = self._map_to_dict(m)
|
|
125
|
-
val = self._lookup_hierarchical(d, parts)
|
|
126
|
-
if val is not None:
|
|
127
|
-
self._seen.remove(outer)
|
|
128
|
-
return str(val)
|
|
129
|
-
|
|
130
|
-
self._seen.remove(outer)
|
|
131
|
-
raise MissingKeyError(f'{outer}({arg_value})')
|
|
132
|
-
|
|
133
|
-
# helpers
|
|
134
|
-
def _map_to_dict(self, m):
|
|
135
|
-
if hasattr(m, 'as_map'):
|
|
136
|
-
return m.as_map()
|
|
137
|
-
if isinstance(m, dict):
|
|
138
|
-
return m
|
|
139
|
-
raise EvaluationError(f'Invalid map object: {m}')
|
|
140
|
-
|
|
141
|
-
def _lookup_hierarchical(self, root, parts):
|
|
142
|
-
cur = root
|
|
143
|
-
for p in parts:
|
|
144
|
-
if not isinstance(cur, dict):
|
|
145
|
-
return None
|
|
146
|
-
if p not in cur:
|
|
147
|
-
return None
|
|
148
|
-
cur = cur[p]
|
|
149
|
-
return cur
|
|
150
|
-
|
|
151
|
-
def _has_recursive_maps(self):
|
|
152
|
-
for m in self.layerstack.all_maps():
|
|
153
|
-
if getattr(m, '__recursive__', False) is True:
|
|
154
|
-
return True
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
def _get_max_depth(self):
|
|
158
|
-
depths = [
|
|
159
|
-
getattr(m, '__max_depth__', 10)
|
|
160
|
-
for m in self.layerstack.all_maps()
|
|
161
|
-
if getattr(m, '__recursive__', False) is True
|
|
162
|
-
]
|
|
163
|
-
return max(depths) if depths else 0
|
|
164
|
-
|
|
165
|
-
def _re_resolve(self, text: str) -> str:
|
|
166
|
-
tokens = Tokenizer(text).tokenize()
|
|
167
|
-
ast = Parser(tokens).parse()
|
|
168
|
-
return self.evaluate(ast)
|
|
169
|
-
|
|
170
|
-
def _syntax_ignores_delimiters(self, syntax_name: str) -> bool:
|
|
171
|
-
for m in self.layerstack.all_maps():
|
|
172
|
-
if getattr(m, '__syntax__', None) == syntax_name:
|
|
173
|
-
if getattr(m, '__ignore_delimiters__', False):
|
|
174
|
-
return True
|
|
175
|
-
return False
|
|
176
|
-
|
|
177
|
-
def _literal_from_syntax(self, name: str, syntax_name: str, is_call: bool = False, arg_text: str | None = None) -> str:
|
|
178
|
-
if not is_call:
|
|
179
|
-
if syntax_name == 'colons':
|
|
180
|
-
return f':{name}:'
|
|
181
|
-
if syntax_name == 'braces':
|
|
182
|
-
return f'{{{{{name}}}}}'
|
|
183
|
-
if syntax_name == 'dollars':
|
|
184
|
-
return f'${{{name}}}'
|
|
185
|
-
if syntax_name == 'angles':
|
|
186
|
-
return f'<{name}>'
|
|
187
|
-
if syntax_name == 'pipes':
|
|
188
|
-
return f'|{name}|'
|
|
189
|
-
if syntax_name == 'percents':
|
|
190
|
-
return f'%{name}%'
|
|
191
|
-
if syntax_name == 'paren_dollars':
|
|
192
|
-
return f'$({name})'
|
|
193
|
-
if syntax_name == 'at_tags':
|
|
194
|
-
return f'@{name}@'
|
|
195
|
-
if syntax_name == 'hash_tags':
|
|
196
|
-
return f'#{name}#'
|
|
197
|
-
return name
|
|
198
|
-
|
|
199
|
-
# call literal: syntax-specific wrapping of name(arg_text)
|
|
200
|
-
arg = arg_text or ''
|
|
201
|
-
inner = f'{name}({arg})'
|
|
202
|
-
|
|
203
|
-
if syntax_name == 'colons':
|
|
204
|
-
return f':{inner}:'
|
|
205
|
-
if syntax_name == 'braces':
|
|
206
|
-
return f'{{{{{inner}}}}}'
|
|
207
|
-
if syntax_name == 'dollars':
|
|
208
|
-
return f'${{{inner}}}'
|
|
209
|
-
if syntax_name == 'angles':
|
|
210
|
-
return f'<{inner}>'
|
|
211
|
-
if syntax_name == 'pipes':
|
|
212
|
-
return f'|{inner}|'
|
|
213
|
-
if syntax_name == 'percents':
|
|
214
|
-
return f'%{inner}%'
|
|
215
|
-
if syntax_name == 'paren_dollars':
|
|
216
|
-
return f'$({inner})'
|
|
217
|
-
if syntax_name == 'at_tags':
|
|
218
|
-
return f'@{inner}@'
|
|
219
|
-
if syntax_name == 'hash_tags':
|
|
220
|
-
return f'#{inner}#'
|
|
221
|
-
return inner
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|