mapres 3a1__tar.gz → 3a2__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-3a1/src/mapres.egg-info → mapres-3a2}/PKG-INFO +1 -1
- {mapres-3a1 → mapres-3a2}/pyproject.toml +1 -1
- {mapres-3a1 → mapres-3a2}/src/mapres/datamap.py +1 -1
- {mapres-3a1 → mapres-3a2}/src/mapres/evaluator.py +62 -16
- mapres-3a2/src/mapres/exceptions.py +8 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/resolver.py +41 -37
- {mapres-3a1 → mapres-3a2/src/mapres.egg-info}/PKG-INFO +1 -1
- mapres-3a1/src/mapres/exceptions.py +0 -10
- {mapres-3a1 → mapres-3a2}/LICENSE +0 -0
- {mapres-3a1 → mapres-3a2}/README.md +0 -0
- {mapres-3a1 → mapres-3a2}/setup.cfg +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/__init__.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/ast.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/cache.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/layers.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/maps/__init__.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/maps/color.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/maps/time.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/parser.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres/tokenizer.py +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres.egg-info/SOURCES.txt +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-3a1 → mapres-3a2}/src/mapres.egg-info/top_level.txt +0 -0
|
@@ -35,7 +35,7 @@ class DataMap:
|
|
|
35
35
|
'''
|
|
36
36
|
__syntax__: str = None
|
|
37
37
|
__mode__: str | None = None # 'dynamic', 'config', 'callable', or None
|
|
38
|
-
__missing_key__: str = 'error' # 'error', 'silent', 'placeholder'
|
|
38
|
+
__missing_key__: str = 'error' # 'error', 'silent', 'placeholder', or 'raw'
|
|
39
39
|
__recursive__: bool = False
|
|
40
40
|
__max_depth__: int = 10
|
|
41
41
|
|
|
@@ -9,10 +9,20 @@ class EvaluationError(MapResError):
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Evaluator:
|
|
12
|
-
def __init__(self, layerstack, ctx=None):
|
|
12
|
+
def __init__(self, layerstack, ctx=None, missing_key_mode: str = 'error'):
|
|
13
|
+
'''
|
|
14
|
+
layerstack: LayerStack instance
|
|
15
|
+
ctx: dict used as highest-priority map
|
|
16
|
+
missing_key_mode:
|
|
17
|
+
- 'error' → raise MissingKeyError
|
|
18
|
+
- 'silent' → return raw token unchanged
|
|
19
|
+
- 'placeholder' → return <missing:key>
|
|
20
|
+
- 'raw' → return raw token unchanged (alias of 'silent' at token level)
|
|
21
|
+
'''
|
|
13
22
|
self.layerstack = layerstack
|
|
14
23
|
self.ctx = ctx or {}
|
|
15
24
|
self._seen = set() # cycle detection
|
|
25
|
+
self.missing_key_mode = missing_key_mode
|
|
16
26
|
|
|
17
27
|
# public api
|
|
18
28
|
def evaluate(self, node):
|
|
@@ -33,14 +43,13 @@ class Evaluator:
|
|
|
33
43
|
return self._eval_call(node)
|
|
34
44
|
|
|
35
45
|
raise EvaluationError(f'Unknown AST node: {node}')
|
|
36
|
-
|
|
46
|
+
|
|
37
47
|
# re-resolve
|
|
38
48
|
def _re_resolve(self, text: str) -> str:
|
|
39
49
|
tokens = Tokenizer(text).tokenize()
|
|
40
50
|
ast = Parser(tokens).parse()
|
|
41
51
|
return self.evaluate(ast)
|
|
42
52
|
|
|
43
|
-
|
|
44
53
|
# template
|
|
45
54
|
def _eval_template(self, node: TemplateNode):
|
|
46
55
|
parts = []
|
|
@@ -48,34 +57,26 @@ class Evaluator:
|
|
|
48
57
|
parts.append(self._eval(child))
|
|
49
58
|
out = ''.join(parts)
|
|
50
59
|
|
|
51
|
-
#
|
|
52
|
-
# PER-MAP RECURSION (syntax-scoped)
|
|
53
|
-
# ---------------------------------------
|
|
54
|
-
# Find all maps in the layerstack that have recursive=True
|
|
60
|
+
# per-map recursion (syntax-scoped)
|
|
55
61
|
recursive_maps = [
|
|
56
62
|
m for m in self.layerstack.all_maps()
|
|
57
63
|
if getattr(m, '__recursive__', False) is True
|
|
58
64
|
]
|
|
59
65
|
|
|
60
|
-
# If no maps are recursive, return the first-pass output
|
|
61
66
|
if not recursive_maps:
|
|
62
67
|
return out
|
|
63
68
|
|
|
64
|
-
# Determine max recursion depth across all recursive maps
|
|
65
69
|
max_depth = max(getattr(m, '__max_depth__', 10) for m in recursive_maps)
|
|
66
70
|
|
|
67
71
|
seen = set()
|
|
68
72
|
depth = 0
|
|
69
73
|
|
|
70
74
|
while depth < max_depth:
|
|
71
|
-
# Re-tokenize and re-evaluate the ENTIRE output string
|
|
72
75
|
new_out = self._re_resolve(out)
|
|
73
76
|
|
|
74
|
-
# If stable, stop recursion
|
|
75
77
|
if new_out == out:
|
|
76
78
|
break
|
|
77
79
|
|
|
78
|
-
# Cycle detection
|
|
79
80
|
if new_out in seen:
|
|
80
81
|
raise EvaluationError('Cycle detected in recursive map resolution')
|
|
81
82
|
|
|
@@ -91,7 +92,7 @@ class Evaluator:
|
|
|
91
92
|
|
|
92
93
|
# cycle detection
|
|
93
94
|
if name in self._seen:
|
|
94
|
-
raise EvaluationError(f
|
|
95
|
+
raise EvaluationError(f'Cycle detected in identifier \'{name}\'')
|
|
95
96
|
self._seen.add(name)
|
|
96
97
|
|
|
97
98
|
parts = name.split('.')
|
|
@@ -114,14 +115,14 @@ class Evaluator:
|
|
|
114
115
|
return str(val)
|
|
115
116
|
|
|
116
117
|
self._seen.remove(name)
|
|
117
|
-
|
|
118
|
+
return self._handle_missing_ident(node)
|
|
118
119
|
|
|
119
120
|
# call (nested token)
|
|
120
121
|
def _eval_call(self, node: CallNode):
|
|
121
122
|
outer = node.name
|
|
122
123
|
|
|
123
124
|
if outer in self._seen:
|
|
124
|
-
raise EvaluationError(f
|
|
125
|
+
raise EvaluationError(f'Cycle detected in call \'{outer}\'')
|
|
125
126
|
self._seen.add(outer)
|
|
126
127
|
|
|
127
128
|
arg_value = self._eval(node.arg)
|
|
@@ -145,7 +146,7 @@ class Evaluator:
|
|
|
145
146
|
return str(val)
|
|
146
147
|
|
|
147
148
|
self._seen.remove(outer)
|
|
148
|
-
|
|
149
|
+
return self._handle_missing_call(node, arg_value)
|
|
149
150
|
|
|
150
151
|
# helpers
|
|
151
152
|
def _map_to_dict(self, m):
|
|
@@ -164,3 +165,48 @@ class Evaluator:
|
|
|
164
165
|
return None
|
|
165
166
|
cur = cur[p]
|
|
166
167
|
return cur
|
|
168
|
+
|
|
169
|
+
# missing-key handling (token-level)
|
|
170
|
+
def _delims_for_syntax(self, syntax_name: str):
|
|
171
|
+
table = {
|
|
172
|
+
'angles': ('<', '>'),
|
|
173
|
+
'braces': ('{{', '}}'),
|
|
174
|
+
'dollars': ('${', '}'),
|
|
175
|
+
'percents': ('%', '%'),
|
|
176
|
+
'pipes': ('|', '|'),
|
|
177
|
+
'colons': (':', ':'),
|
|
178
|
+
}
|
|
179
|
+
return table.get(syntax_name, ('<', '>'))
|
|
180
|
+
|
|
181
|
+
def _handle_missing_ident(self, node: IdentNode):
|
|
182
|
+
name = node.name
|
|
183
|
+
open_, close_ = self._delims_for_syntax(node.syntax)
|
|
184
|
+
raw_token = f'{open_}{name}{close_}'
|
|
185
|
+
|
|
186
|
+
mode = self.missing_key_mode or 'error'
|
|
187
|
+
|
|
188
|
+
if mode in ('silent', 'raw'):
|
|
189
|
+
# token-level fallback: leave the unresolved token as-is
|
|
190
|
+
return raw_token
|
|
191
|
+
|
|
192
|
+
if mode == 'placeholder':
|
|
193
|
+
return f'<missing:{name}>'
|
|
194
|
+
|
|
195
|
+
# default: error
|
|
196
|
+
raise MissingKeyError(name, token=raw_token)
|
|
197
|
+
|
|
198
|
+
def _handle_missing_call(self, node: CallNode, arg_value: str):
|
|
199
|
+
outer = node.name
|
|
200
|
+
open_, close_ = self._delims_for_syntax(node.syntax)
|
|
201
|
+
raw_token = f'{open_}{outer}({arg_value}){close_}'
|
|
202
|
+
key_repr = f'{outer}({arg_value})'
|
|
203
|
+
|
|
204
|
+
mode = self.missing_key_mode or 'error'
|
|
205
|
+
|
|
206
|
+
if mode in ('silent', 'raw'):
|
|
207
|
+
return raw_token
|
|
208
|
+
|
|
209
|
+
if mode == 'placeholder':
|
|
210
|
+
return f'<missing:{key_repr}>'
|
|
211
|
+
|
|
212
|
+
raise MissingKeyError(key_repr, token=raw_token)
|
|
@@ -3,12 +3,12 @@ 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
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MapResolver:
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
'''
|
|
11
|
+
AST-based resolver for mapres.
|
|
12
12
|
|
|
13
13
|
Pipeline:
|
|
14
14
|
tokenize → parse → evaluate → return string
|
|
@@ -19,37 +19,44 @@ class MapResolver:
|
|
|
19
19
|
- multiple map layers
|
|
20
20
|
- ctx fallback
|
|
21
21
|
- caching (optional)
|
|
22
|
-
|
|
22
|
+
- per-map missing-key modes via __missing_key__
|
|
23
|
+
'''
|
|
23
24
|
|
|
24
|
-
def __init__(self, layers=None, cache=False, cache_size=1024):
|
|
25
|
+
def __init__(self, layers=None, cache: bool = False, cache_size: int = 1024):
|
|
25
26
|
self.layers = layers or LayerStack()
|
|
26
27
|
self.cache_enabled = cache
|
|
27
28
|
self.cache = LRUCache(cache_size) if cache else None
|
|
28
29
|
|
|
29
|
-
#
|
|
30
|
-
# PUBLIC API
|
|
31
|
-
# ---------------------------------------
|
|
30
|
+
# public api
|
|
32
31
|
def res(self, text: str, *, extra_maps=None, override_maps=None, **ctx) -> str:
|
|
33
|
-
|
|
32
|
+
'''
|
|
34
33
|
Resolve a template string using:
|
|
35
34
|
- tokenizer
|
|
36
35
|
- parser
|
|
37
36
|
- evaluator
|
|
38
37
|
- layerstack
|
|
39
38
|
- ctx
|
|
40
|
-
|
|
39
|
+
|
|
40
|
+
Missing-key behavior is controlled by:
|
|
41
|
+
- datamap.__missing_key__ on any map in the layerstack
|
|
42
|
+
('error', 'silent', 'placeholder', 'raw')
|
|
43
|
+
'''
|
|
41
44
|
|
|
42
45
|
try:
|
|
43
46
|
# caching
|
|
47
|
+
cache_key = None
|
|
44
48
|
if self.cache_enabled:
|
|
45
|
-
|
|
46
|
-
cached = self.cache.get(
|
|
49
|
+
cache_key = (text, tuple(sorted(ctx.items())))
|
|
50
|
+
cached = self.cache.get(cache_key)
|
|
47
51
|
if cached is not None:
|
|
48
52
|
return cached
|
|
49
53
|
|
|
50
54
|
# build working layerstack
|
|
51
55
|
layerstack = self._build_layerstack(extra_maps, override_maps)
|
|
52
56
|
|
|
57
|
+
# determine missing-key mode
|
|
58
|
+
missing_mode = self._resolve_missing_key_mode(layerstack)
|
|
59
|
+
|
|
53
60
|
# tokenize
|
|
54
61
|
tokens = Tokenizer(text).tokenize()
|
|
55
62
|
|
|
@@ -57,38 +64,32 @@ class MapResolver:
|
|
|
57
64
|
ast = Parser(tokens).parse()
|
|
58
65
|
|
|
59
66
|
# evaluate
|
|
60
|
-
|
|
67
|
+
evaluator = Evaluator(layerstack, ctx, missing_key_mode=missing_mode)
|
|
68
|
+
result = evaluator.evaluate(ast)
|
|
61
69
|
|
|
62
70
|
# store cache
|
|
63
|
-
if self.cache_enabled:
|
|
64
|
-
self.cache.set(
|
|
71
|
+
if self.cache_enabled and cache_key is not None:
|
|
72
|
+
self.cache.set(cache_key, result)
|
|
65
73
|
|
|
66
74
|
return result
|
|
67
75
|
|
|
68
|
-
except
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
76
|
+
except MapResError as exc:
|
|
77
|
+
# propagate MapResError as-is
|
|
78
|
+
raise
|
|
77
79
|
|
|
78
80
|
except Exception as exc:
|
|
81
|
+
# wrap any other exception
|
|
79
82
|
raise MapResError(f'Resolver error: {exc}') from exc
|
|
80
83
|
|
|
81
|
-
#
|
|
82
|
-
# LAYERSTACK BUILDING
|
|
83
|
-
# ---------------------------------------
|
|
84
|
+
# layerstack building
|
|
84
85
|
def _build_layerstack(self, extra_maps, override_maps):
|
|
85
|
-
|
|
86
|
+
'''
|
|
86
87
|
Build a temporary layerstack for this resolution call.
|
|
87
88
|
Supports:
|
|
88
89
|
- override maps (priority 0)
|
|
89
90
|
- extra maps (priority 999)
|
|
90
91
|
- normal layers (existing priorities)
|
|
91
|
-
|
|
92
|
+
'''
|
|
92
93
|
|
|
93
94
|
if override_maps is not None:
|
|
94
95
|
temp = LayerStack()
|
|
@@ -102,18 +103,21 @@ class MapResolver:
|
|
|
102
103
|
|
|
103
104
|
return self.layers
|
|
104
105
|
|
|
105
|
-
def _resolve_missing_key_mode(self):
|
|
106
|
-
|
|
106
|
+
def _resolve_missing_key_mode(self, layerstack: LayerStack) -> str:
|
|
107
|
+
'''
|
|
108
|
+
Determine missing-key mode from maps in the given layerstack.
|
|
109
|
+
|
|
110
|
+
First map that defines __missing_key__ wins.
|
|
111
|
+
Defaults to 'error' if none are set.
|
|
112
|
+
'''
|
|
113
|
+
for m in layerstack.all_maps():
|
|
107
114
|
mode = getattr(m, '__missing_key__', None)
|
|
108
115
|
if mode is not None:
|
|
109
116
|
return mode
|
|
110
117
|
return 'error'
|
|
111
118
|
|
|
112
119
|
|
|
113
|
-
#
|
|
114
|
-
# SIMPLE GLOBAL USAGE
|
|
115
|
-
# ---------------------------------------
|
|
116
|
-
|
|
120
|
+
# simple global usage
|
|
117
121
|
_DEFAULT_RESOLVER = MapResolver()
|
|
118
122
|
|
|
119
123
|
def setGlobalMaps(*maps, priority=0):
|
|
@@ -146,11 +150,11 @@ def setGlobalMaps(*maps, priority=0):
|
|
|
146
150
|
|
|
147
151
|
|
|
148
152
|
def res(text: str, **ctx) -> str:
|
|
149
|
-
'''Simple global resolution'''
|
|
153
|
+
'''Simple global resolution.'''
|
|
150
154
|
return _DEFAULT_RESOLVER.res(text, **ctx)
|
|
151
155
|
|
|
152
156
|
def rprint(text: str, sep, end, file, flush, **ctx) -> None:
|
|
153
|
-
'''Simple global resolution and print'''
|
|
157
|
+
'''Simple global resolution and print.'''
|
|
154
158
|
print(res(text, **ctx), sep=sep, end=end, file=file, flush=flush)
|
|
155
159
|
|
|
156
160
|
res.setGlobalMaps = setGlobalMaps
|
|
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
|