mapres 2.3.1__tar.gz → 2.4__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: 2.3.1
3
+ Version: 2.4
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 = "2.3.1"
10
+ version = "2.4"
11
11
  description = "A powerfull mapping utility"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -7,7 +7,7 @@ try:
7
7
  except PackageNotFoundError:
8
8
  __version__ = '0.0.0'
9
9
 
10
- from .resolver import MapResolver, res
10
+ from .resolver import MapResolver, res, setGlobalMaps, setDefaultPasses
11
11
  from .datamap import DataMap, datamap, syntax
12
12
  from .layers import Layer, LayerStack
13
13
 
@@ -37,6 +37,8 @@ __all__ = [
37
37
  'syntax',
38
38
  'Layer',
39
39
  'LayerStack',
40
+ 'setGlobalMaps',
41
+ 'setDefaultPasses',
40
42
 
41
43
  # maps
42
44
  'ColorMap',
@@ -0,0 +1,90 @@
1
+ import re
2
+ from dataclasses import dataclass, fields, is_dataclass
3
+
4
+ class syntax:
5
+ double_braces = r'\{\{([A-Za-z][A-Za-z0-9_]*)\}\}' # {{value}}
6
+ dollars = r'\$\{([A-Za-z][A-Za-z0-9_]*)\}' # ${value}
7
+ angles = r'<([A-Za-z][A-Za-z0-9_]*)>' # <value>
8
+ percents = r'%([A-Za-z][A-Za-z0-9_]*)%' # %value%
9
+ at_tags = r'@([A-Za-z][A-Za-z0-9_]*)@' # @value@
10
+ hash_tags = r'#([A-Za-z][A-Za-z0-9_]*)#' # #value#
11
+ pipe_tags = r'\|([A-Za-z][A-Za-z0-9_]*)\|' # |value|
12
+ paren_dollar = r'\$\(([A-Za-z][A-Za-z0-9_]*)\)' # $(value)
13
+ colons = r':([A-Za-z][A-Za-z0-9_]*):' # :value:
14
+
15
+ @dataclass
16
+ class DataMap:
17
+ '''Core datamap class. Contains logic used to make datamaps'''
18
+ __syntax__: str = syntax.double_braces
19
+ __mode__: str | None = None
20
+
21
+ def as_map(self):
22
+ result = {}
23
+ for f in fields(self):
24
+ if not f.init or f.name in ('__syntax__', '__mode__'):
25
+ continue
26
+ val = getattr(self, f.name)
27
+ if self.__mode__ == 'dynamic':
28
+ providers = getattr(self, 'providers', None)
29
+ if isinstance(providers, dict) and f.name in providers:
30
+ val = providers[f.name]
31
+ if callable(val):
32
+ val = val()
33
+ result[f.name] = val
34
+ return result
35
+
36
+ @classmethod
37
+ def get_syntax(cls):
38
+ return getattr(cls, '__syntax__', syntax.double_braces)
39
+
40
+
41
+ # main decorator factory
42
+ def datamap(_cls=None, *, syntax=syntax.double_braces, mode=None):
43
+ '''@datamap decorator with optional values'''
44
+ def wrap(cls):
45
+ cls.__syntax__ = syntax
46
+ cls.__mode__ = mode
47
+ namespace = dict(cls.__dict__)
48
+ namespace['__dict__'] = {}
49
+ rules_obj = namespace.get('rules', None)
50
+ cls = type(cls.__name__, (DataMap,), namespace)
51
+ if rules_obj is not None:
52
+ setattr(cls, 'rules', rules_obj)
53
+ return dataclass(frozen=False)(cls)
54
+ if _cls is not None:
55
+ return wrap(_cls)
56
+ return wrap
57
+
58
+
59
+ # syntax-bound decorator factories
60
+ class _SyntaxFactory:
61
+ def __init__(self, syntax_value):
62
+ self._syntax = syntax_value
63
+
64
+ def __call__(self, _cls=None, **kwargs):
65
+ # Case 1: bare decorator: @datamap.colons
66
+ if _cls is not None and isinstance(_cls, type):
67
+ return datamap(_cls, syntax=self._syntax)
68
+
69
+ # Case 2: decorator with args: @datamap.colons(mode='config')
70
+ return datamap(syntax=self._syntax, **kwargs)
71
+
72
+ @property
73
+ def config(self):
74
+ # Case 3: @datamap.colons.config
75
+ return datamap(syntax=self._syntax, mode='config')
76
+
77
+
78
+ # build syntax shortcuts
79
+ datamap.double_braces = _SyntaxFactory(syntax.double_braces)
80
+ datamap.angles = _SyntaxFactory(syntax.angles)
81
+ datamap.dollars = _SyntaxFactory(syntax.dollars)
82
+ datamap.percents = _SyntaxFactory(syntax.percents)
83
+ datamap.at_tags = _SyntaxFactory(syntax.at_tags)
84
+ datamap.hash_tags = _SyntaxFactory(syntax.hash_tags)
85
+ datamap.pipe_tags = _SyntaxFactory(syntax.pipe_tags)
86
+ datamap.paren_dollar = _SyntaxFactory(syntax.paren_dollar)
87
+ datamap.colons = _SyntaxFactory(syntax.colons)
88
+
89
+ # global mode shortcut
90
+ datamap.config = datamap(mode='config')
@@ -1,31 +1,36 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
  from zoneinfo import ZoneInfo
3
3
  from mapres.datamap import datamap, syntax
4
4
 
5
5
 
6
+ def safe_zoneinfo(tz: str):
7
+ try:
8
+ return ZoneInfo(tz)
9
+ except Exception:
10
+ return timezone.utc
11
+
12
+
6
13
  @datamap(syntax=syntax.percents, mode='dynamic')
7
14
  class TimeMap:
8
- # default timezone
9
- _default_tz = ZoneInfo('UTC')
10
-
11
- # dynamic fields
12
- hh: str = None
13
- h: str = None
14
- h12: str = None
15
- hh12: str = None
16
- ampm: str = None
17
- mm: str = None
18
- m: str = None
19
- ss: str = None
20
- s: str = None
21
- ms: str = None
22
- YYYY: str = None
23
- MM: str = None
24
- DD: str = None
25
- weekday: str = None
15
+ _default_tz = safe_zoneinfo("America/Chicago")
16
+
17
+ hh: str | None = None
18
+ h: str | None = None
19
+ h12: str | None = None
20
+ hh12: str | None = None
21
+ ampm: str | None = None
22
+ mm: str | None = None
23
+ m: str | None = None
24
+ ss: str | None = None
25
+ s: str | None = None
26
+ ms: str | None = None
27
+ YYYY: str | None = None
28
+ MM: str | None = None
29
+ DD: str | None = None
30
+ weekday: str | None = None
26
31
 
27
32
  def __init__(self, tz: str | None = None):
28
- self.TZ = ZoneInfo(tz) if tz else self._default_tz
33
+ self.TZ = safe_zoneinfo(tz) if tz else self._default_tz
29
34
 
30
35
  @property
31
36
  def providers(self):
@@ -5,6 +5,38 @@ from .layers import Layer, LayerStack
5
5
  from .exceptions import MapResError, MissingKeyError, MapSyntaxError
6
6
 
7
7
 
8
+ # escape pipeline
9
+ ESCAPES = {
10
+ r'\:': '__ESC_COLON__',
11
+ r'\<': '__ESC_LT__',
12
+ r'\%': '__ESC_PERCENT__',
13
+ r'\$': '__ESC_DOLLAR__',
14
+ r'\{': '__ESC_LBRACE__',
15
+ r'\|': '__ESC_PIPE__',
16
+ }
17
+
18
+ # Reverse mapping: placeholder -> literal char (without backslash)
19
+ UNESCAPES = {
20
+ placeholder: esc[1:] # remove leading backslash
21
+ for esc, placeholder in ESCAPES.items()
22
+ }
23
+
24
+
25
+ def escape_stage(text: str, ctx: dict, resolver) -> str:
26
+ """Replace escaped opening delimiters with placeholders."""
27
+ for esc, placeholder in ESCAPES.items():
28
+ text = text.replace(esc, placeholder)
29
+ return text
30
+
31
+
32
+ def unescape_stage(text: str, ctx: dict, resolver) -> str:
33
+ """Restore escaped delimiters after resolution."""
34
+ for placeholder, literal in UNESCAPES.items():
35
+ text = text.replace(placeholder, literal)
36
+ return text
37
+
38
+
39
+ # core resolver
8
40
  class MapResolver:
9
41
  '''
10
42
  Core resolution engine for mapres.
@@ -14,7 +46,7 @@ class MapResolver:
14
46
  - syntax providers
15
47
  - layered maps
16
48
  - context substitution
17
- - recursive multi-pass resolution
49
+ - multi-pass resolution (controlled by `passes`)
18
50
  - optional caching
19
51
  '''
20
52
  def __init__(
@@ -24,14 +56,22 @@ class MapResolver:
24
56
  pipeline: list | None = None,
25
57
  cache: bool = False,
26
58
  cache_size: int = 1024,
27
- max_depth: int = 5
59
+ passes_default: int = 5
28
60
  ):
29
61
  self.layers = layers or LayerStack()
30
62
  self.syntax = syntax or []
31
63
  self.pipeline = pipeline or []
32
64
  self.cache_enabled = cache
33
65
  self.cache = LRUCache(cache_size) if cache else None
34
- self.max_depth = max_depth
66
+
67
+ # unified recursion control
68
+ self.passes_default = passes_default
69
+
70
+ # Insert escape stage FIRST
71
+ self.pipeline.insert(0, escape_stage)
72
+
73
+ # Append unescape stage LAST
74
+ self.pipeline.append(unescape_stage)
35
75
 
36
76
  # pipeline
37
77
  def _apply_pipeline(self, text: str, ctx: dict) -> str:
@@ -47,22 +87,26 @@ class MapResolver:
47
87
  for provider in self.syntax:
48
88
  pattern = provider.pattern
49
89
  d = provider.get_map(ctx, self)
90
+
50
91
  def repl(match: re.Match) -> str:
51
92
  k = match.group(1)
52
93
  if k not in d:
53
94
  raise MissingKeyError(f'Missing key "{k}" in syntax provider {provider}')
54
95
  return str(d[k])
96
+
55
97
  try:
56
98
  text = re.sub(pattern, repl, text)
57
99
  except re.error as exc:
58
100
  raise MapSyntaxError(
59
101
  f'Regex error in syntax provider {provider}: {exc}'
60
102
  ) from exc
103
+
61
104
  return text
62
105
 
63
106
  # layered maps
64
107
  def _apply_maps(self, text: str, ctx: dict, layerstack: LayerStack):
65
108
  syntax_patterns = []
109
+
66
110
  for m in layerstack:
67
111
  if isinstance(m, type) and hasattr(m, 'as_map'):
68
112
  m = m()
@@ -72,10 +116,13 @@ class MapResolver:
72
116
  pattern = ctx.get('syntax')
73
117
  if pattern:
74
118
  syntax_patterns.append(pattern)
119
+
75
120
  syntax_patterns = list(dict.fromkeys(syntax_patterns))
121
+
76
122
  for pattern in syntax_patterns:
77
123
  def repl(match: re.Match):
78
124
  k = match.group(1)
125
+
79
126
  for m in layerstack:
80
127
  layer = m
81
128
  if isinstance(layer, type) and hasattr(layer, 'as_map'):
@@ -86,6 +133,7 @@ class MapResolver:
86
133
  d = layer
87
134
  else:
88
135
  continue
136
+
89
137
  if k in d:
90
138
  return str(d[k])
91
139
 
@@ -98,19 +146,23 @@ class MapResolver:
98
146
  text = re.sub(pattern, repl, text)
99
147
  except re.error as exc:
100
148
  raise MapSyntaxError(f'Regex error in syntax pattern {pattern}: {exc}') from exc
149
+
101
150
  return text
102
151
 
103
152
  # context
104
153
  def _apply_ctx(self, text: str, ctx: dict) -> str:
105
154
  if not ctx:
106
155
  return text
107
- pattern = syntax.braces
156
+
157
+ pattern = syntax.double_braces
108
158
  d = ctx
159
+
109
160
  def repl(match: re.Match) -> str:
110
161
  k = match.group(1)
111
162
  if k not in d:
112
163
  raise MissingKeyError(f'Missing ctx key "{k}"')
113
164
  return str(d[k])
165
+
114
166
  try:
115
167
  return re.sub(pattern, repl, text)
116
168
  except re.error as exc:
@@ -124,21 +176,12 @@ class MapResolver:
124
176
  text = self._apply_ctx(text, ctx)
125
177
  return text
126
178
 
127
- # recursion
128
- def _recursive(self, text: str, ctx: dict, layerstack: LayerStack) -> str:
129
- current = text
130
- for _ in range(self.max_depth):
131
- result = self._resolve_once(current, ctx, layerstack)
132
- if result == current:
133
- return result
134
- current = result
135
- return current
136
-
137
179
  # public api
138
180
  def res(
139
181
  self,
140
182
  text: str,
141
183
  *,
184
+ passes: int | None = None,
142
185
  extra_maps: dict | None = None,
143
186
  override_maps: dict | None = None,
144
187
  **ctx
@@ -147,22 +190,38 @@ class MapResolver:
147
190
  Resolve a string using maps, syntax providers, pipeline, and context.
148
191
  '''
149
192
  try:
193
+ depth = passes if passes is not None else self.passes_default
194
+
195
+ if depth <= 0:
196
+ raise MapResError("passes must be >= 1")
197
+
198
+ # override maps
150
199
  if override_maps is not None:
151
200
  temp = LayerStack([Layer('override', override_maps, priority=0)])
152
- return self._recursive(text, ctx, temp)
201
+ return self._resolve_once(text, ctx, temp)
153
202
 
203
+ # extra maps
154
204
  if extra_maps:
155
205
  temp = self.layers.clone()
156
206
  temp.add_layer(Layer('extra', extra_maps, priority=999))
157
- return self._recursive(text, ctx, temp)
207
+ return self._resolve_once(text, ctx, temp)
158
208
 
209
+ # caching
159
210
  if self.cache_enabled:
160
211
  key = (text, tuple(sorted(ctx.items())))
161
212
  cached = self.cache.get(key)
162
213
  if cached is not None:
163
214
  return cached
164
215
 
165
- result = self._recursive(text, ctx, self.layers)
216
+ # multi-pass resolution
217
+ current = text
218
+ for _ in range(depth):
219
+ new = self._resolve_once(current, ctx, self.layers)
220
+ if new == current:
221
+ break
222
+ current = new
223
+
224
+ result = current
166
225
 
167
226
  if self.cache_enabled:
168
227
  self.cache.set(key, result)
@@ -175,11 +234,31 @@ class MapResolver:
175
234
  raise MapResError(f'Unhandled resolver error: {exc}') from exc
176
235
 
177
236
 
178
- # global resolver
179
- _DEFAULT = MapResolver()
237
+ # ----------------------------------
238
+ # ---------- SIMPLE USAGE ----------
239
+ # ----------------------------------
240
+
241
+ _DEFAULT_RESOLVER = MapResolver()
242
+
243
+ def setGlobalMaps(maps, *, name=None, priority=0):
244
+ '''
245
+ Register a global map or datamap class into the default resolver.
246
+
247
+ maps: datamap class OR dict
248
+ name: optional layer name (default = class name or 'global')
249
+ priority: layer priority (lower = earlier)
250
+ '''
251
+ layer_name = name or getattr(maps, "__name__", "global")
252
+ layer = Layer(layer_name, maps=[maps], priority=priority)
253
+ _DEFAULT_RESOLVER.layers.add_layer(layer)
254
+
255
+ def setDefaultPasses(passes: int):
256
+ if passes < 1:
257
+ raise MapResError("Passes cannot be less than 1")
258
+ _DEFAULT_RESOLVER.passes_default = passes
180
259
 
181
- def res(text: str, **ctx) -> str:
260
+ def res(text: str, passes=None, **ctx) -> str:
182
261
  '''
183
- Resolve using the global default resolver.
262
+ Simple resolution using the global default resolver.
184
263
  '''
185
- return _DEFAULT.res(text, **ctx)
264
+ return _DEFAULT_RESOLVER.res(text, passes=passes, **ctx)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 2.3.1
3
+ Version: 2.4
4
4
  Summary: A powerfull mapping utility
5
5
  Author: iFamished
6
6
  License: MIT
@@ -1,64 +0,0 @@
1
- import re
2
- from dataclasses import dataclass, fields, is_dataclass
3
-
4
- class syntax:
5
- braces = r'\{([^{}]+)\}'
6
- dollars = r'\$\{([^{}]+)\}'
7
- angles = r'<([^<>]+)>'
8
- percents = r'%([^%]+)%'
9
-
10
-
11
- @dataclass
12
- class DataMap:
13
- '''Core datamap class. Contains logic used to make datamaps'''
14
- __syntax__: str = syntax.braces
15
- __mode__: str | None = None
16
-
17
- def as_map(self):
18
- result = {}
19
- for f in fields(self):
20
- if not f.init or f.name in ('__syntax__', '__mode__'):
21
- continue
22
- val = getattr(self, f.name)
23
- if self.__mode__ == 'dynamic':
24
- providers = getattr(self, 'providers', None)
25
- if isinstance(providers, dict) and f.name in providers:
26
- val = providers[f.name]
27
- if callable(val):
28
- val = val()
29
- result[f.name] = val
30
- return result
31
-
32
- @classmethod
33
- def get_syntax(cls):
34
- return getattr(cls, '__syntax__', syntax.braces)
35
-
36
-
37
- # decorator
38
- def datamap(
39
- _cls = None,
40
- *,
41
- syntax: str = syntax.braces,
42
- mode: bool | None = None,
43
- ):
44
- '''@datamap decorator with optional values'''
45
- def wrap(cls):
46
- cls.__syntax__ = syntax
47
- cls.__mode__ = mode
48
- namespace = dict(cls.__dict__)
49
- namespace['__dict__'] = {}
50
- # preserve any rules class defined in the original namespace by attaching it to the generated class
51
- rules_obj = namespace.get('rules', None)
52
- cls = type(cls.__name__, (DataMap,), namespace)
53
- # attach rules (if present) to the new class so validators can read them via the datamap class
54
- if rules_obj is not None:
55
- setattr(cls, 'rules', rules_obj)
56
- return dataclass(frozen=False)(cls)
57
- return wrap if _cls is None else wrap(_cls)
58
-
59
-
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)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes