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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 3a1
3
+ Version: 3a2
4
4
  Summary: A powerfull mapping utility
5
5
  Author: iFamished
6
6
  License: MIT
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "mapres"
10
- version = "3-alpha.1"
10
+ version = "3-alpha.2"
11
11
  description = "A powerfull mapping utility"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -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"Cycle detected in identifier '{name}'")
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
- raise MissingKeyError(name)
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"Cycle detected in call '{outer}'")
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
- raise MissingKeyError(f'{outer}({arg_value})')
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)
@@ -0,0 +1,8 @@
1
+ class MapResError(Exception):
2
+ pass
3
+
4
+ class MissingKeyError(MapResError):
5
+ def __init__(self, key: str, token: str | None = None):
6
+ super().__init__(f"Missing key {key!r}")
7
+ self.key = key
8
+ self.token = token or key
@@ -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, MissingKeyError
6
+ from .exceptions import MapResError
7
7
 
8
8
 
9
9
  class MapResolver:
10
- """
11
- New AST-based resolver for mapres.
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
- key = (text, tuple(sorted(ctx.items())))
46
- cached = self.cache.get(key)
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
- result = Evaluator(layerstack, ctx).evaluate(ast)
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(key, result)
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 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
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
- for m in self.layers.all_maps():
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 3a1
3
+ Version: 3a2
4
4
  Summary: A powerfull mapping utility
5
5
  Author: iFamished
6
6
  License: MIT
@@ -1,10 +0,0 @@
1
- class MapResError(Exception):
2
- pass
3
-
4
- class MissingKeyError(MapResError):
5
- def __init__(self, key):
6
- super().__init__(f'Missing key {key!r}')
7
- self.key = key
8
-
9
- class MapSyntaxError(MapResError):
10
- pass
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