mapres 2.4.dev6__py3-none-any.whl → 3a2__py3-none-any.whl

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/__init__.py CHANGED
@@ -7,7 +7,7 @@ try:
7
7
  except PackageNotFoundError:
8
8
  __version__ = '0.0.0'
9
9
 
10
- from .resolver import MapResolver, res, setGlobalMaps
10
+ from .resolver import MapResolver, res, rprint, setGlobalMaps
11
11
  from .datamap import DataMap, datamap, syntax
12
12
  from .layers import Layer, LayerStack
13
13
 
@@ -28,6 +28,10 @@ maps = SimpleNamespace(
28
28
  time = time,
29
29
  )
30
30
 
31
+ utils = SimpleNamespace(
32
+ rprint = rprint,
33
+ )
34
+
31
35
  __all__ = [
32
36
  # core modules
33
37
  'MapResolver',
@@ -39,6 +43,9 @@ __all__ = [
39
43
  'LayerStack',
40
44
  'setGlobalMaps',
41
45
 
46
+ # utils
47
+ 'rprint',
48
+
42
49
  # maps
43
50
  'ColorMap',
44
51
  'ascii_colors',
@@ -49,4 +56,5 @@ __all__ = [
49
56
 
50
57
  # namespaces
51
58
  'maps',
59
+ 'utils',
52
60
  ]
mapres/ast.py ADDED
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Node:
6
+ pass
7
+
8
+
9
+ @dataclass
10
+ class TemplateNode(Node):
11
+ children: list[Node]
12
+
13
+
14
+ @dataclass
15
+ class TextNode(Node):
16
+ text: str
17
+
18
+
19
+ @dataclass
20
+ class IdentNode(Node):
21
+ name: str
22
+ syntax: str
23
+
24
+
25
+ @dataclass
26
+ class CallNode(Node):
27
+ name: str # outer identifier
28
+ arg: Node # nested TemplateNode
29
+ syntax: str # which delimiter type produced it
mapres/datamap.py CHANGED
@@ -1,64 +1,160 @@
1
- import re
2
1
  from dataclasses import dataclass, fields, is_dataclass
3
2
 
3
+ # syntax identifiers
4
4
  class syntax:
5
- braces = r'\{([^{}]+)\}'
6
- dollars = r'\$\{([^{}]+)\}'
7
- angles = r'<([^<>]+)>'
8
- percents = r'%([^%]+)%'
5
+ braces = 'braces' # {{value}}
6
+ dollars = 'dollars' # ${value}
7
+ angles = 'angles' # <value>
8
+ percents = 'percents' # %value%
9
+ pipes = 'pipes' # |value|
10
+ colons = 'colons' # :value:
11
+ at_tags = 'at_tags' # @value@
12
+ hash_tags = 'hash_tags' # #value#
13
+ paren_dollars = 'paren_dollars' # $(value)
9
14
 
10
15
 
16
+ # base datamap class
11
17
  @dataclass
12
18
  class DataMap:
13
- '''Core datamap class. Contains logic used to make datamaps'''
14
- __syntax__: str = syntax.braces
15
- __mode__: str | None = None
19
+ '''
20
+ Core datamap class for mapres 3.
21
+
22
+ Responsibilities:
23
+ - hold fields
24
+ - optionally evaluate dynamic providers
25
+ - return a dict via as_map()
26
+
27
+ Syntax metadata (__syntax__) determines which delimiters
28
+ this map responds to (angle, brace, colon, percent, pipe, etc).
29
+
30
+ Mode metadata (__mode__) controls behavior:
31
+ - None: plain static map
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
35
+ '''
36
+ __syntax__: str = None
37
+ __mode__: str | None = None # 'dynamic', 'config', 'callable', or None
38
+ __missing_key__: str = 'error' # 'error', 'silent', 'placeholder', or 'raw'
39
+ __recursive__: bool = False
40
+ __max_depth__: int = 10
16
41
 
17
42
  def as_map(self):
43
+ '''
44
+ Convert datamap fields into a dictionary.
45
+ If mode='dynamic', evaluate provider functions.
46
+
47
+ Callable mode does NOT change as_map() behavior; methods are
48
+ used by the evaluator, not exported as map entries.
49
+ '''
18
50
  result = {}
51
+
52
+ # dataclass fields
19
53
  for f in fields(self):
20
- if not f.init or f.name in ('__syntax__', '__mode__'):
54
+ if not f.init or f.name.startswith('__'):
21
55
  continue
56
+
22
57
  val = getattr(self, f.name)
58
+
59
+ # dynamic provider support
23
60
  if self.__mode__ == 'dynamic':
24
61
  providers = getattr(self, 'providers', None)
25
62
  if isinstance(providers, dict) and f.name in providers:
26
63
  val = providers[f.name]
27
64
  if callable(val):
28
65
  val = val()
66
+
29
67
  result[f.name] = val
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
+
30
76
  return result
31
77
 
32
- @classmethod
33
- def get_syntax(cls):
34
- return getattr(cls, '__syntax__', syntax.braces)
78
+ def _class_to_dict(self, cls):
79
+ out = {}
80
+
81
+ if is_dataclass(cls):
82
+ dummy = cls()
83
+ for f in fields(dummy):
84
+ if not f.init:
85
+ continue
86
+ out[f.name] = getattr(dummy, f.name)
87
+
88
+ for name, obj in cls.__dict__.items():
89
+ if name.startswith('__'):
90
+ continue
91
+ if isinstance(obj, type):
92
+ out[name] = self._class_to_dict(obj)
93
+
94
+ return out
35
95
 
36
96
 
37
- # decorator
38
- def datamap(
39
- _cls = None,
40
- *,
41
- syntax: str = syntax.braces,
42
- mode: bool | str | None = None,
43
- ):
44
- '''@datamap decorator with optional values'''
97
+ def datamap(_cls=None, *, syntax=None, mode=None, missing_key=None, recursive=None, max_depth=None):
45
98
  def wrap(cls):
46
99
  cls.__syntax__ = syntax
47
100
  cls.__mode__ = mode
101
+ cls.__missing_key__ = missing_key or 'error'
102
+ cls.__recursive__ = recursive or False
103
+ cls.__max_depth__ = max_depth or 10
104
+
48
105
  namespace = dict(cls.__dict__)
49
106
  namespace['__dict__'] = {}
50
- # preserve any rules class defined in the original namespace by attaching it to the generated class
51
107
  rules_obj = namespace.get('rules', None)
108
+
52
109
  cls = type(cls.__name__, (DataMap,), namespace)
53
- # attach rules (if present) to the new class so validators can read them via the datamap class
110
+
54
111
  if rules_obj is not None:
55
112
  setattr(cls, 'rules', rules_obj)
113
+
56
114
  return dataclass(frozen=False)(cls)
57
- return wrap if _cls is None else wrap(_cls)
58
115
 
116
+ if _cls is not None and isinstance(_cls, type):
117
+ return wrap(_cls)
118
+
119
+ return wrap
120
+
121
+
122
+ # syntax-bound decorator factories
123
+ class _SyntaxFactory:
124
+ def __init__(self, syntax_name):
125
+ self._syntax = syntax_name
126
+
127
+ def __call__(self, _cls=None, **kwargs):
128
+ if _cls is not None and isinstance(_cls, type):
129
+ return datamap(_cls, syntax=self._syntax)
130
+ return datamap(syntax=self._syntax, **kwargs)
131
+
132
+ def config(self, _cls=None, **kwargs):
133
+ if _cls is not None and isinstance(_cls, type):
134
+ return datamap(_cls, syntax=self._syntax, mode='config')
135
+ return datamap(syntax=self._syntax, mode='config', **kwargs)
136
+
137
+ def dynamic(self, _cls=None, **kwargs):
138
+ if _cls is not None and isinstance(_cls, type):
139
+ return datamap(_cls, syntax=self._syntax, mode='dynamic')
140
+ return datamap(syntax=self._syntax, mode='dynamic', **kwargs)
141
+
142
+ def callable(self, _cls=None, **kwargs):
143
+ if _cls is not None and isinstance(_cls, type):
144
+ return datamap(_cls, syntax=self._syntax, mode='callable')
145
+ return datamap(syntax=self._syntax, mode='callable', **kwargs)
146
+
147
+
148
+ datamap.braces = _SyntaxFactory(syntax.braces)
149
+ datamap.dollars = _SyntaxFactory(syntax.dollars)
150
+ datamap.angles = _SyntaxFactory(syntax.angles)
151
+ datamap.percents = _SyntaxFactory(syntax.percents)
152
+ datamap.pipes = _SyntaxFactory(syntax.pipes)
153
+ datamap.colons = _SyntaxFactory(syntax.colons)
154
+ datamap.at_tags = _SyntaxFactory(syntax.at_tags)
155
+ datamap.hash_tags = _SyntaxFactory(syntax.hash_tags)
156
+ datamap.paren_dollars = _SyntaxFactory(syntax.paren_dollars)
59
157
 
60
- # decorator shortcuts
61
- datamap.braces = datamap(syntax=syntax.braces)
62
- datamap.angles = datamap(syntax=syntax.angles)
63
- datamap.dollars = datamap(syntax=syntax.dollars)
64
- datamap.percents = datamap(syntax=syntax.percents)
158
+ datamap.config = datamap(mode='config')
159
+ datamap.dynamic = datamap(mode='dynamic')
160
+ datamap.callable = datamap(mode='callable')
mapres/evaluator.py ADDED
@@ -0,0 +1,212 @@
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, 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
+ '''
22
+ self.layerstack = layerstack
23
+ self.ctx = ctx or {}
24
+ self._seen = set() # cycle detection
25
+ self.missing_key_mode = missing_key_mode
26
+
27
+ # public api
28
+ def evaluate(self, node):
29
+ return self._eval(node)
30
+
31
+ # internal dispatch
32
+ def _eval(self, node):
33
+ if isinstance(node, TemplateNode):
34
+ return self._eval_template(node)
35
+
36
+ if isinstance(node, TextNode):
37
+ return node.text
38
+
39
+ if isinstance(node, IdentNode):
40
+ return self._eval_ident(node)
41
+
42
+ if isinstance(node, CallNode):
43
+ return self._eval_call(node)
44
+
45
+ raise EvaluationError(f'Unknown AST node: {node}')
46
+
47
+ # re-resolve
48
+ def _re_resolve(self, text: str) -> str:
49
+ tokens = Tokenizer(text).tokenize()
50
+ ast = Parser(tokens).parse()
51
+ return self.evaluate(ast)
52
+
53
+ # template
54
+ def _eval_template(self, node: TemplateNode):
55
+ parts = []
56
+ for child in node.children:
57
+ parts.append(self._eval(child))
58
+ out = ''.join(parts)
59
+
60
+ # per-map recursion (syntax-scoped)
61
+ recursive_maps = [
62
+ m for m in self.layerstack.all_maps()
63
+ if getattr(m, '__recursive__', False) is True
64
+ ]
65
+
66
+ if not recursive_maps:
67
+ return out
68
+
69
+ max_depth = max(getattr(m, '__max_depth__', 10) for m in recursive_maps)
70
+
71
+ seen = set()
72
+ depth = 0
73
+
74
+ while depth < max_depth:
75
+ new_out = self._re_resolve(out)
76
+
77
+ if new_out == out:
78
+ break
79
+
80
+ if new_out in seen:
81
+ raise EvaluationError('Cycle detected in recursive map resolution')
82
+
83
+ seen.add(new_out)
84
+ out = new_out
85
+ depth += 1
86
+
87
+ return out
88
+
89
+ # identifier
90
+ def _eval_ident(self, node: IdentNode):
91
+ name = node.name
92
+
93
+ # cycle detection
94
+ if name in self._seen:
95
+ raise EvaluationError(f'Cycle detected in identifier \'{name}\'')
96
+ self._seen.add(name)
97
+
98
+ parts = name.split('.')
99
+
100
+ # 1. ctx first
101
+ val = self._lookup_hierarchical(self.ctx, parts)
102
+ if val is not None:
103
+ self._seen.remove(name)
104
+ return str(val)
105
+
106
+ # 2. syntax-bound maps
107
+ for m in self.layerstack.all_maps():
108
+ if getattr(m, '__syntax__', None) not in (None, node.syntax):
109
+ continue
110
+
111
+ d = self._map_to_dict(m)
112
+ val = self._lookup_hierarchical(d, parts)
113
+ if val is not None:
114
+ self._seen.remove(name)
115
+ return str(val)
116
+
117
+ self._seen.remove(name)
118
+ return self._handle_missing_ident(node)
119
+
120
+ # call (nested token)
121
+ def _eval_call(self, node: CallNode):
122
+ outer = node.name
123
+
124
+ if outer in self._seen:
125
+ raise EvaluationError(f'Cycle detected in call \'{outer}\'')
126
+ self._seen.add(outer)
127
+
128
+ arg_value = self._eval(node.arg)
129
+ parts = [outer, arg_value]
130
+
131
+ # ctx first
132
+ val = self._lookup_hierarchical(self.ctx, parts)
133
+ if val is not None:
134
+ self._seen.remove(outer)
135
+ return str(val)
136
+
137
+ # syntax-bound maps
138
+ for m in self.layerstack.all_maps():
139
+ if getattr(m, '__syntax__', None) not in (None, node.syntax):
140
+ continue
141
+
142
+ d = self._map_to_dict(m)
143
+ val = self._lookup_hierarchical(d, parts)
144
+ if val is not None:
145
+ self._seen.remove(outer)
146
+ return str(val)
147
+
148
+ self._seen.remove(outer)
149
+ return self._handle_missing_call(node, arg_value)
150
+
151
+ # helpers
152
+ def _map_to_dict(self, m):
153
+ if hasattr(m, 'as_map'):
154
+ return m.as_map()
155
+ if isinstance(m, dict):
156
+ return m
157
+ raise EvaluationError(f'Invalid map object: {m}')
158
+
159
+ def _lookup_hierarchical(self, root, parts):
160
+ cur = root
161
+ for p in parts:
162
+ if not isinstance(cur, dict):
163
+ return None
164
+ if p not in cur:
165
+ return None
166
+ cur = cur[p]
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)
mapres/exceptions.py CHANGED
@@ -2,7 +2,7 @@ class MapResError(Exception):
2
2
  pass
3
3
 
4
4
  class MissingKeyError(MapResError):
5
- pass
6
-
7
- class MapSyntaxError(MapResError):
8
- pass
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
mapres/layers.py CHANGED
@@ -1,7 +1,10 @@
1
1
  class Layer:
2
- '''Holds a map with a priority'''
3
- def __init__(self, name, maps=None, priority=0):
4
- self.name = name
2
+ """
3
+ A single layer containing one or more maps.
4
+ Priority determines resolution order.
5
+ Lower priority = earlier lookup.
6
+ """
7
+ def __init__(self, maps=None, priority=0):
5
8
  self.maps = maps or []
6
9
  self.priority = priority
7
10
 
@@ -13,31 +16,39 @@ class Layer:
13
16
 
14
17
 
15
18
  class LayerStack:
16
- '''Manages ordered layers'''
17
- def __init__(self, layers=None):
18
- self.layers = {}
19
- if layers:
20
- for layer in layers:
21
- self.layers[layer.name] = layer
22
-
23
- def add_layer(self, layer):
24
- self.layers[layer.name] = layer
25
-
26
- def remove_layer(self, name):
27
- if name in self.layers:
28
- del self.layers[name]
29
-
30
- def get_layer(self, name):
31
- return self.layers.get(name)
32
-
33
- def clone(self):
34
- return LayerStack(list(self.layers.values()))
19
+ """
20
+ A simple ordered list of layers.
21
+ No names, no dicts, no complexity.
22
+ Just priority-sorted layers.
23
+ """
24
+ def __init__(self):
25
+ self.layers = [] # list of Layer objects
26
+
27
+ def add_layer(self, maps, priority=0):
28
+ """
29
+ Add a new layer containing one or more maps.
30
+ maps: datamap instance, datamap class, or dict
31
+ priority: lower = earlier lookup
32
+ """
33
+ layer = Layer(
34
+ maps=[maps] if not isinstance(maps, list) else maps,
35
+ priority=priority
36
+ )
37
+ self.layers.append(layer)
38
+ self.layers.sort(key=lambda l: l.priority)
35
39
 
36
40
  def all_maps(self):
37
- ordered = sorted(self.layers.values(), key=lambda l: l.priority)
38
- for layer in ordered:
41
+ """
42
+ Yield all maps in priority order.
43
+ """
44
+ for layer in self.layers:
39
45
  for m in layer:
40
46
  yield m
41
47
 
42
- def __iter__(self):
43
- return self.all_maps()
48
+ def clone(self):
49
+ """
50
+ Return a shallow copy of the layerstack.
51
+ """
52
+ new = LayerStack()
53
+ new.layers = list(self.layers)
54
+ return new
mapres/maps/color.py CHANGED
@@ -1,6 +1,6 @@
1
- from mapres.datamap import datamap, syntax
1
+ from mapres.datamap import datamap
2
2
 
3
- @datamap(syntax=syntax.angles) # produces <>
3
+ @datamap.angles
4
4
  class ColorMap:
5
5
  black: str = '\033[30m'
6
6
  dark_blue: str = '\033[34m'
@@ -22,13 +22,8 @@ class ColorMap:
22
22
  bold: str = '\033[1m'
23
23
  italic: str = '\033[3m'
24
24
 
25
- def as_dict(self):
26
- return {f'<{k}>': getattr(self, k) for k in self.__dataclass_fields__}
27
-
28
- # ASCII color map
29
25
  ascii_colors = ColorMap()
30
26
 
31
- # Minecraft color map
32
27
  mc_colors = ColorMap(
33
28
  black='§0',
34
29
  dark_blue='§1',
@@ -51,7 +46,6 @@ mc_colors = ColorMap(
51
46
  italic='§o'
52
47
  )
53
48
 
54
- # Color strip map
55
49
  strip_colors = ColorMap(
56
50
  black='',
57
51
  dark_blue='',
@@ -72,4 +66,4 @@ strip_colors = ColorMap(
72
66
  reset='',
73
67
  bold='',
74
68
  italic=''
75
- )
69
+ )
mapres/maps/time.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime, timezone
2
2
  from zoneinfo import ZoneInfo
3
- from mapres.datamap import datamap, syntax
3
+ from mapres.datamap import datamap
4
4
 
5
5
 
6
6
  def safe_zoneinfo(tz: str):
@@ -10,7 +10,7 @@ def safe_zoneinfo(tz: str):
10
10
  return timezone.utc
11
11
 
12
12
 
13
- @datamap(syntax=syntax.percents, mode='dynamic')
13
+ @datamap.percents.dynamic
14
14
  class TimeMap:
15
15
  _default_tz = safe_zoneinfo("America/Chicago")
16
16