mapres 2.4.dev6__tar.gz → 3a2__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-3a2/PKG-INFO +33 -0
- mapres-3a2/README.md +7 -0
- {mapres-2.4.dev6 → mapres-3a2}/pyproject.toml +6 -11
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres/__init__.py +9 -1
- mapres-3a2/src/mapres/ast.py +29 -0
- mapres-3a2/src/mapres/datamap.py +160 -0
- mapres-3a2/src/mapres/evaluator.py +212 -0
- mapres-3a2/src/mapres/exceptions.py +8 -0
- mapres-3a2/src/mapres/layers.py +54 -0
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres/maps/color.py +3 -9
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres/maps/time.py +2 -2
- mapres-3a2/src/mapres/parser.py +112 -0
- mapres-3a2/src/mapres/resolver.py +161 -0
- mapres-3a2/src/mapres/tokenizer.py +325 -0
- mapres-3a2/src/mapres.egg-info/PKG-INFO +33 -0
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres.egg-info/SOURCES.txt +4 -1
- mapres-2.4.dev6/PKG-INFO +0 -47
- mapres-2.4.dev6/README.md +0 -18
- mapres-2.4.dev6/src/mapres/datamap.py +0 -64
- mapres-2.4.dev6/src/mapres/exceptions.py +0 -8
- mapres-2.4.dev6/src/mapres/layers.py +0 -43
- mapres-2.4.dev6/src/mapres/resolver.py +0 -219
- mapres-2.4.dev6/src/mapres.egg-info/PKG-INFO +0 -47
- mapres-2.4.dev6/src/mapres.egg-info/requires.txt +0 -3
- {mapres-2.4.dev6 → mapres-3a2}/LICENSE +0 -0
- {mapres-2.4.dev6 → mapres-3a2}/setup.cfg +0 -0
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres/cache.py +0 -0
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres/maps/__init__.py +0 -0
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-2.4.dev6 → mapres-3a2}/src/mapres.egg-info/top_level.txt +0 -0
mapres-3a2/PKG-INFO
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mapres
|
|
3
|
+
Version: 3a2
|
|
4
|
+
Summary: A powerfull mapping utility
|
|
5
|
+
Author: iFamished
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iFamished/mapres
|
|
8
|
+
Project-URL: Documentation, https://github.com/iFamished/mapres
|
|
9
|
+
Project-URL: Source, https://github.com/iFamished/mapres
|
|
10
|
+
Project-URL: Issues, https://github.com/iFamished/mapres/issues
|
|
11
|
+
Keywords: templating,automation,server-management
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Development Status :: 4 - Beta
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# MapRes
|
|
28
|
+
A powerful string-mapping utility for many use cases.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
```bash
|
|
32
|
+
pip install mapres
|
|
33
|
+
```
|
mapres-3a2/README.md
ADDED
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "mapres"
|
|
10
|
-
version = "
|
|
10
|
+
version = "3-alpha.2"
|
|
11
11
|
description = "A powerfull mapping utility"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
@@ -18,13 +18,8 @@ keywords = [
|
|
|
18
18
|
"templating",
|
|
19
19
|
"automation",
|
|
20
20
|
"server-management",
|
|
21
|
-
"hungerlib",
|
|
22
|
-
]
|
|
23
|
-
dependencies = [
|
|
24
|
-
"requests",
|
|
25
|
-
"mcrcon",
|
|
26
|
-
"PyYaml",
|
|
27
21
|
]
|
|
22
|
+
dependencies = []
|
|
28
23
|
classifiers = [
|
|
29
24
|
"Programming Language :: Python :: 3",
|
|
30
25
|
"Programming Language :: Python :: 3.10",
|
|
@@ -42,10 +37,10 @@ classifiers = [
|
|
|
42
37
|
text = "MIT"
|
|
43
38
|
|
|
44
39
|
[project.urls]
|
|
45
|
-
Homepage = "https://github.com/
|
|
46
|
-
Documentation = "https://github.com/
|
|
47
|
-
Source = "https://github.com/
|
|
48
|
-
Issues = "https://github.com/
|
|
40
|
+
Homepage = "https://github.com/iFamished/mapres"
|
|
41
|
+
Documentation = "https://github.com/iFamished/mapres"
|
|
42
|
+
Source = "https://github.com/iFamished/mapres"
|
|
43
|
+
Issues = "https://github.com/iFamished/mapres/issues"
|
|
49
44
|
|
|
50
45
|
[tool.setuptools.packages.find]
|
|
51
46
|
where = [
|
|
@@ -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
|
]
|
|
@@ -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
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
2
|
+
|
|
3
|
+
# syntax identifiers
|
|
4
|
+
class syntax:
|
|
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)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# base datamap class
|
|
17
|
+
@dataclass
|
|
18
|
+
class DataMap:
|
|
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
|
|
41
|
+
|
|
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
|
+
'''
|
|
50
|
+
result = {}
|
|
51
|
+
|
|
52
|
+
# dataclass fields
|
|
53
|
+
for f in fields(self):
|
|
54
|
+
if not f.init or f.name.startswith('__'):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
val = getattr(self, f.name)
|
|
58
|
+
|
|
59
|
+
# dynamic provider support
|
|
60
|
+
if self.__mode__ == 'dynamic':
|
|
61
|
+
providers = getattr(self, 'providers', None)
|
|
62
|
+
if isinstance(providers, dict) and f.name in providers:
|
|
63
|
+
val = providers[f.name]
|
|
64
|
+
if callable(val):
|
|
65
|
+
val = val()
|
|
66
|
+
|
|
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
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
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
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def datamap(_cls=None, *, syntax=None, mode=None, missing_key=None, recursive=None, max_depth=None):
|
|
98
|
+
def wrap(cls):
|
|
99
|
+
cls.__syntax__ = syntax
|
|
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
|
+
|
|
105
|
+
namespace = dict(cls.__dict__)
|
|
106
|
+
namespace['__dict__'] = {}
|
|
107
|
+
rules_obj = namespace.get('rules', None)
|
|
108
|
+
|
|
109
|
+
cls = type(cls.__name__, (DataMap,), namespace)
|
|
110
|
+
|
|
111
|
+
if rules_obj is not None:
|
|
112
|
+
setattr(cls, 'rules', rules_obj)
|
|
113
|
+
|
|
114
|
+
return dataclass(frozen=False)(cls)
|
|
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)
|
|
157
|
+
|
|
158
|
+
datamap.config = datamap(mode='config')
|
|
159
|
+
datamap.dynamic = datamap(mode='dynamic')
|
|
160
|
+
datamap.callable = datamap(mode='callable')
|
|
@@ -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)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class Layer:
|
|
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):
|
|
8
|
+
self.maps = maps or []
|
|
9
|
+
self.priority = priority
|
|
10
|
+
|
|
11
|
+
def add(self, m):
|
|
12
|
+
self.maps.append(m)
|
|
13
|
+
|
|
14
|
+
def __iter__(self):
|
|
15
|
+
return iter(self.maps)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LayerStack:
|
|
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)
|
|
39
|
+
|
|
40
|
+
def all_maps(self):
|
|
41
|
+
"""
|
|
42
|
+
Yield all maps in priority order.
|
|
43
|
+
"""
|
|
44
|
+
for layer in self.layers:
|
|
45
|
+
for m in layer:
|
|
46
|
+
yield m
|
|
47
|
+
|
|
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
|
|
@@ -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
|
|