mapres 2.4.dev5__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.
Files changed (26) hide show
  1. {mapres-2.4.dev5/src/mapres.egg-info → mapres-3.dev1}/PKG-INFO +1 -1
  2. {mapres-2.4.dev5 → mapres-3.dev1}/pyproject.toml +1 -1
  3. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres/__init__.py +2 -1
  4. mapres-3.dev1/src/mapres/ast.py +24 -0
  5. mapres-3.dev1/src/mapres/datamap.py +129 -0
  6. mapres-3.dev1/src/mapres/evaluator.py +131 -0
  7. mapres-3.dev1/src/mapres/layers.py +56 -0
  8. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres/maps/color.py +3 -9
  9. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres/maps/time.py +2 -2
  10. mapres-3.dev1/src/mapres/parser.py +105 -0
  11. mapres-3.dev1/src/mapres/resolver.py +113 -0
  12. mapres-3.dev1/src/mapres/tokenizer.py +217 -0
  13. {mapres-2.4.dev5 → mapres-3.dev1/src/mapres.egg-info}/PKG-INFO +1 -1
  14. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres.egg-info/SOURCES.txt +4 -0
  15. mapres-2.4.dev5/src/mapres/datamap.py +0 -64
  16. mapres-2.4.dev5/src/mapres/layers.py +0 -43
  17. mapres-2.4.dev5/src/mapres/resolver.py +0 -201
  18. {mapres-2.4.dev5 → mapres-3.dev1}/LICENSE +0 -0
  19. {mapres-2.4.dev5 → mapres-3.dev1}/README.md +0 -0
  20. {mapres-2.4.dev5 → mapres-3.dev1}/setup.cfg +0 -0
  21. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres/cache.py +0 -0
  22. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres/exceptions.py +0 -0
  23. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres/maps/__init__.py +0 -0
  24. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres.egg-info/dependency_links.txt +0 -0
  25. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres.egg-info/requires.txt +0 -0
  26. {mapres-2.4.dev5 → mapres-3.dev1}/src/mapres.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 2.4.dev5
3
+ Version: 3.dev1
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.4-dev.5"
10
+ version = "3-dev.1"
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, 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, syntax
1
+ from mapres.datamap import datamap
2
2
 
3
- @datamap(syntax=syntax.angles) # produces <>
3
+ @datamap.angles
4
4
  class ColorMap:
5
5
  black: str = '\033[30m'
6
6
  dark_blue: str = '\033[34m'
@@ -22,13 +22,8 @@ class ColorMap:
22
22
  bold: str = '\033[1m'
23
23
  italic: str = '\033[3m'
24
24
 
25
- def as_dict(self):
26
- return {f'<{k}>': getattr(self, k) for k in self.__dataclass_fields__}
27
-
28
- # ASCII color map
29
25
  ascii_colors = ColorMap()
30
26
 
