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.
- {mapres-2.3.1/src/mapres.egg-info → mapres-2.4}/PKG-INFO +1 -1
- {mapres-2.3.1 → mapres-2.4}/pyproject.toml +1 -1
- {mapres-2.3.1 → mapres-2.4}/src/mapres/__init__.py +3 -1
- mapres-2.4/src/mapres/datamap.py +90 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres/maps/time.py +25 -20
- {mapres-2.3.1 → mapres-2.4}/src/mapres/resolver.py +101 -22
- {mapres-2.3.1 → mapres-2.4/src/mapres.egg-info}/PKG-INFO +1 -1
- mapres-2.3.1/src/mapres/datamap.py +0 -64
- {mapres-2.3.1 → mapres-2.4}/LICENSE +0 -0
- {mapres-2.3.1 → mapres-2.4}/README.md +0 -0
- {mapres-2.3.1 → mapres-2.4}/setup.cfg +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres/cache.py +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres/exceptions.py +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres/layers.py +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres/maps/__init__.py +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres/maps/color.py +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres.egg-info/SOURCES.txt +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres.egg-info/requires.txt +0 -0
- {mapres-2.3.1 → mapres-2.4}/src/mapres.egg-info/top_level.txt +0 -0
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
179
|
-
|
|
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
|
-
|
|
262
|
+
Simple resolution using the global default resolver.
|
|
184
263
|
'''
|
|
185
|
-
return
|
|
264
|
+
return _DEFAULT_RESOLVER.res(text, passes=passes, **ctx)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|