|
1 | | -from typing import Any, Generic, Self, TypeVar, Unpack |
| 1 | +from __future__ import annotations |
2 | 2 |
|
3 | | -from pydantic import ConfigDict, model_validator |
| 3 | +from abc import ABC, abstractmethod |
| 4 | +from typing import Any, ClassVar, Generic, Self, TypeVar, Unpack |
| 5 | + |
| 6 | +from pydantic import ConfigDict, Field, model_validator |
4 | 7 | from pydantic.functional_validators import ModelWrapValidatorHandler |
| 8 | +from typing_extensions import override |
5 | 9 |
|
6 | 10 | from hexdoc.core import ResourceLocation |
7 | 11 | from hexdoc.minecraft import LocalizedStr |
|
14 | 18 |
|
15 | 19 | _T_Recipe = TypeVar("_T_Recipe", bound=Recipe) |
16 | 20 |
|
| 21 | +_T_PageWithAccumulator = TypeVar( |
| 22 | + "_T_PageWithAccumulator", |
| 23 | + bound="PageWithAccumulator[Any]", |
| 24 | + contravariant=True, |
| 25 | +) |
| 26 | + |
| 27 | +_T_PageWithRecipeAccumulator = TypeVar( |
| 28 | + "_T_PageWithRecipeAccumulator", |
| 29 | + bound="PageWithRecipeAccumulator[Any]", |
| 30 | + contravariant=True, |
| 31 | +) |
| 32 | + |
17 | 33 |
|
18 | 34 | class Page(TypeTaggedTemplate, AdvancementSpoilered, Flagged, type=None): |
19 | 35 | """Base class for Patchouli page types. |
@@ -107,3 +123,139 @@ class PageWithDoubleRecipe(PageWithTitle, Generic[_T_Recipe], type=None): |
107 | 123 | @property |
108 | 124 | def recipes(self) -> list[_T_Recipe]: |
109 | 125 | return [r for r in [self.recipe, self.recipe2] if r is not None] |
| 126 | + |
| 127 | + |
| 128 | +class PageWithAccumulator(Page, ABC, type=None): |
| 129 | + """Base class for pages that can merge together when adjacent.""" |
| 130 | + |
| 131 | + @classmethod |
| 132 | + @abstractmethod |
| 133 | + def accumulator_type(cls) -> type[AccumulatorPage[Any]]: |
| 134 | + """Returns the RecipeAccumulator class for this page type. |
| 135 | +
|
| 136 | + The template type of the returned class must match that of this class. |
| 137 | + """ |
| 138 | + |
| 139 | + @property |
| 140 | + @abstractmethod |
| 141 | + def accumulator_title(self) -> LocalizedStr | None: |
| 142 | + """Returns the page's title, if any, for use in the accumulator.""" |
| 143 | + |
| 144 | + @property |
| 145 | + @abstractmethod |
| 146 | + def accumulator_text(self) -> FormatTree | None: |
| 147 | + """Returns the page's text, if any, for use in the accumulator.""" |
| 148 | + |
| 149 | + |
| 150 | +class PageWithRecipeAccumulator( |
| 151 | + PageWithAccumulator, ABC, Generic[_T_Recipe], type=None |
| 152 | +): |
| 153 | + @property |
| 154 | + @abstractmethod |
| 155 | + def accumulator_recipes(self) -> list[_T_Recipe]: |
| 156 | + """Returns the page's recipes for use in the accumulator.""" |
| 157 | + |
| 158 | + |
| 159 | +class PageWithDoubleRecipeAccumulator( |
| 160 | + PageWithDoubleRecipe[_T_Recipe], |
| 161 | + PageWithRecipeAccumulator[_T_Recipe], |
| 162 | + ABC, |
| 163 | + Generic[_T_Recipe], |
| 164 | + type=None, |
| 165 | +): |
| 166 | + @property |
| 167 | + @override |
| 168 | + def accumulator_title(self) -> LocalizedStr | None: |
| 169 | + return self.title |
| 170 | + |
| 171 | + @property |
| 172 | + @override |
| 173 | + def accumulator_text(self) -> FormatTree | None: |
| 174 | + return self.text |
| 175 | + |
| 176 | + @property |
| 177 | + @override |
| 178 | + def accumulator_recipes(self) -> list[_T_Recipe]: |
| 179 | + return self.recipes |
| 180 | + |
| 181 | + |
| 182 | +class AccumulatorPage(PageWithTitle, ABC, Generic[_T_PageWithAccumulator], type=None): |
| 183 | + """Base class for virtual pages generated to merge adjacent instances of a |
| 184 | + PageWithAccumulator page type together.""" |
| 185 | + |
| 186 | + _page_type: ClassVar[ResourceLocation | None] |
| 187 | + |
| 188 | + def __init_subclass__( |
| 189 | + cls, |
| 190 | + *, |
| 191 | + page_type: type[PageWithAccumulator[_T_PageWithAccumulator]] | None = None, |
| 192 | + **kwargs: Unpack[ConfigDict], |
| 193 | + ) -> None: |
| 194 | + if page_type: |
| 195 | + super().__init_subclass__( |
| 196 | + type=None, |
| 197 | + template_type=str(page_type.template_id), |
| 198 | + **kwargs, |
| 199 | + ) |
| 200 | + cls._page_type = page_type.type |
| 201 | + else: |
| 202 | + super().__init_subclass__(type=None, template_type=None, **kwargs) |
| 203 | + cls._page_type = None |
| 204 | + |
| 205 | + @classmethod |
| 206 | + def from_page(cls, page: _T_PageWithAccumulator) -> Self: |
| 207 | + """Constructs a new accumulator from the given page. |
| 208 | +
|
| 209 | + Note: `append(page)` is always called immediately after this. |
| 210 | + """ |
| 211 | + if cls._page_type is None: |
| 212 | + raise RuntimeError(f"Cannot instantiate {cls} because page_type is None") |
| 213 | + if page.type != cls._page_type: |
| 214 | + raise ValueError(f"Mismatched page type: {cls}, {page}") |
| 215 | + |
| 216 | + self = cls.model_construct() |
| 217 | + self.title = page.accumulator_title |
| 218 | + self.anchor = page.anchor |
| 219 | + self.advancement = page.advancement |
| 220 | + return self |
| 221 | + |
| 222 | + @property |
| 223 | + def has_content(self) -> bool: |
| 224 | + """Returns True if this accumulator contains any user-visible content.""" |
| 225 | + return bool(self.title or self.text) |
| 226 | + |
| 227 | + def can_append(self, page: _T_PageWithAccumulator) -> bool: |
| 228 | + """Returns True if this accumulator can append the given page.""" |
| 229 | + return ( |
| 230 | + self._page_type == page.type |
| 231 | + and self.title == page.accumulator_title |
| 232 | + and self.anchor == page.anchor |
| 233 | + and self.advancement == page.advancement |
| 234 | + ) |
| 235 | + |
| 236 | + @abstractmethod |
| 237 | + def append(self, page: _T_PageWithAccumulator): |
| 238 | + """Appends the given page to this accumulator.""" |
| 239 | + self.text = page.accumulator_text |
| 240 | + |
| 241 | + @property |
| 242 | + def can_append_more(self) -> bool: |
| 243 | + """Returns True if this accumulator can append more pages.""" |
| 244 | + return self.text is None |
| 245 | + |
| 246 | + |
| 247 | +class RecipeAccumulatorPage( |
| 248 | + AccumulatorPage[_T_PageWithRecipeAccumulator], |
| 249 | + Generic[_T_PageWithRecipeAccumulator, _T_Recipe], |
| 250 | +): |
| 251 | + recipes: list[_T_Recipe] = Field(default_factory=lambda: []) |
| 252 | + |
| 253 | + @property |
| 254 | + @override |
| 255 | + def has_content(self) -> bool: |
| 256 | + return super().has_content or bool(self.recipes) |
| 257 | + |
| 258 | + @override |
| 259 | + def append(self, page: _T_PageWithRecipeAccumulator): |
| 260 | + super().append(page) |
| 261 | + self.recipes += page.accumulator_recipes |
0 commit comments