mapres 2.4.dev6__tar.gz → 3.dev1__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.4.dev6/src/mapres.egg-info → mapres-3.dev1}/PKG-INFO +1 -1
- {mapres-2.4.dev6 → mapres-3.dev1}/pyproject.toml +1 -1
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres/__init__.py +2 -1
- mapres-3.dev1/src/mapres/ast.py +24 -0
- mapres-3.dev1/src/mapres/datamap.py +129 -0
- mapres-3.dev1/src/mapres/evaluator.py +131 -0
- mapres-3.dev1/src/mapres/layers.py +56 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres/maps/color.py +3 -9
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres/maps/time.py +2 -2
- mapres-3.dev1/src/mapres/parser.py +105 -0
- mapres-3.dev1/src/mapres/resolver.py +113 -0
- mapres-3.dev1/src/mapres/tokenizer.py +217 -0
- {mapres-2.4.dev6 → mapres-3.dev1/src/mapres.egg-info}/PKG-INFO +1 -1
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres.egg-info/SOURCES.txt +4 -0
- mapres-2.4.dev6/src/mapres/datamap.py +0 -64
- mapres-2.4.dev6/src/mapres/layers.py +0 -43
- mapres-2.4.dev6/src/mapres/resolver.py +0 -219
- {mapres-2.4.dev6 → mapres-3.dev1}/LICENSE +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/README.md +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/setup.cfg +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres/cache.py +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres/exceptions.py +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres/maps/__init__.py +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/src/mapres.egg-info/requires.txt +0 -0
- {mapres-2.4.dev6 → mapres-3.dev1}/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, setGlobalMaps
|
|
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
|
|
|
@@ -38,6 +38,7 @@ __all__ = [
|
|
|
38
38
|
'Layer',
|
|
39
39
|
'LayerStack',
|
|
40
40
|
'setGlobalMaps',
|
|
41
|
+
'setDefaultPasses',
|
|
41
42
|
|
|
42
43
|
# maps
|
|
43
44
|
'ColorMap',
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Node:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TextNode(Node):
|
|
12
|
+
text: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class IdentNode(Node):
|
|
17
|
+
name: str # supports dots: server.status.online
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CallNode(Node):
|
|
22
|
+
name: str # outer identifier
|
|
23
|
+
arg: Node # single argument (Template fragment)
|
|
24
|
+
syntax: str # which delimiter type produced it (colon/brace/etc)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
3
|
+
|
|
4
|
+
# syntax identifiers
|
|
5
|
+
class syntax:
|
|
6
|
+
braces = 'braces' # {{value}}
|
|
7
|
+
dollars = 'dollars' # ${value}
|
|
8
|
+
angles = 'angles' # <value>
|
|
9
|
+
percents = 'percents' # %value%
|
|
10
|
+
pipes = 'pipes' # |value|
|
|
11
|
+
colons = 'colons' # :value:
|
|
12
|
+
at_tags = 'at_tags' # @value@
|
|
13
|
+
hash_tags = 'hash_tags' # #value#
|
|
14
|
+
paren_dollars = 'paren_dollars' # $(value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# base datamap class
|
|
18
|
+
@dataclass
|
|
19
|
+
class DataMap:
|
|
20
|
+
'''
|
|
21
|
+
Core datamap class for mapres 3.
|
|
22
|
+
|
|
23
|
+
Responsibilities:
|
|
24
|
+
- hold fields
|
|
25
|
+
- optionally evaluate dynamic providers
|
|
26
|
+
- return a dict via as_map()
|
|
27
|
+
|
|
28
|
+
Syntax metadata (__syntax__) determines which delimiters
|
|
29
|
+
this map responds to (angle, brace, colon, percent, pipe, etc).
|
|
30
|
+
'''
|
|
31
|
+
__syntax__: str = None
|
|
32
|
+
__mode__: str | None = None # "dynamic", "config", or None
|
|
33
|
+
|
|
34
|
+
def as_map(self):
|
|
35
|
+
'''
|
|
36
|
+
Convert datamap fields into a dictionary.
|
|
37
|
+
If mode='dynamic', evaluate provider functions.
|
|
38
|
+
'''
|
|
39
|
+
result = {}
|
|
40
|
+
|
|
41
|
+
for f in fields(self):
|
|
42
|
+
if not f.init or f.name in ('__syntax__', '__mode__'):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
val = getattr(self, f.name)
|
|
46
|
+
|
|
47
|
+
# dynamic provider support
|
|
48
|
+
if self.__mode__ == 'dynamic':
|
|
49
|
+
providers = getattr(self, 'providers', None)
|
|
50
|
+
if isinstance(providers, dict) and f.name in providers:
|
|
51
|
+
val = providers[f.name]
|
|
52
|
+
if callable(val):
|
|
53
|
+
val = val()
|
|
54
|
+
|
|
55
|
+
result[f.name] = val
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# main decorator factory
|
|
61
|
+
def datamap(_cls=None, *, syntax=None, mode=None):
|
|
62
|
+
'''
|
|
63
|
+
@datamap decorator.
|
|
64
|
+
|
|
65
|
+
Supports:
|
|
66
|
+
@datamap
|
|
67
|
+
@datamap(mode='dynamic')
|
|
68
|
+
@datamap(syntax='angle')
|
|
69
|
+
@datamap(syntax='angle', mode='config')
|
|
70
|
+
'''
|
|
71
|
+
def wrap(cls):
|
|
72
|
+
cls.__syntax__ = syntax
|
|
73
|
+
cls.__mode__ = mode
|
|
74
|
+
namespace = dict(cls.__dict__)
|
|
75
|
+
namespace['__dict__'] = {}
|
|
76
|
+
rules_obj = namespace.get('rules', None)
|
|
77
|
+
cls = type(cls.__name__, (DataMap,), namespace)
|
|
78
|
+
if rules_obj is not None:
|
|
79
|
+
setattr(cls, 'rules', rules_obj)
|
|
80
|
+
return dataclass(frozen=False)(cls)
|
|
81
|
+
if _cls is not None:
|
|
82
|
+
return wrap(_cls)
|
|
83
|
+
return wrap
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# syntax-bound decorator factories
|
|
87
|
+
class _SyntaxFactory:
|
|
88
|
+
'''
|
|
89
|
+
Allows:
|
|
90
|
+
@datamap.angles
|
|
91
|
+
@datamap.angles(mode='config')
|
|
92
|
+
@datamap.angles.config
|
|
93
|
+
'''
|
|
94
|
+
def __init__(self, syntax_name):
|
|
95
|
+
self._syntax = syntax_name
|
|
96
|
+
|
|
97
|
+
def __call__(self, _cls=None, **kwargs):
|
|
98
|
+
# Case 1: bare decorator: @datamap.angles
|
|
99
|
+
if _cls is not None and isinstance(_cls, type):
|
|
100
|
+
return datamap(_cls, syntax=self._syntax)
|
|
101
|
+
|
|
102
|
+
# Case 2: decorator with args: @datamap.angles(mode='config')
|
|
103
|
+
return datamap(syntax=self._syntax, **kwargs)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def config(self):
|
|
107
|
+
# Case 3: @datamap.angles.config
|
|
108
|
+
return datamap(syntax=self._syntax, mode='config')
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def dynamic(self):
|
|
112
|
+
return datamap(syntax=self._syntax, mode='dynamic')
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# build syntax shortcuts
|
|
117
|
+
datamap.braces = _SyntaxFactory(syntax.braces)
|
|
118
|
+
|
|
119
|
+
datamap.dollars = _SyntaxFactory(syntax.dollars)
|
|
120
|
+
datamap.angles = _SyntaxFactory(syntax.angles)
|
|
121
|
+
datamap.percents = _SyntaxFactory(syntax.percents)
|
|
122
|
+
datamap.pipes = _SyntaxFactory(syntax.pipes)
|
|
123
|
+
datamap.colons = _SyntaxFactory(syntax.colons)
|
|
124
|
+
datamap.at_tags = _SyntaxFactory(syntax.at_tags)
|
|
125
|
+
datamap.hash_tags = _SyntaxFactory(syntax.hash_tags)
|
|
126
|
+
datamap.paren_dollars = _SyntaxFactory(syntax.paren_dollars)
|
|
127
|
+
|
|
128
|
+
# global mode shortcut
|
|
129
|
+
datamap.config = datamap(mode='config')
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EvaluationError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Evaluator:
|
|
9
|
+
def __init__(self, layerstack, ctx=None):
|
|
10
|
+
self.layerstack = layerstack
|
|
11
|
+
self.ctx = ctx or {}
|
|
12
|
+
self._seen = set() # cycle detection
|
|
13
|
+
|
|
14
|
+
# -----------------------------
|
|
15
|
+
# PUBLIC API
|
|
16
|
+
# -----------------------------
|
|
17
|
+
def evaluate(self, node):
|
|
18
|
+
return self._eval(node)
|
|
19
|
+
|
|
20
|
+
# -----------------------------
|
|
21
|
+
# INTERNAL DISPATCH
|
|
22
|
+
# -----------------------------
|
|
23
|
+
def _eval(self, node):
|
|
24
|
+
if isinstance(node, TemplateNode):
|
|
25
|
+
return self._eval_template(node)
|
|
26
|
+
|
|
27
|
+
if isinstance(node, TextNode):
|
|
28
|
+
return node.text
|
|
29
|
+
|
|
30
|
+
if isinstance(node, IdentNode):
|
|
31
|
+
return self._eval_ident(node)
|
|
32
|
+
|
|
33
|
+
if isinstance(node, CallNode):
|
|
34
|
+
return self._eval_call(node)
|
|
35
|
+
|
|
36
|
+
raise EvaluationError(f"Unknown AST node: {node}")
|
|
37
|
+
|
|
38
|
+
# -----------------------------
|
|
39
|
+
# TEMPLATE
|
|
40
|
+
# -----------------------------
|
|
41
|
+
def _eval_template(self, node: TemplateNode):
|
|
42
|
+
parts = []
|
|
43
|
+
for child in node.children:
|
|
44
|
+
parts.append(self._eval(child))
|
|
45
|
+
return "".join(parts)
|
|
46
|
+
|
|
47
|
+
# -----------------------------
|
|
48
|
+
# IDENTIFIER
|
|
49
|
+
# -----------------------------
|
|
50
|
+
def _eval_ident(self, node: IdentNode):
|
|
51
|
+
name = node.name
|
|
52
|
+
|
|
53
|
+
# cycle detection
|
|
54
|
+
if name in self._seen:
|
|
55
|
+
raise EvaluationError(f"Cycle detected in identifier '{name}'")
|
|
56
|
+
self._seen.add(name)
|
|
57
|
+
|
|
58
|
+
parts = name.split(".")
|
|
59
|
+
|
|
60
|
+
# 1. ctx first
|
|
61
|
+
val = self._lookup_hierarchical(self.ctx, parts)
|
|
62
|
+
if val is not None:
|
|
63
|
+
self._seen.remove(name)
|
|
64
|
+
return str(val)
|
|
65
|
+
|
|
66
|
+
# 2. syntax-bound maps
|
|
67
|
+
for m in self.layerstack.all_maps():
|
|
68
|
+
if getattr(m, "__syntax__", None) not in (None, node.syntax):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
d = self._map_to_dict(m)
|
|
72
|
+
val = self._lookup_hierarchical(d, parts)
|
|
73
|
+
if val is not None:
|
|
74
|
+
self._seen.remove(name)
|
|
75
|
+
return str(val)
|
|
76
|
+
|
|
77
|
+
self._seen.remove(name)
|
|
78
|
+
raise EvaluationError(f"Missing key '{name}'")
|
|
79
|
+
|
|
80
|
+
# -----------------------------
|
|
81
|
+
# CALL (nested token)
|
|
82
|
+
# -----------------------------
|
|
83
|
+
def _eval_call(self, node: CallNode):
|
|
84
|
+
outer = node.name
|
|
85
|
+
|
|
86
|
+
if outer in self._seen:
|
|
87
|
+
raise EvaluationError(f"Cycle detected in call '{outer}'")
|
|
88
|
+
self._seen.add(outer)
|
|
89
|
+
|
|
90
|
+
arg_value = self._eval(node.arg)
|
|
91
|
+
parts = [outer, arg_value]
|
|
92
|
+
|
|
93
|
+
# ctx first
|
|
94
|
+
val = self._lookup_hierarchical(self.ctx, parts)
|
|
95
|
+
if val is not None:
|
|
96
|
+
self._seen.remove(outer)
|
|
97
|
+
return str(val)
|
|
98
|
+
|
|
99
|
+
# syntax-bound maps
|
|
100
|
+
for m in self.layerstack.all_maps():
|
|
101
|
+
if getattr(m, "__syntax__", None) not in (None, node.syntax):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
d = self._map_to_dict(m)
|
|
105
|
+
val = self._lookup_hierarchical(d, parts)
|
|
106
|
+
if val is not None:
|
|
107
|
+
self._seen.remove(outer)
|
|
108
|
+
return str(val)
|
|
109
|
+
|
|
110
|
+
self._seen.remove(outer)
|
|
111
|
+
raise EvaluationError(f"Missing nested key '{outer}({arg_value})'")
|
|
112
|
+
|
|
113
|
+
# -----------------------------
|
|
114
|
+
# HELPERS
|
|
115
|
+
# -----------------------------
|
|
116
|
+
def _map_to_dict(self, m):
|
|
117
|
+
if hasattr(m, "as_map"):
|
|
118
|
+
return m.as_map()
|
|
119
|
+
if isinstance(m, dict):
|
|
120
|
+
return m
|
|
121
|
+
raise EvaluationError(f"Invalid map object: {m}")
|
|
122
|
+
|
|
123
|
+
def _lookup_hierarchical(self, root, parts):
|
|
124
|
+
cur = root
|
|
125
|
+
for p in parts:
|
|
126
|
+
if not isinstance(cur, dict):
|
|
127
|
+
return None
|
|
128
|
+
if p not in cur:
|
|
129
|
+
return None
|
|
130
|
+
cur = cur[p]
|
|
131
|
+
return cur
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# layers.py
|
|
2
|
+
|
|
3
|
+
class Layer:
|
|
4
|
+
"""
|
|
5
|
+
A single layer containing one or more maps.
|
|
6
|
+
Priority determines resolution order.
|
|
7
|
+
Lower priority = earlier lookup.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, maps=None, priority=0):
|
|
10
|
+
self.maps = maps or []
|
|
11
|
+
self.priority = priority
|
|
12
|
+
|
|
13
|
+
def add(self, m):
|
|
14
|
+
self.maps.append(m)
|
|
15
|
+
|
|
16
|
+
def __iter__(self):
|
|
17
|
+
return iter(self.maps)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LayerStack:
|
|
21
|
+
"""
|
|
22
|
+
A simple ordered list of layers.
|
|
23
|
+
No names, no dicts, no complexity.
|
|
24
|
+
Just priority-sorted layers.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.layers = [] # list of Layer objects
|
|
28
|
+
|
|
29
|
+
def add_layer(self, maps, priority=0):
|
|
30
|
+
"""
|
|
31
|
+
Add a new layer containing one or more maps.
|
|
32
|
+
maps: datamap instance, datamap class, or dict
|
|
33
|
+
priority: lower = earlier lookup
|
|
34
|
+
"""
|
|
35
|
+
layer = Layer(
|
|
36
|
+
maps=[maps] if not isinstance(maps, list) else maps,
|
|
37
|
+
priority=priority
|
|
38
|
+
)
|
|
39
|
+
self.layers.append(layer)
|
|
40
|
+
self.layers.sort(key=lambda l: l.priority)
|
|
41
|
+
|
|
42
|
+
def all_maps(self):
|
|
43
|
+
"""
|
|
44
|
+
Yield all maps in priority order.
|
|
45
|
+
"""
|
|
46
|
+
for layer in self.layers:
|
|
47
|
+
for m in layer:
|
|
48
|
+
yield m
|
|
49
|
+
|
|
50
|
+
def clone(self):
|
|
51
|
+
"""
|
|
52
|
+
Return a shallow copy of the layerstack.
|
|
53
|
+
"""
|
|
54
|
+
new = LayerStack()
|
|
55
|
+
new.layers = list(self.layers)
|
|
56
|
+
return new
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from mapres.datamap import datamap
|
|
1
|
+
from mapres.datamap import datamap
|
|
2
2
|
|
|
3
|
-
@datamap
|
|
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
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
2
|
from zoneinfo import ZoneInfo
|
|
3
|
-
from mapres.datamap import datamap
|
|
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
|
|
13
|
+
@datamap.percents.dynamic
|
|
14
14
|
class TimeMap:
|
|
15
15
|
_default_tz = safe_zoneinfo("America/Chicago")
|
|
16
16
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from tokenizer import TokenType, Token
|
|
2
|
+
from ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ParserError(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Parser:
|
|
10
|
+
def __init__(self, tokens: list[Token]):
|
|
11
|
+
self.tokens = tokens
|
|
12
|
+
self.i = 0
|
|
13
|
+
self.n = len(tokens)
|
|
14
|
+
|
|
15
|
+
def peek(self, k=0) -> Token | None:
|
|
16
|
+
idx = self.i + k
|
|
17
|
+
return self.tokens[idx] if idx < self.n else None
|
|
18
|
+
|
|
19
|
+
def advance(self, k=1):
|
|
20
|
+
self.i += k
|
|
21
|
+
|
|
22
|
+
def expect(self, type_: TokenType):
|
|
23
|
+
tok = self.peek()
|
|
24
|
+
if tok is None or tok.type != type_:
|
|
25
|
+
raise ParserError(f"Expected {type_}, got {tok}")
|
|
26
|
+
self.advance()
|
|
27
|
+
return tok
|
|
28
|
+
|
|
29
|
+
def parse(self) -> TemplateNode:
|
|
30
|
+
children = []
|
|
31
|
+
while self.peek() is not None:
|
|
32
|
+
tok = self.peek()
|
|
33
|
+
if tok.type is TokenType.TEXT:
|
|
34
|
+
children.append(TextNode(tok.value))
|
|
35
|
+
self.advance()
|
|
36
|
+
else:
|
|
37
|
+
children.append(self.parse_token())
|
|
38
|
+
return TemplateNode(children)
|
|
39
|
+
|
|
40
|
+
def parse_token(self) -> Node:
|
|
41
|
+
tok = self.peek()
|
|
42
|
+
|
|
43
|
+
if tok.type is TokenType.COLON_OPEN:
|
|
44
|
+
return self.parse_colon_token()
|
|
45
|
+
|
|
46
|
+
if tok.type is TokenType.BRACE_OPEN:
|
|
47
|
+
return self.parse_brace_token()
|
|
48
|
+
|
|
49
|
+
if tok.type is TokenType.DOLLAR_OPEN:
|
|
50
|
+
return self.parse_dollar_token()
|
|
51
|
+
|
|
52
|
+
if tok.type is TokenType.ANGLE_OPEN:
|
|
53
|
+
return self.parse_angle_token()
|
|
54
|
+
|
|
55
|
+
if tok.type is TokenType.PIPE_OPEN:
|
|
56
|
+
return self.parse_pipe_token()
|
|
57
|
+
|
|
58
|
+
if tok.type is TokenType.PERCENT_OPEN:
|
|
59
|
+
return self.parse_percent_token()
|
|
60
|
+
|
|
61
|
+
raise ParserError(f"Unexpected token {tok}")
|
|
62
|
+
|
|
63
|
+
# --- core pattern: IDENT or IDENT(arg) ---
|
|
64
|
+
|
|
65
|
+
def _parse_ident_or_call(self, closing_type, syntax_name: str) -> Node:
|
|
66
|
+
ident_tok = self.expect(TokenType.IDENT)
|
|
67
|
+
name = ident_tok.value
|
|
68
|
+
|
|
69
|
+
# single-argument nested form: IDENT LPAREN Template RPAREN
|
|
70
|
+
if self.peek() and self.peek().type is TokenType.LPAREN:
|
|
71
|
+
self.advance() # consume '('
|
|
72
|
+
arg = self.parse() # nested Template
|
|
73
|
+
self.expect(TokenType.RPAREN)
|
|
74
|
+
self.expect(closing_type)
|
|
75
|
+
return CallNode(name=name, arg=arg, syntax=syntax_name)
|
|
76
|
+
|
|
77
|
+
# simple identifier: IDENT
|
|
78
|
+
self.expect(closing_type)
|
|
79
|
+
return IdentNode(name=name)
|
|
80
|
+
|
|
81
|
+
# --- syntax-bound token handlers ---
|
|
82
|
+
|
|
83
|
+
def parse_colon_token(self) -> Node:
|
|
84
|
+
self.expect(TokenType.COLON_OPEN)
|
|
85
|
+
return self._parse_ident_or_call(TokenType.COLON_CLOSE, "colons")
|
|
86
|
+
|
|
87
|
+
def parse_brace_token(self) -> Node:
|
|
88
|
+
self.expect(TokenType.BRACE_OPEN)
|
|
89
|
+
return self._parse_ident_or_call(TokenType.BRACE_CLOSE, "braces")
|
|
90
|
+
|
|
91
|
+
def parse_dollar_token(self) -> Node:
|
|
92
|
+
self.expect(TokenType.DOLLAR_OPEN)
|
|
93
|
+
return self._parse_ident_or_call(TokenType.DOLLAR_CLOSE, "dollars")
|
|
94
|
+
|
|
95
|
+
def parse_angle_token(self) -> Node:
|
|
96
|
+
self.expect(TokenType.ANGLE_OPEN)
|
|
97
|
+
return self._parse_ident_or_call(TokenType.ANGLE_CLOSE, "angles")
|
|
98
|
+
|
|
99
|
+
def parse_pipe_token(self) -> Node:
|
|
100
|
+
self.expect(TokenType.PIPE_OPEN)
|
|
101
|
+
return self._parse_ident_or_call(TokenType.PIPE_CLOSE, "pipes")
|
|
102
|
+
|
|
103
|
+
def parse_percent_token(self) -> Node:
|
|
104
|
+
self.expect(TokenType.PERCENT_OPEN)
|
|
105
|
+
return self._parse_ident_or_call(TokenType.PERCENT_CLOSE, "percents")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from .tokenizer import Tokenizer
|
|
2
|
+
from .parser import Parser
|
|
3
|
+
from .evaluator import Evaluator
|
|
4
|
+
from .layers import LayerStack
|
|
5
|
+
from .cache import LRUCache
|
|
6
|
+
from .exceptions import MapResError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MapResolver:
|
|
10
|
+
"""
|
|
11
|
+
New AST-based resolver for mapres.
|
|
12
|
+
|
|
13
|
+
Pipeline:
|
|
14
|
+
tokenize → parse → evaluate → return string
|
|
15
|
+
|
|
16
|
+
Supports:
|
|
17
|
+
- hierarchical identifiers
|
|
18
|
+
- nested tokens (single argument)
|
|
19
|
+
- multiple map layers
|
|
20
|
+
- ctx fallback
|
|
21
|
+
- caching (optional)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, layers=None, cache=False, cache_size=1024):
|
|
25
|
+
self.layers = layers or LayerStack()
|
|
26
|
+
self.cache_enabled = cache
|
|
27
|
+
self.cache = LRUCache(cache_size) if cache else None
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------
|
|
30
|
+
# PUBLIC API
|
|
31
|
+
# ---------------------------------------
|
|
32
|
+
def res(self, text: str, *, extra_maps=None, override_maps=None, **ctx) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Resolve a template string using:
|
|
35
|
+
- tokenizer
|
|
36
|
+
- parser
|
|
37
|
+
- evaluator
|
|
38
|
+
- layerstack
|
|
39
|
+
- ctx
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# caching
|
|
44
|
+
if self.cache_enabled:
|
|
45
|
+
key = (text, tuple(sorted(ctx.items())))
|
|
46
|
+
cached = self.cache.get(key)
|
|
47
|
+
if cached is not None:
|
|
48
|
+
return cached
|
|
49
|
+
|
|
50
|
+
# build working layerstack
|
|
51
|
+
layerstack = self._build_layerstack(extra_maps, override_maps)
|
|
52
|
+
|
|
53
|
+
# tokenize
|
|
54
|
+
tokens = Tokenizer(text).tokenize()
|
|
55
|
+
|
|
56
|
+
# parse
|
|
57
|
+
ast = Parser(tokens).parse()
|
|
58
|
+
|
|
59
|
+
# evaluate
|
|
60
|
+
result = Evaluator(layerstack, ctx).evaluate(ast)
|
|
61
|
+
|
|
62
|
+
# store cache
|
|
63
|
+
if self.cache_enabled:
|
|
64
|
+
self.cache.set(key, result)
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
raise MapResError(f"Resolver error: {exc}") from exc
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------
|
|
72
|
+
# LAYERSTACK BUILDING
|
|
73
|
+
# ---------------------------------------
|
|
74
|
+
def _build_layerstack(self, extra_maps, override_maps):
|
|
75
|
+
"""
|
|
76
|
+
Build a temporary layerstack for this resolution call.
|
|
77
|
+
Supports:
|
|
78
|
+
- override maps (priority 0)
|
|
79
|
+
- extra maps (priority 999)
|
|
80
|
+
- normal layers (existing priorities)
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if override_maps is not None:
|
|
84
|
+
temp = LayerStack()
|
|
85
|
+
temp.add_layer(override_maps, priority=0)
|
|
86
|
+
return temp
|
|
87
|
+
|
|
88
|
+
if extra_maps is not None:
|
|
89
|
+
temp = self.layers.clone()
|
|
90
|
+
temp.add_layer(extra_maps, priority=999)
|
|
91
|
+
return temp
|
|
92
|
+
|
|
93
|
+
return self.layers
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------
|
|
97
|
+
# SIMPLE GLOBAL USAGE
|
|
98
|
+
# ---------------------------------------
|
|
99
|
+
|
|
100
|
+
_DEFAULT_RESOLVER = MapResolver()
|
|
101
|
+
|
|
102
|
+
def setGlobalMaps(maps, *, priority=0):
|
|
103
|
+
"""
|
|
104
|
+
Register a global map or datamap instance/class/dict.
|
|
105
|
+
Lower priority = earlier lookup.
|
|
106
|
+
"""
|
|
107
|
+
_DEFAULT_RESOLVER.layers.add_layer(maps, priority=priority)
|
|
108
|
+
|
|
109
|
+
def res(text: str, **ctx) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Simple global resolution.
|
|
112
|
+
"""
|
|
113
|
+
return _DEFAULT_RESOLVER.res(text, **ctx)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TokenType(Enum):
|
|
5
|
+
TEXT = auto()
|
|
6
|
+
IDENT = auto()
|
|
7
|
+
|
|
8
|
+
COLON_OPEN = auto()
|
|
9
|
+
COLON_CLOSE = auto()
|
|
10
|
+
|
|
11
|
+
BRACE_OPEN = auto()
|
|
12
|
+
BRACE_CLOSE = auto()
|
|
13
|
+
|
|
14
|
+
DOLLAR_OPEN = auto()
|
|
15
|
+
DOLLAR_CLOSE = auto()
|
|
16
|
+
|
|
17
|
+
ANGLE_OPEN = auto()
|
|
18
|
+
ANGLE_CLOSE = auto()
|
|
19
|
+
|
|
20
|
+
PIPE_OPEN = auto()
|
|
21
|
+
PIPE_CLOSE = auto()
|
|
22
|
+
|
|
23
|
+
PERCENT_OPEN = auto()
|
|
24
|
+
PERCENT_CLOSE = auto()
|
|
25
|
+
|
|
26
|
+
LPAREN = auto()
|
|
27
|
+
RPAREN = auto()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Token:
|
|
31
|
+
def __init__(self, type_, value=None, pos=0):
|
|
32
|
+
self.type = type_
|
|
33
|
+
self.value = value
|
|
34
|
+
self.pos = pos
|
|
35
|
+
|
|
36
|
+
def __repr__(self):
|
|
37
|
+
if self.value is not None:
|
|
38
|
+
return f"Token({self.type}, {self.value!r}, pos={self.pos})"
|
|
39
|
+
return f"Token({self.type}, pos={self.pos})"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Tokenizer:
|
|
43
|
+
def __init__(self, text):
|
|
44
|
+
self.text = text
|
|
45
|
+
self.i = 0
|
|
46
|
+
self.n = len(text)
|
|
47
|
+
self.tokens = []
|
|
48
|
+
|
|
49
|
+
def peek(self, k=0):
|
|
50
|
+
idx = self.i + k
|
|
51
|
+
if idx >= self.n:
|
|
52
|
+
return ''
|
|
53
|
+
return self.text[idx]
|
|
54
|
+
|
|
55
|
+
def advance(self, k=1):
|
|
56
|
+
self.i += k
|
|
57
|
+
|
|
58
|
+
def match(self, s):
|
|
59
|
+
return self.text.startswith(s, self.i)
|
|
60
|
+
|
|
61
|
+
def emit(self, type_, value=None):
|
|
62
|
+
self.tokens.append(Token(type_, value, self.i))
|
|
63
|
+
|
|
64
|
+
def tokenize(self):
|
|
65
|
+
buf = []
|
|
66
|
+
|
|
67
|
+
def flush_text():
|
|
68
|
+
if buf:
|
|
69
|
+
self.emit(TokenType.TEXT, ''.join(buf))
|
|
70
|
+
buf.clear()
|
|
71
|
+
|
|
72
|
+
while self.i < self.n:
|
|
73
|
+
ch = self.peek()
|
|
74
|
+
|
|
75
|
+
# ------------------------------------
|
|
76
|
+
# ESCAPING: \X → literal X
|
|
77
|
+
# ------------------------------------
|
|
78
|
+
if self.match(r'\:'):
|
|
79
|
+
buf.append(':'); self.advance(2); continue
|
|
80
|
+
if self.match(r'\<'):
|
|
81
|
+
buf.append('<'); self.advance(2); continue
|
|
82
|
+
if self.match(r'\>'):
|
|
83
|
+
buf.append('>'); self.advance(2); continue
|
|
84
|
+
if self.match(r'\%'):
|
|
85
|
+
buf.append('%'); self.advance(2); continue
|
|
86
|
+
if self.match(r'\$'):
|
|
87
|
+
buf.append('$'); self.advance(2); continue
|
|
88
|
+
if self.match(r'\{'):
|
|
89
|
+
buf.append('{'); self.advance(2); continue
|
|
90
|
+
if self.match(r'\}'):
|
|
91
|
+
buf.append('}'); self.advance(2); continue
|
|
92
|
+
if self.match(r'\|'):
|
|
93
|
+
buf.append('|'); self.advance(2); continue
|
|
94
|
+
if self.match(r'\('):
|
|
95
|
+
buf.append('('); self.advance(2); continue
|
|
96
|
+
if self.match(r'\)'):
|
|
97
|
+
buf.append(')'); self.advance(2); continue
|
|
98
|
+
|
|
99
|
+
# ------------------------------------
|
|
100
|
+
# DOUBLE BRACES {{value}}
|
|
101
|
+
# ------------------------------------
|
|
102
|
+
if self.match("{{"):
|
|
103
|
+
flush_text()
|
|
104
|
+
self.emit(TokenType.BRACE_OPEN)
|
|
105
|
+
self.advance(2)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if self.match("}}"):
|
|
109
|
+
flush_text()
|
|
110
|
+
self.emit(TokenType.BRACE_CLOSE)
|
|
111
|
+
self.advance(2)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# ------------------------------------
|
|
115
|
+
# DOLLARS ${value}
|
|
116
|
+
# ------------------------------------
|
|
117
|
+
if self.match("${"):
|
|
118
|
+
flush_text()
|
|
119
|
+
self.emit(TokenType.DOLLAR_OPEN)
|
|
120
|
+
self.advance(2)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if self.match("}"):
|
|
124
|
+
flush_text()
|
|
125
|
+
self.emit(TokenType.DOLLAR_CLOSE)
|
|
126
|
+
self.advance()
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# ------------------------------------
|
|
130
|
+
# ANGLES <value>
|
|
131
|
+
# ------------------------------------
|
|
132
|
+
if self.match("<"):
|
|
133
|
+
flush_text()
|
|
134
|
+
self.emit(TokenType.ANGLE_OPEN)
|
|
135
|
+
self.advance()
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if self.match(">"):
|
|
139
|
+
flush_text()
|
|
140
|
+
self.emit(TokenType.ANGLE_CLOSE)
|
|
141
|
+
self.advance()
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# ------------------------------------
|
|
145
|
+
# PIPES |value|
|
|
146
|
+
# ------------------------------------
|
|
147
|
+
if self.match("|"):
|
|
148
|
+
flush_text()
|
|
149
|
+
# explicit open/close detection:
|
|
150
|
+
# |IDENT| → PIPE_OPEN IDENT PIPE_CLOSE
|
|
151
|
+
# parser enforces structure
|
|
152
|
+
if self.tokens and self.tokens[-1].type == TokenType.PIPE_OPEN:
|
|
153
|
+
self.emit(TokenType.PIPE_CLOSE)
|
|
154
|
+
else:
|
|
155
|
+
self.emit(TokenType.PIPE_OPEN)
|
|
156
|
+
self.advance()
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# ------------------------------------
|
|
160
|
+
# PERCENTS %value%
|
|
161
|
+
# ------------------------------------
|
|
162
|
+
if self.match("%"):
|
|
163
|
+
flush_text()
|
|
164
|
+
if self.tokens and self.tokens[-1].type == TokenType.PERCENT_OPEN:
|
|
165
|
+
self.emit(TokenType.PERCENT_CLOSE)
|
|
166
|
+
else:
|
|
167
|
+
self.emit(TokenType.PERCENT_OPEN)
|
|
168
|
+
self.advance()
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# ------------------------------------
|
|
172
|
+
# COLONS :value:
|
|
173
|
+
# ------------------------------------
|
|
174
|
+
if self.match(":"):
|
|
175
|
+
flush_text()
|
|
176
|
+
if self.tokens and self.tokens[-1].type == TokenType.COLON_OPEN:
|
|
177
|
+
self.emit(TokenType.COLON_CLOSE)
|
|
178
|
+
else:
|
|
179
|
+
self.emit(TokenType.COLON_OPEN)
|
|
180
|
+
self.advance()
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# ------------------------------------
|
|
184
|
+
# PARENTHESES
|
|
185
|
+
# ------------------------------------
|
|
186
|
+
if ch == "(":
|
|
187
|
+
flush_text()
|
|
188
|
+
self.emit(TokenType.LPAREN)
|
|
189
|
+
self.advance()
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if ch == ")":
|
|
193
|
+
flush_text()
|
|
194
|
+
self.emit(TokenType.RPAREN)
|
|
195
|
+
self.advance()
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# ------------------------------------
|
|
199
|
+
# IDENTIFIERS (with dots)
|
|
200
|
+
# ------------------------------------
|
|
201
|
+
if ch.isalpha():
|
|
202
|
+
flush_text()
|
|
203
|
+
start = self.i
|
|
204
|
+
while self.peek().isalnum() or self.peek() in "._":
|
|
205
|
+
self.advance()
|
|
206
|
+
ident = self.text[start:self.i]
|
|
207
|
+
self.emit(TokenType.IDENT, ident)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# ------------------------------------
|
|
211
|
+
# DEFAULT: TEXT
|
|
212
|
+
# ------------------------------------
|
|
213
|
+
buf.append(ch)
|
|
214
|
+
self.advance()
|
|
215
|
+
|
|
216
|
+
flush_text()
|
|
217
|
+
return self.tokens
|
|
@@ -2,11 +2,15 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/mapres/__init__.py
|
|
5
|
+
src/mapres/ast.py
|
|
5
6
|
src/mapres/cache.py
|
|
6
7
|
src/mapres/datamap.py
|
|
8
|
+
src/mapres/evaluator.py
|
|
7
9
|
src/mapres/exceptions.py
|
|
8
10
|
src/mapres/layers.py
|
|
11
|
+
src/mapres/parser.py
|
|
9
12
|
src/mapres/resolver.py
|
|
13
|
+
src/mapres/tokenizer.py
|
|
10
14
|
src/mapres.egg-info/PKG-INFO
|
|
11
15
|
src/mapres.egg-info/SOURCES.txt
|
|
12
16
|
src/mapres.egg-info/dependency_links.txt
|
|
@@ -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 | str | 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)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
class Layer:
|
|
2
|
-
'''Holds a map with a priority'''
|
|
3
|
-
def __init__(self, name, maps=None, priority=0):
|
|
4
|
-
self.name = name
|
|
5
|
-
self.maps = maps or []
|
|
6
|
-
self.priority = priority
|
|
7
|
-
|
|
8
|
-
def add(self, m):
|
|
9
|
-
self.maps.append(m)
|
|
10
|
-
|
|
11
|
-
def __iter__(self):
|
|
12
|
-
return iter(self.maps)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
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()))
|
|
35
|
-
|
|
36
|
-
def all_maps(self):
|
|
37
|
-
ordered = sorted(self.layers.values(), key=lambda l: l.priority)
|
|
38
|
-
for layer in ordered:
|
|
39
|
-
for m in layer:
|
|
40
|
-
yield m
|
|
41
|
-
|
|
42
|
-
def __iter__(self):
|
|
43
|
-
return self.all_maps()
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from .datamap import DataMap, syntax
|
|
3
|
-
from .cache import LRUCache
|
|
4
|
-
from .layers import Layer, LayerStack
|
|
5
|
-
from .exceptions import MapResError, MissingKeyError, MapSyntaxError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class MapResolver:
|
|
9
|
-
'''
|
|
10
|
-
Core resolution engine for mapres.
|
|
11
|
-
|
|
12
|
-
Handles:
|
|
13
|
-
- pipeline stages
|
|
14
|
-
- syntax providers
|
|
15
|
-
- layered maps
|
|
16
|
-
- context substitution
|
|
17
|
-
- multi-pass resolution (controlled by `passes`)
|
|
18
|
-
- optional caching
|
|
19
|
-
'''
|
|
20
|
-
def __init__(
|
|
21
|
-
self,
|
|
22
|
-
layers: list | None = None,
|
|
23
|
-
syntax: list | None = None,
|
|
24
|
-
pipeline: list | None = None,
|
|
25
|
-
cache: bool = False,
|
|
26
|
-
cache_size: int = 1024,
|
|
27
|
-
passes_default: int = 5
|
|
28
|
-
):
|
|
29
|
-
self.layers = layers or LayerStack()
|
|
30
|
-
self.syntax = syntax or []
|
|
31
|
-
self.pipeline = pipeline or []
|
|
32
|
-
self.cache_enabled = cache
|
|
33
|
-
self.cache = LRUCache(cache_size) if cache else None
|
|
34
|
-
|
|
35
|
-
# unified recursion control
|
|
36
|
-
self.passes_default = passes_default
|
|
37
|
-
|
|
38
|
-
# pipeline
|
|
39
|
-
def _apply_pipeline(self, text: str, ctx: dict) -> str:
|
|
40
|
-
try:
|
|
41
|
-
for stage in self.pipeline:
|
|
42
|
-
text = stage(text, ctx, self)
|
|
43
|
-
return text
|
|
44
|
-
except Exception as exc:
|
|
45
|
-
raise MapResError(f'Pipeline stage failed: {exc}') from exc
|
|
46
|
-
|
|
47
|
-
# syntax providers
|
|
48
|
-
def _apply_syntax(self, text: str, ctx: dict) -> str:
|
|
49
|
-
for provider in self.syntax:
|
|
50
|
-
pattern = provider.pattern
|
|
51
|
-
d = provider.get_map(ctx, self)
|
|
52
|
-
|
|
53
|
-
def repl(match: re.Match) -> str:
|
|
54
|
-
k = match.group(1)
|
|
55
|
-
if k not in d:
|
|
56
|
-
raise MissingKeyError(f'Missing key "{k}" in syntax provider {provider}')
|
|
57
|
-
return str(d[k])
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
text = re.sub(pattern, repl, text)
|
|
61
|
-
except re.error as exc:
|
|
62
|
-
raise MapSyntaxError(
|
|
63
|
-
f'Regex error in syntax provider {provider}: {exc}'
|
|
64
|
-
) from exc
|
|
65
|
-
|
|
66
|
-
return text
|
|
67
|
-
|
|
68
|
-
# layered maps
|
|
69
|
-
def _apply_maps(self, text: str, ctx: dict, layerstack: LayerStack):
|
|
70
|
-
syntax_patterns = []
|
|
71
|
-
|
|
72
|
-
for m in layerstack:
|
|
73
|
-
if isinstance(m, type) and hasattr(m, 'as_map'):
|
|
74
|
-
m = m()
|
|
75
|
-
if hasattr(m, 'as_map'):
|
|
76
|
-
syntax_patterns.append(m.get_syntax())
|
|
77
|
-
elif isinstance(m, dict):
|
|
78
|
-
pattern = ctx.get('syntax')
|
|
79
|
-
if pattern:
|
|
80
|
-
syntax_patterns.append(pattern)
|
|
81
|
-
|
|
82
|
-
syntax_patterns = list(dict.fromkeys(syntax_patterns))
|
|
83
|
-
|
|
84
|
-
for pattern in syntax_patterns:
|
|
85
|
-
def repl(match: re.Match):
|
|
86
|
-
k = match.group(1)
|
|
87
|
-
|
|
88
|
-
for m in layerstack:
|
|
89
|
-
layer = m
|
|
90
|
-
if isinstance(layer, type) and hasattr(layer, 'as_map'):
|
|
91
|
-
layer = layer()
|
|
92
|
-
if hasattr(layer, 'as_map'):
|
|
93
|
-
d = layer.as_map()
|
|
94
|
-
elif isinstance(layer, dict):
|
|
95
|
-
d = layer
|
|
96
|
-
else:
|
|
97
|
-
continue
|
|
98
|
-
|
|
99
|
-
if k in d:
|
|
100
|
-
return str(d[k])
|
|
101
|
-
|
|
102
|
-
if k in ctx:
|
|
103
|
-
return str(ctx[k])
|
|
104
|
-
|
|
105
|
-
raise MissingKeyError(f'Missing key "{k}" in any layer or context')
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
text = re.sub(pattern, repl, text)
|
|
109
|
-
except re.error as exc:
|
|
110
|
-
raise MapSyntaxError(f'Regex error in syntax pattern {pattern}: {exc}') from exc
|
|
111
|
-
|
|
112
|
-
return text
|
|
113
|
-
|
|
114
|
-
# context
|
|
115
|
-
def _apply_ctx(self, text: str, ctx: dict) -> str:
|
|
116
|
-
if not ctx:
|
|
117
|
-
return text
|
|
118
|
-
|
|
119
|
-
pattern = syntax.braces
|
|
120
|
-
d = ctx
|
|
121
|
-
|
|
122
|
-
def repl(match: re.Match) -> str:
|
|
123
|
-
k = match.group(1)
|
|
124
|
-
if k not in d:
|
|
125
|
-
raise MissingKeyError(f'Missing ctx key "{k}"')
|
|
126
|
-
return str(d[k])
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
return re.sub(pattern, repl, text)
|
|
130
|
-
except re.error as exc:
|
|
131
|
-
raise MapSyntaxError(f'Regex error in ctx syntax: {exc}') from exc
|
|
132
|
-
|
|
133
|
-
# one pass
|
|
134
|
-
def _resolve_once(self, text: str, ctx: dict, layerstack: LayerStack) -> str:
|
|
135
|
-
text = self._apply_pipeline(text, ctx)
|
|
136
|
-
text = self._apply_syntax(text, ctx)
|
|
137
|
-
text = self._apply_maps(text, ctx, layerstack)
|
|
138
|
-
text = self._apply_ctx(text, ctx)
|
|
139
|
-
return text
|
|
140
|
-
|
|
141
|
-
# public api
|
|
142
|
-
def res(
|
|
143
|
-
self,
|
|
144
|
-
text: str,
|
|
145
|
-
*,
|
|
146
|
-
passes: int | None = None,
|
|
147
|
-
extra_maps: dict | None = None,
|
|
148
|
-
override_maps: dict | None = None,
|
|
149
|
-
**ctx
|
|
150
|
-
) -> str:
|
|
151
|
-
'''
|
|
152
|
-
Resolve a string using maps, syntax providers, pipeline, and context.
|
|
153
|
-
'''
|
|
154
|
-
try:
|
|
155
|
-
# determine number of passes
|
|
156
|
-
depth = passes if passes is not None else self.passes_default
|
|
157
|
-
|
|
158
|
-
if depth <= 0:
|
|
159
|
-
raise MapResError("passes must be >= 1")
|
|
160
|
-
|
|
161
|
-
# override maps
|
|
162
|
-
if override_maps is not None:
|
|
163
|
-
temp = LayerStack([Layer('override', override_maps, priority=0)])
|
|
164
|
-
return self._resolve_once(text, ctx, temp)
|
|
165
|
-
|
|
166
|
-
# extra maps
|
|
167
|
-
if extra_maps:
|
|
168
|
-
temp = self.layers.clone()
|
|
169
|
-
temp.add_layer(Layer('extra', extra_maps, priority=999))
|
|
170
|
-
return self._resolve_once(text, ctx, temp)
|
|
171
|
-
|
|
172
|
-
# caching
|
|
173
|
-
if self.cache_enabled:
|
|
174
|
-
key = (text, tuple(sorted(ctx.items())))
|
|
175
|
-
cached = self.cache.get(key)
|
|
176
|
-
if cached is not None:
|
|
177
|
-
return cached
|
|
178
|
-
|
|
179
|
-
# multi-pass resolution
|
|
180
|
-
current = text
|
|
181
|
-
for _ in range(depth):
|
|
182
|
-
new = self._resolve_once(current, ctx, self.layers)
|
|
183
|
-
if new == current:
|
|
184
|
-
break
|
|
185
|
-
current = new
|
|
186
|
-
|
|
187
|
-
result = current
|
|
188
|
-
|
|
189
|
-
if self.cache_enabled:
|
|
190
|
-
self.cache.set(key, result)
|
|
191
|
-
|
|
192
|
-
return result
|
|
193
|
-
|
|
194
|
-
except MapResError:
|
|
195
|
-
raise
|
|
196
|
-
except Exception as exc:
|
|
197
|
-
raise MapResError(f'Unhandled resolver error: {exc}') from exc
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# global resolver
|
|
201
|
-
_DEFAULT = MapResolver()
|
|
202
|
-
|
|
203
|
-
def res(text: str, passes=None, **ctx) -> str:
|
|
204
|
-
'''
|
|
205
|
-
Resolve using the global default resolver.
|
|
206
|
-
'''
|
|
207
|
-
return _DEFAULT.res(text, passes=passes, **ctx)
|
|
208
|
-
|
|
209
|
-
def setGlobalMaps(maps, *, name=None, priority=0):
|
|
210
|
-
"""
|
|
211
|
-
Register a global map or datamap class into the default resolver.
|
|
212
|
-
|
|
213
|
-
maps: datamap class OR dict
|
|
214
|
-
name: optional layer name (default = class name or 'global')
|
|
215
|
-
priority: layer priority (lower = earlier)
|
|
216
|
-
"""
|
|
217
|
-
layer_name = name or getattr(maps, "__name__", "global")
|
|
218
|
-
layer = Layer(layer_name, maps=[maps], priority=priority)
|
|
219
|
-
_DEFAULT.layers.add_layer(layer)
|
|
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
|