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.
Files changed (23) hide show
  1. {mapres-3.dev14/src/mapres.egg-info → mapres-3.dev15}/PKG-INFO +1 -1
  2. {mapres-3.dev14 → mapres-3.dev15}/pyproject.toml +1 -1
  3. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/datamap.py +35 -3
  4. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/evaluator.py +50 -10
  5. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/exceptions.py +3 -1
  6. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/resolver.py +19 -2
  7. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/tokenizer.py +69 -23
  8. {mapres-3.dev14 → mapres-3.dev15/src/mapres.egg-info}/PKG-INFO +1 -1
  9. {mapres-3.dev14 → mapres-3.dev15}/LICENSE +0 -0
  10. {mapres-3.dev14 → mapres-3.dev15}/README.md +0 -0
  11. {mapres-3.dev14 → mapres-3.dev15}/setup.cfg +0 -0
  12. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/__init__.py +0 -0
  13. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/ast.py +0 -0
  14. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/cache.py +0 -0
  15. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/layers.py +0 -0
  16. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/maps/__init__.py +0 -0
  17. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/maps/color.py +0 -0
  18. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/maps/time.py +0 -0
  19. {mapres-3.dev14 → mapres-3.dev15}/src/mapres/parser.py +0 -0
  20. {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/SOURCES.txt +0 -0
  21. {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/dependency_links.txt +0 -0
  22. {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/requires.txt +0 -0
  23. {mapres-3.dev14 → mapres-3.dev15}/src/mapres.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 3.dev14
3
+ Version: 3.dev15
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-dev.14"
10
+ version = "3-dev.15"
11
11
  description = "A powerfull mapping utility"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -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 # "dynamic", "config", "callable", or 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(Exception):
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"Unknown AST node: {node}")
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
- return "".join(parts)
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, "__syntax__", None) not in (None, node.syntax):
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 EvaluationError(f"Missing key '{name}'")
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, "__syntax__", None) not in (None, node.syntax):
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 EvaluationError(f"Missing nested key '{outer}({arg_value})'")
122
+ raise MissingKeyError(f'{outer}({arg_value})')
102
123
 
103
124
  # helpers
104
125
  def _map_to_dict(self, m):
105
- if hasattr(m, "as_map"):
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"Invalid map object: {m}")
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)
@@ -2,7 +2,9 @@ class MapResError(Exception):
2
2
  pass
3
3
 
4
4
  class MissingKeyError(MapResError):
5
- pass
5
+ def __init__(self, key):
6
+ super().__init__(f'Missing key {key!r}')
7
+ self.key = key
6
8
 
7
9
  class MapSyntaxError(MapResError):
8
10
  pass
@@ -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"Resolver error: {exc}") from exc
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"Token({self.type}, {self.value!r}, pos={self.pos})"
32
- return f"Token({self.type}, pos={self.pos})"
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
- if self.match("{{"):
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
- if self.match("${"):
113
+ # angles < >
114
+ if self.match('<'):
105
115
  flush_text()
106
- self.emit(TokenType.DOLLAR_OPEN)
107
- self.advance(2)
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.DOLLAR_CLOSE)
122
+ self.emit(TokenType.ANGLE_CLOSE)
113
123
  self.advance()
114
124
  continue
115
125
 
116
- if self.match("<"):
126
+ # --- mixed delimiters (dollars) ---
127
+
128
+ if self.match('${'):
117
129
  flush_text()
118
- self.emit(TokenType.ANGLE_OPEN)
119
- self.advance()
130
+ self.emit(TokenType.DOLLAR_OPEN)
131
+ self.in_dollar = True
132
+ self.advance(2)
120
133
  continue
121
134
 
122
- if self.match(">"):
135
+ if ch == '}':
123
136
  flush_text()
124
- self.emit(TokenType.ANGLE_CLOSE)
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
- if self.match("|"):
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
- if self.match("%"):
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
- if self.match(":"):
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
- if ch == "(":
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
- if ch.isalpha():
197
+ # --- IDENT (your rules) ---
198
+
199
+ if ch.isalnum():
174
200
  flush_text()
175
201
  start = self.i
176
- while self.peek().isalnum() or self.peek() in "._":
177
- self.advance()
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
- self.emit(TokenType.IDENT, ident)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 3.dev14
3
+ Version: 3.dev15
4
4
  Summary: A powerfull mapping utility
5
5
  Author: iFamished
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes