11import logging
22import re
3+ from abc import ABC , abstractmethod
34from pathlib import Path
5+ from typing import Any , Iterable , Literal
46
57from hexdoc .core import IsVersion , ModResourceLoader , Properties , ResourceLocation
68from hexdoc .minecraft import Tag
79from hexdoc .model import HexdocModel , StripHiddenModel , ValidationContextModel
810from hexdoc .utils import TRACE , RelativePath
9- from pydantic import Field
11+ from pydantic import Field , TypeAdapter
12+ from typing_extensions import override
1013
1114from .utils .pattern import Direction , PatternInfo
1215
@@ -23,16 +26,101 @@ def path(cls, modid: str) -> Path:
2326 return Path (f"{ modid } .patterns.hexdoc.json" )
2427
2528
26- class PatternStubProps (StripHiddenModel ):
29+ class BasePatternStubProps (StripHiddenModel , ABC ):
30+ type : Any
2731 path : RelativePath
28- regex : re .Pattern [str ]
29- per_world_value : str | None = "true"
3032 required : bool = True
3133 """If `True` (the default), raise an error if no patterns were loaded from here."""
3234
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+
33120
34121class HexProperties (StripHiddenModel ):
35- pattern_stubs : list [PatternStubProps ]
122+ pattern_stubs : list [PatternStubProps ] = Field (default_factory = list )
123+ allow_duplicates : bool = False
36124
37125
38126# conthext, perhaps
@@ -84,7 +172,7 @@ def _add_patterns_0_11(
84172
85173 # for each stub, load all the patterns in the file
86174 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 ):
88176 self ._add_pattern (pattern , signatures )
89177
90178 def _add_patterns_0_10 (
@@ -93,7 +181,7 @@ def _add_patterns_0_10(
93181 props : Properties ,
94182 ):
95183 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 ):
97185 self ._add_pattern (pattern , signatures )
98186
99187 def _add_pattern (self , pattern : PatternInfo , signatures : dict [str , PatternInfo ]):
@@ -103,47 +191,11 @@ def _add_pattern(self, pattern: PatternInfo, signatures: dict[str, PatternInfo])
103191 if duplicate := (
104192 self .patterns .get (pattern .id ) or signatures .get (pattern .signature )
105193 ):
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 )
107199
108200 self .patterns [pattern .id ] = pattern
109201 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