31
- # Minecraft color map
32
27
  mc_colors = ColorMap(
33
28
  black='§0',
34
29
  dark_blue='§1',
@@ -51,7 +46,6 @@ mc_colors = ColorMap(
51
46
  italic='§o'
52
47
  )
53
48
 
54
- # Color strip map
55
49
  strip_colors = ColorMap(
56
50
  black='',
57
51
  dark_blue='',
@@ -72,4 +66,4 @@ strip_colors = ColorMap(
72
66
  reset='',
73
67
  bold='',
74
68
  italic=''
75
- )
69
+ )
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime, timezone
2
2
  from zoneinfo import ZoneInfo
3
- from mapres.datamap import datamap, syntax
3
+ from mapres.datamap import datamap
4
4
 
5
5
 
6
6
  def safe_zoneinfo(tz: str):
@@ -10,7 +10,7 @@ def safe_zoneinfo(tz: str):
10
10
  return timezone.utc
11
11
 
12
12
 
13
- @datamap(syntax=syntax.percents, mode='dynamic')
13
+ @datamap.percents.dynamic
14
14
  class TimeMap:
15
15
  _default_tz = safe_zoneinfo("America/Chicago")
16
16
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapres
3
- Version: 2.4.dev5
3
+ Version: 3.dev1
4
4
  Summary: A powerfull mapping utility
5
5
  Author: iFamished
6
6
  License: MIT
@@ -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,201 +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
- - recursive multi-pass resolution
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
- max_depth: 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
- self.max_depth = max_depth
35
-
36
- # pipeline
37
- def _apply_pipeline(self, text: str, ctx: dict) -> str:
38
- try:
39
- for stage in self.pipeline:
40
- text = stage(text, ctx, self)
41
- return text
42
- except Exception as exc:
43
- raise MapResError(f'Pipeline stage failed: {exc}') from exc
44
-
45
- # syntax providers
46
- def _apply_syntax(self, text: str, ctx: dict) -> str:
47
- for provider in self.syntax:
48
- pattern = provider.pattern
49
- d = provider.get_map(ctx, self)
50
- def repl(match: re.Match) -> str:
51
- k = match.group(1)
52
- if k not in d:
53
- raise MissingKeyError(f'Missing key "{k}" in syntax provider {provider}')
54
- return str(d[k])
55
- try:
56
- text = re.sub(pattern, repl, text)
57
- except re.error as exc:
58
- raise MapSyntaxError(
59
- f'Regex error in syntax provider {provider}: {exc}'
60
- ) from exc
61
- return text
62
-
63
- # layered maps
64
- def _apply_maps(self, text: str, ctx: dict, layerstack: LayerStack):
65
- syntax_patterns = []
66
- for m in layerstack:
67
- if isinstance(m, type) and hasattr(m, 'as_map'):
68
- m = m()
69
- if hasattr(m, 'as_map'):
70
- syntax_patterns.append(m.get_syntax())
71
- elif isinstance(m, dict):
72
- pattern = ctx.get('syntax')
73
- if pattern:
74
- syntax_patterns.append(pattern)
75
- syntax_patterns = list(dict.fromkeys(syntax_patterns))
76
- for pattern in syntax_patterns:
77
- def repl(match: re.Match):
78
- k = match.group(1)
79
- for m in layerstack:
80
- layer = m
81
- if isinstance(layer, type) and hasattr(layer, 'as_map'):
82
- layer = layer()
83
- if hasattr(layer, 'as_map'):
84
- d = layer.as_map()
85
- elif isinstance(layer, dict):
86
- d = layer
87
- else:
88
- continue
89
- if k in d:
90
- return str(d[k])
91
-
92
- if k in ctx:
93
- return str(ctx[k])
94
-
95
- raise MissingKeyError(f'Missing key "{k}" in any layer or context')
96
-
97
- try:
98
- text = re.sub(pattern, repl, text)
99
- except re.error as exc:
100
- raise MapSyntaxError(f'Regex error in syntax pattern {pattern}: {exc}') from exc
101
- return text
102
-
103
- # context
104
- def _apply_ctx(self, text: str, ctx: dict) -> str:
105
- if not ctx:
106
- return text
107
- pattern = syntax.braces
108
- d = ctx
109
- def repl(match: re.Match) -> str:
110
- k = match.group(1)
111
- if k not in d:
112
- raise MissingKeyError(f'Missing ctx key "{k}"')
113
- return str(d[k])
114
- try:
115
- return re.sub(pattern, repl, text)
116
- except re.error as exc:
117
- raise MapSyntaxError(f'Regex error in ctx syntax: {exc}') from exc
118
-
119
- # one pass
120
- def _resolve_once(self, text: str, ctx: dict, layerstack: LayerStack) -> str:
121
- text = self._apply_pipeline(text, ctx)
122
- text = self._apply_syntax(text, ctx)
123
- text = self._apply_maps(text, ctx, layerstack)
124
- text = self._apply_ctx(text, ctx)
125
- return text
126
-
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
- # public api
138
- def res(
139
- self,
140
- text: str,
141
- *,
142
- extra_maps: dict | None = None,
143
- override_maps: dict | None = None,
144
- **ctx
145
- ) -> str:
146
- '''
147
- Resolve a string using maps, syntax providers, pipeline, and context.
148
- '''
149
- try:
150
- if override_maps is not None:
151
- temp = LayerStack([Layer('override', override_maps, priority=0)])
152
- return self._recursive(text, ctx, temp)
153
-
154
- if extra_maps:
155
- temp = self.layers.clone()
156
- temp.add_layer(Layer('extra', extra_maps, priority=999))
157
- return self._recursive(text, ctx, temp)
158
-
159
- if self.cache_enabled:
160
- key = (text, tuple(sorted(ctx.items())))
161
- cached = self.cache.get(key)
162
- if cached is not None:
163
- return cached
164
-
165
- result = self._recursive(text, ctx, self.layers)
166
-
167
- if self.cache_enabled:
168
- self.cache.set(key, result)
169
-
170
- return result
171
-
172
- except MapResError:
173
- raise
174
- except Exception as exc:
175
- raise MapResError(f'Unhandled resolver error: {exc}') from exc
176
-
177
-
178
- # global resolver
179
- _DEFAULT = MapResolver()
180
-
181
- def res(text: str, **ctx) -> str:
182
- '''
183
- Resolve using the global default resolver.
184
- '''
185
- return _DEFAULT.res(text, **ctx)
186
-
187
- def setGlobalMaps(maps, *, name=None, priority=0):
188
- """
189
- Register a global map or datamap class into the default resolver.
190
-
191
- maps: datamap class OR dict
192
- name: optional layer name (default = class name or 'global')
193
- priority: layer priority (lower = earlier)
194
- """
195
- layer_name = name or getattr(maps, "__name__", "global")
196
-
197
- # Wrap datamap class or dict in a Layer
198
- layer = Layer(layer_name, maps=[maps], priority=priority)
199
-
200
- # Add to global resolver
201
- _DEFAULT.layers.add_layer(layer)
File without changes
File without changes
File without changes
File without changes