1
1
import logging
2
2
import re
3
+ from abc import ABC , abstractmethod
3
4
from pathlib import Path
5
+ from typing import Any , Iterable , Literal
4
6
5
7
from hexdoc .core import IsVersion , ModResourceLoader , Properties , ResourceLocation
6
8
from hexdoc .minecraft import Tag
7
9
from hexdoc .model import HexdocModel , StripHiddenModel , ValidationContextModel
8
10
from hexdoc .utils import TRACE , RelativePath
9
- from pydantic import Field
11
+ from pydantic import Field , TypeAdapter
12
+ from typing_extensions import override
10
13
11
14
from .utils .pattern import Direction , PatternInfo
12
15
@@ -23,16 +26,101 @@ def path(cls, modid: str) -> Path:
23
26
return Path (f"{ modid } .patterns.hexdoc.json" )
24
27
25
28
26
- class PatternStubProps (StripHiddenModel ):
29
+ class BasePatternStubProps (StripHiddenModel , ABC ):
30
+ type : Any
27
31
path : RelativePath
28
- regex : re .Pattern [str ]
29
- per_world_value : str | None = "true"
30
32
required : bool = True
31
33
"""If `True` (the default), raise an error if no patterns were loaded from here."""
32
34
35
+ def load_patterns (
36
+ self ,
37
+ props : Properties ,
38
+ per_world_tag : Tag | None ,
39
+ ) -> list [PatternInfo ]:
40
+ logger .debug (f"Load { self .type } pattern stub from { self .path } " )
41
+
42
+ patterns = list [PatternInfo ]()
43
+
44
+ try :
45
+ for pattern in self ._iter_patterns (props ):
46
+ if per_world_tag is not None :
47
+ pattern .is_per_world = pattern .id in per_world_tag .values
48
+ patterns .append (pattern )
49
+ except Exception as e :
50
+ # hack: notes don't seem to be working on pydantic exceptions :/
51
+ logger .error (f"Failed to load { self .type } pattern stub from { self .path } ." )
52
+ raise e
53
+
54
+ pretty_path = self .path .resolve ().relative_to (Path .cwd ())
55
+
56
+ if self .required and not patterns :
57
+ raise ValueError (self ._no_patterns_error .format (path = pretty_path ))
58
+
59
+ logger .info (f"Loaded { len (patterns )} patterns from { pretty_path } " )
60
+ return patterns
61
+
62
+ @abstractmethod
63
+ def _iter_patterns (self , props : Properties ) -> Iterable [PatternInfo ]:
64
+ """Loads and iterates over the patterns from this stub.
65
+
66
+ Note: the `is_per_world` value returned by this function should be **ignored**
67
+ in 0.11+, since that information can be found in the per world tag.
68
+ """
69
+
70
+ @property
71
+ def _no_patterns_error (self ) -> str :
72
+ return "No patterns found in {path}, but required is True"
73
+
74
+
75
+ class RegexPatternStubProps (BasePatternStubProps ):
76
+ """Fetches pattern info by scraping source code with regex."""
77
+
78
+ type : Literal ["regex" ] = "regex"
79
+ regex : re .Pattern [str ]
80
+ per_world_value : str | None = "true"
81
+
82
+ @override
83
+ def _iter_patterns (self , props : Properties ) -> Iterable [PatternInfo ]:
84
+ stub_text = self .path .read_text ("utf-8" )
85
+
86
+ for match in self .regex .finditer (stub_text ):
87
+ groups = match .groupdict ()
88
+
89
+ if ":" in groups ["name" ]:
90
+ id = ResourceLocation .from_str (groups ["name" ])
91
+ else :
92
+ id = props .mod_loc (groups ["name" ])
93
+
94
+ yield PatternInfo (
95
+ id = id ,
96
+ startdir = Direction [groups ["startdir" ]],
97
+ signature = groups ["signature" ],
98
+ is_per_world = groups .get ("is_per_world" ) == self .per_world_value ,
99
+ )
100
+
101
+ @property
102
+ @override
103
+ def _no_patterns_error (self ):
104
+ return super ()._no_patterns_error + " (check the pattern regex)"
105
+
106
+
107
+ class JsonPatternStubProps (BasePatternStubProps ):
108
+ """Fetches pattern info from a JSON file."""
109
+
110
+ type : Literal ["json" ]
111
+
112
+ @override
113
+ def _iter_patterns (self , props : Properties ) -> Iterable [PatternInfo ]:
114
+ data = self .path .read_bytes ()
115
+ return TypeAdapter (list [PatternInfo ]).validate_json (data )
116
+
117
+
118
+ PatternStubProps = RegexPatternStubProps | JsonPatternStubProps
119
+
33
120
34
121
class HexProperties (StripHiddenModel ):
35
- pattern_stubs : list [PatternStubProps ]
122
+ pattern_stubs : list [PatternStubProps ] = Field (default_factory = list )
123
+ allow_duplicates : bool = False
36
124
37
125
38
126
# conthext, perhaps
@@ -84,7 +172,7 @@ def _add_patterns_0_11(
84
172
85
173
# for each stub, load all the patterns in the file
86
174
for stub in self .hex_props .pattern_stubs :
87
- for pattern in self . _load_stub_patterns (loader .props , stub , per_world ):
175
+ for pattern in stub . load_patterns (loader .props , per_world ):
88
176
self ._add_pattern (pattern , signatures )
89
177
90
178
def _add_patterns_0_10 (
@@ -93,7 +181,7 @@ def _add_patterns_0_10(
93
181
props : Properties ,
94
182
):
95
183
for stub in self .hex_props .pattern_stubs :
96
- for pattern in self . _load_stub_patterns (props , stub , None ):
184
+ for pattern in stub . load_patterns (props , None ):
97
185
self ._add_pattern (pattern , signatures )
98
186
99
187
def _add_pattern (self , pattern : PatternInfo , signatures : dict [str , PatternInfo ]):
@@ -103,47 +191,11 @@ def _add_pattern(self, pattern: PatternInfo, signatures: dict[str, PatternInfo])
103
191
if duplicate := (
104
192
self .patterns .get (pattern .id ) or signatures .get (pattern .signature )
105
193
):
106
- raise ValueError (f"Duplicate pattern { pattern .id } \n { pattern } \n { duplicate } " )
194
+ message = f"pattern { pattern .id } \n { pattern } \n { duplicate } "
195
+ if self .hex_props .allow_duplicates :
196
+ logger .warning ("Ignoring duplicate " + message )
197
+ return
198
+ raise ValueError ("Duplicate" + message )
107
199
108
200
self .patterns [pattern .id ] = pattern
109
201
signatures [pattern .signature ] = pattern
110
-
111
- def _load_stub_patterns (
112
- self ,
113
- props : Properties ,
114
- stub : PatternStubProps ,
115
- per_world_tag : Tag | None ,
116
- ):
117
- # TODO: add Gradle task to generate json with this data. this is dumb and fragile.
118
- logger .debug (f"Load pattern stub from { stub .path } " )
119
- stub_text = stub .path .read_text ("utf-8" )
120
-
121
- patterns = list [PatternInfo ]()
122
-
123
- for match in stub .regex .finditer (stub_text ):
124
- groups = match .groupdict ()
125
- id = props .mod_loc (groups ["name" ])
126
-
127
- if per_world_tag is not None :
128
- is_per_world = id in per_world_tag .values
129
- else :
130
- is_per_world = groups .get ("is_per_world" ) == stub .per_world_value
131
-
132
- patterns .append (
133
- PatternInfo (
134
- id = id ,
135
- startdir = Direction [groups ["startdir" ]],
136
- signature = groups ["signature" ],
137
- is_per_world = is_per_world ,
138
- )
139
- )
140
-
141
- pretty_path = stub .path .resolve ().relative_to (Path .cwd ())
142
-
143
- if stub .required and not patterns :
144
- raise ValueError (
145
- f"No patterns found in { pretty_path } (check the pattern regex)"
146
- )
147
-
148
- logger .info (f"Loaded { len (patterns )} patterns from { pretty_path } " )
149
- return patterns
0 commit comments