mapres 3.dev14__tar.gz → 3.dev15__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.dev14/src/mapres.egg-info → mapres-3.dev15}/PKG-INFO +1 -1
- {mapres-3.dev14 → mapres-3.dev15}/pyproject.toml +1 -1
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/datamap.py +35 -3
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/evaluator.py +50 -10
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/exceptions.py +3 -1
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/resolver.py +19 -2
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/tokenizer.py +69 -23
- {mapres-3.dev14 → mapres-3.dev15/src/mapres.egg-info}/PKG-INFO +1 -1
- {mapres-3.dev14 → mapres-3.dev15}/LICENSE +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/README.md +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/setup.cfg +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/__init__.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/ast.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/cache.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/layers.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/maps/__init__.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/maps/color.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/maps/time.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres/parser.py +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/SOURCES.txt +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/requires.txt +0 -0
- {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/top_level.txt +0 -0
|
@@ -35,7 +35,9 @@ class DataMap:
|
|
|
35
35
|
- "callable": map exposes callable methods for nested calls
|
|
36
36
|
'''
|
|
37
37
|
__syntax__: str = None
|
|
38
|
-
__mode__: str | None = None
|
|
38
|
+
__mode__: str | None = None # "dynamic", "config", "callable", or None
|
|
39
|
+
__recursive__: bool = False
|
|
40
|
+
__missing_key__: str = 'error' # 'error', 'silent', 'placeholder'
|
|
39
41
|
|
|
40
42
|
def as_map(self):
|
|
41
43
|
'''
|
|
@@ -47,8 +49,9 @@ class DataMap:
|
|
|
47
49
|
'''
|
|
48
50
|
result = {}
|
|
49
51
|
|
|
52
|
+
# dataclass fields
|
|
50
53
|
for f in fields(self):
|
|
51
|
-
if not f.init or f.name in ('__syntax__', '__mode__'):
|
|
54
|
+
if not f.init or f.name in ('__syntax__', '__mode__', '__recursive__', '__missing_key__'):
|
|
52
55
|
continue
|
|
53
56
|
|
|
54
57
|
val = getattr(self, f.name)
|
|
@@ -63,15 +66,44 @@ class DataMap:
|
|
|
63
66
|
|
|
64
67
|
result[f.name] = val
|
|
65
68
|
|
|
69
|
+
# nested classes > nested dicts
|
|
70
|
+
for name, obj in self.__class__.__dict__.items():
|
|
71
|
+
if name.startswith('__'):
|
|
72
|
+
continue
|
|
73
|
+
if isinstance(obj, type):
|
|
74
|
+
result[name] = self._class_to_dict(obj)
|
|
75
|
+
|
|
66
76
|
return result
|
|
67
77
|
|
|
78
|
+
def _class_to_dict(self, cls):
|
|
79
|
+
out = {}
|
|
80
|
+
|
|
81
|
+
# dataclass fields on nested class
|
|
82
|
+
if is_dataclass(cls):
|
|
83
|
+
dummy = cls() # assumes no required __init__ args
|
|
84
|
+
for f in fields(dummy):
|
|
85
|
+
if not f.init:
|
|
86
|
+
continue
|
|
87
|
+
out[f.name] = getattr(dummy, f.name)
|
|
88
|
+
|
|
89
|
+
# nested classes inside nested class
|
|
90
|
+
for name, obj in cls.__dict__.items():
|
|
91
|
+
if name.startswith('__'):
|
|
92
|
+
continue
|
|
93
|
+
if isinstance(obj, type):
|
|
94
|
+
out[name] = self._class_to_dict(obj)
|
|
95
|
+
|
|
96
|
+
return out
|
|
97
|
+
|
|
68
98
|
|
|
69
99
|
# main decorator factory
|
|
70
|
-
def datamap(_cls=None, *, syntax=None, mode=None):
|
|
100
|
+
def datamap(_cls=None, *, syntax=None, mode=None, recursive=None, missing_key=None):
|
|
71
101
|
'''@datamap decorator'''
|
|
72
102
|
def wrap(cls):
|
|
73
103
|
cls.__syntax__ = syntax
|
|
74
104
|
cls.__mode__ = mode
|
|
105
|
+
cls.__recursive__ = recursive
|
|
106
|
+
cls.__missing_key__ = missing_key or 'error'
|
|
75
107
|
namespace = dict(cls.__dict__)
|
|
76
108
|
namespace['__dict__'] = {}
|
|
77
109
|
rules_obj = namespace.get('rules', None)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from .ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
2
|
+
from .tokenizer import Tokenizer
|
|
3
|
+
from .parser import Parser
|
|
4
|
+
from .exceptions import MapResError, MissingKeyError
|
|
2
5
|
|
|
3
6
|
|
|
4
|
-
class EvaluationError(
|
|
7
|
+
class EvaluationError(MapResError):
|
|
5
8
|
pass
|
|
6
9
|
|
|
7
10
|
|
|
@@ -29,14 +32,32 @@ class Evaluator:
|
|
|
29
32
|
if isinstance(node, CallNode):
|
|
30
33
|
return self._eval_call(node)
|
|
31
34
|
|
|
32
|
-
raise EvaluationError(f
|
|
35
|
+
raise EvaluationError(f'Unknown AST node: {node}')
|
|
33
36
|
|
|
34
37
|
# template
|
|
35
38
|
def _eval_template(self, node: TemplateNode):
|
|
36
39
|
parts = []
|
|
37
40
|
for child in node.children:
|
|
38
41
|
parts.append(self._eval(child))
|
|
39
|
-
|
|
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
|
|
40
61
|
|
|
41
62
|
# identifier
|
|
42
63
|
def _eval_ident(self, node: IdentNode):
|
|
@@ -47,7 +68,7 @@ class Evaluator:
|
|
|
47
68
|
raise EvaluationError(f"Cycle detected in identifier '{name}'")
|
|
48
69
|
self._seen.add(name)
|
|
49
70
|
|
|
50
|
-
parts = name.split(
|
|
71
|
+
parts = name.split('.')
|
|
51
72
|
|
|
52
73
|
# 1. ctx first
|
|
53
74
|
val = self._lookup_hierarchical(self.ctx, parts)
|
|
@@ -57,7 +78,7 @@ class Evaluator:
|
|
|
57
78
|
|
|
58
79
|
# 2. syntax-bound maps
|
|
59
80
|
for m in self.layerstack.all_maps():
|
|
60
|
-
if getattr(m,
|
|
81
|
+
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
61
82
|
continue
|
|
62
83
|
|
|
63
84
|
d = self._map_to_dict(m)
|
|
@@ -67,7 +88,7 @@ class Evaluator:
|
|
|
67
88
|
return str(val)
|
|
68
89
|
|
|
69
90
|
self._seen.remove(name)
|
|
70
|
-
raise
|
|
91
|
+
raise MissingKeyError(name)
|
|
71
92
|
|
|
72
93
|
# call (nested token)
|
|
73
94
|
def _eval_call(self, node: CallNode):
|
|
@@ -88,7 +109,7 @@ class Evaluator:
|
|
|
88
109
|
|
|
89
110
|
# syntax-bound maps
|
|
90
111
|
for m in self.layerstack.all_maps():
|
|
91
|
-
if getattr(m,
|
|
112
|
+
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
92
113
|
continue
|
|
93
114
|
|
|
94
115
|
d = self._map_to_dict(m)
|
|
@@ -98,15 +119,15 @@ class Evaluator:
|
|
|
98
119
|
return str(val)
|
|
99
120
|
|
|
100
121
|
self._seen.remove(outer)
|
|
101
|
-
raise
|
|
122
|
+
raise MissingKeyError(f'{outer}({arg_value})')
|
|
102
123
|
|
|
103
124
|
# helpers
|
|
104
125
|
def _map_to_dict(self, m):
|
|
105
|
-
if hasattr(m,
|
|
126
|
+
if hasattr(m, 'as_map'):
|
|
106
127
|
return m.as_map()
|
|
107
128
|
if isinstance(m, dict):
|
|
108
129
|
return m
|
|
109
|
-
raise EvaluationError(f
|
|
130
|
+
raise EvaluationError(f'Invalid map object: {m}')
|
|
110
131
|
|
|
111
132
|
def _lookup_hierarchical(self, root, parts):
|
|
112
133
|
cur = root
|
|
@@ -117,3 +138,22 @@ class Evaluator:
|
|
|
117
138
|
return None
|
|
118
139
|
cur = cur[p]
|
|
119
140
|
return cur
|
|
141
|
+
|
|
142
|
+
def _has_recursive_maps(self):
|
|
143
|
+
for m in self.layerstack.all_maps():
|
|
144
|
+
if getattr(m, '__recursive__', False):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def _get_max_depth(self):
|
|
149
|
+
depths = [
|
|
150
|
+
getattr(m, '__max_depth__', 10)
|
|
151
|
+
for m in self.layerstack.all_maps()
|
|
152
|
+
if getattr(m, '__recursive__', False)
|
|
153
|
+
]
|
|
154
|
+
return max(depths) if depths else 0
|
|
155
|
+
|
|
156
|
+
def _re_resolve(self, text: str) -> str:
|
|
157
|
+
tokens = Tokenizer(text).tokenize()
|
|
158
|
+
ast = Parser(tokens).parse()
|
|
159
|
+
return self.evaluate(ast)
|
|
@@ -3,7 +3,7 @@ from .parser import Parser
|
|
|
3
3
|
from .evaluator import Evaluator
|
|
4
4
|
from .layers import LayerStack
|
|
5
5
|
from .cache import LRUCache
|
|
6
|
-
from .exceptions import MapResError
|
|
6
|
+
from .exceptions import MapResError, MissingKeyError
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MapResolver:
|
|
@@ -65,8 +65,18 @@ class MapResolver:
|
|
|
65
65
|
|
|
66
66
|
return result
|
|
67
67
|
|
|
68
|
+
except MissingKeyError as exc:
|
|
69
|
+
mode = self._resolve_missing_key_mode()
|
|
70
|
+
if mode == 'error':
|
|
71
|
+
raise MapResError(f'Resolver error: {exc}') from exc
|
|
72
|
+
if mode == 'silent':
|
|
73
|
+
return text
|
|
74
|
+
if mode == 'placeholder':
|
|
75
|
+
return f'<missing:{exc.key}>'
|
|
76
|
+
raise MapResError(f'Resolver error: {exc}') from exc
|
|
77
|
+
|
|
68
78
|
except Exception as exc:
|
|
69
|
-
raise MapResError(f
|
|
79
|
+
raise MapResError(f'Resolver error: {exc}') from exc
|
|
70
80
|
|
|
71
81
|
# ---------------------------------------
|
|
72
82
|
# LAYERSTACK BUILDING
|
|
@@ -92,6 +102,13 @@ class MapResolver:
|
|
|
92
102
|
|
|
93
103
|
return self.layers
|
|
94
104
|
|
|
105
|
+
def _resolve_missing_key_mode(self):
|
|
106
|
+
for m in self.layers.all_maps():
|
|
107
|
+
mode = getattr(m, '__missing_key__', None)
|
|
108
|
+
if mode is not None:
|
|
109
|
+
return mode
|
|
110
|
+
return 'error'
|
|
111
|
+
|
|
95
112
|
|
|
96
113
|
# ---------------------------------------
|
|
97
114
|
# SIMPLE GLOBAL USAGE
|
|
@@ -28,8 +28,8 @@ class Token:
|
|
|
28
28
|
|
|
29
29
|
def __repr__(self):
|
|
30
30
|
if self.value is not None:
|
|
31
|
-
return f
|
|
32
|
-
return f
|
|
31
|
+
return f'Token({self.type}, {self.value!r}, pos={self.pos})'
|
|
32
|
+
return f'Token({self.type}, pos={self.pos})'
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class Tokenizer:
|
|
@@ -38,10 +38,15 @@ class Tokenizer:
|
|
|
38
38
|
self.i = 0
|
|
39
39
|
self.n = len(text)
|
|
40
40
|
self.tokens = []
|
|
41
|
+
|
|
42
|
+
# toggle states
|
|
41
43
|
self.in_pipe = False
|
|
42
44
|
self.in_percent = False
|
|
43
45
|
self.in_colon = False
|
|
44
46
|
|
|
47
|
+
# mixed states
|
|
48
|
+
self.in_dollar = False
|
|
49
|
+
|
|
45
50
|
def peek(self, k=0):
|
|
46
51
|
idx = self.i + k
|
|
47
52
|
if idx >= self.n:
|
|
@@ -68,6 +73,7 @@ class Tokenizer:
|
|
|
68
73
|
while self.i < self.n:
|
|
69
74
|
ch = self.peek()
|
|
70
75
|
|
|
76
|
+
# escapes
|
|
71
77
|
if self.match(r'\:'):
|
|
72
78
|
buf.append(':'); self.advance(2); continue
|
|
73
79
|
if self.match(r'\<'):
|
|
@@ -89,43 +95,57 @@ class Tokenizer:
|
|
|
89
95
|
if self.match(r'\)'):
|
|
90
96
|
buf.append(')'); self.advance(2); continue
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
# --- paired delimiters ---
|
|
99
|
+
|
|
100
|
+
# braces {{ }}
|
|
101
|
+
if self.match('{{'):
|
|
93
102
|
flush_text()
|
|
94
103
|
self.emit(TokenType.BRACE_OPEN)
|
|
95
104
|
self.advance(2)
|
|
96
105
|
continue
|
|
97
106
|
|
|
98
|
-
if self.match(
|
|
107
|
+
if self.match('}}'):
|
|
99
108
|
flush_text()
|
|
100
109
|
self.emit(TokenType.BRACE_CLOSE)
|
|
101
110
|
self.advance(2)
|
|
102
111
|
continue
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
# angles < >
|
|
114
|
+
if self.match('<'):
|
|
105
115
|
flush_text()
|
|
106
|
-
self.emit(TokenType.
|
|
107
|
-
self.advance(
|
|
116
|
+
self.emit(TokenType.ANGLE_OPEN)
|
|
117
|
+
self.advance()
|
|
108
118
|
continue
|
|
109
119
|
|
|
110
|
-
if self.match(
|
|
120
|
+
if self.match('>'):
|
|
111
121
|
flush_text()
|
|
112
|
-
self.emit(TokenType.
|
|
122
|
+
self.emit(TokenType.ANGLE_CLOSE)
|
|
113
123
|
self.advance()
|
|
114
124
|
continue
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
# --- mixed delimiters (dollars) ---
|
|
127
|
+
|
|
128
|
+
if self.match('${'):
|
|
117
129
|
flush_text()
|
|
118
|
-
self.emit(TokenType.
|
|
119
|
-
self.
|
|
130
|
+
self.emit(TokenType.DOLLAR_OPEN)
|
|
131
|
+
self.in_dollar = True
|
|
132
|
+
self.advance(2)
|
|
120
133
|
continue
|
|
121
134
|
|
|
122
|
-
if
|
|
135
|
+
if ch == '}':
|
|
123
136
|
flush_text()
|
|
124
|
-
self.
|
|
137
|
+
if self.in_dollar:
|
|
138
|
+
self.emit(TokenType.DOLLAR_CLOSE)
|
|
139
|
+
self.in_dollar = False
|
|
140
|
+
else:
|
|
141
|
+
buf.append('}')
|
|
125
142
|
self.advance()
|
|
126
143
|
continue
|
|
127
144
|
|
|
128
|
-
|
|
145
|
+
# --- toggle delimiters ---
|
|
146
|
+
|
|
147
|
+
# pipes |...|
|
|
148
|
+
if self.match('|'):
|
|
129
149
|
flush_text()
|
|
130
150
|
if not self.in_pipe:
|
|
131
151
|
self.emit(TokenType.PIPE_OPEN)
|
|
@@ -136,7 +156,8 @@ class Tokenizer:
|
|
|
136
156
|
self.advance()
|
|
137
157
|
continue
|
|
138
158
|
|
|
139
|
-
|
|
159
|
+
# percents %...%
|
|
160
|
+
if self.match('%'):
|
|
140
161
|
flush_text()
|
|
141
162
|
if not self.in_percent:
|
|
142
163
|
self.emit(TokenType.PERCENT_OPEN)
|
|
@@ -147,7 +168,8 @@ class Tokenizer:
|
|
|
147
168
|
self.advance()
|
|
148
169
|
continue
|
|
149
170
|
|
|
150
|
-
|
|
171
|
+
# colons :...:
|
|
172
|
+
if self.match(':'):
|
|
151
173
|
flush_text()
|
|
152
174
|
if not self.in_colon:
|
|
153
175
|
self.emit(TokenType.COLON_OPEN)
|
|
@@ -158,27 +180,51 @@ class Tokenizer:
|
|
|
158
180
|
self.advance()
|
|
159
181
|
continue
|
|
160
182
|
|
|
161
|
-
|
|
183
|
+
# --- parens ---
|
|
184
|
+
|
|
185
|
+
if ch == '(':
|
|
162
186
|
flush_text()
|
|
163
187
|
self.emit(TokenType.LPAREN)
|
|
164
188
|
self.advance()
|
|
165
189
|
continue
|
|
166
190
|
|
|
167
|
-
if ch ==
|
|
191
|
+
if ch == ')':
|
|
168
192
|
flush_text()
|
|
169
193
|
self.emit(TokenType.RPAREN)
|
|
170
194
|
self.advance()
|
|
171
195
|
continue
|
|
172
196
|
|
|
173
|
-
|
|
197
|
+
# --- IDENT (your rules) ---
|
|
198
|
+
|
|
199
|
+
if ch.isalnum():
|
|
174
200
|
flush_text()
|
|
175
201
|
start = self.i
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
|
|
203
|
+
# first char: [a-zA-Z0-9]
|
|
204
|
+
self.advance()
|
|
205
|
+
|
|
206
|
+
# middle chars: [a-zA-Z0-9._]*
|
|
207
|
+
while True:
|
|
208
|
+
nxt = self.peek()
|
|
209
|
+
if not nxt:
|
|
210
|
+
break
|
|
211
|
+
if nxt.isalnum() or nxt in '._':
|
|
212
|
+
self.advance()
|
|
213
|
+
else:
|
|
214
|
+
break
|
|
215
|
+
|
|
178
216
|
ident = self.text[start:self.i]
|
|
179
|
-
|
|
217
|
+
|
|
218
|
+
# enforce: no leading/trailing '_', no '-'
|
|
219
|
+
if ident.startswith('_') or ident.endswith('_') or '-' in ident:
|
|
220
|
+
# treat as TEXT instead
|
|
221
|
+
buf.append(ident)
|
|
222
|
+
else:
|
|
223
|
+
self.emit(TokenType.IDENT, ident)
|
|
180
224
|
continue
|
|
181
225
|
|
|
226
|
+
# --- fallback: TEXT ---
|
|
227
|
+
|
|
182
228
|
buf.append(ch)
|
|
183
229
|
self.advance()
|
|
184
230
|
|
|
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
|