Skip to content

Commit

Permalink
Move most of the loading out of Book validation
Browse files Browse the repository at this point in the history
  • Loading branch information
object-Object committed Nov 2, 2023
1 parent ad2e9c7 commit 8ac0cd3
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 92 deletions.
4 changes: 2 additions & 2 deletions doc/src/hexdoc/cli/utils/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def load_book(
all_metadata = load_all_metadata(props, pm, loader)
i18n = _load_i18n(loader, None, allow_missing)[lang]

_, data = Book.load_book_json(loader, props.book)
data = Book.load_book_json(loader, props.book)
book = load_hex_book(data, pm, loader, i18n, all_metadata)

return lang, book, i18n
Expand All @@ -90,7 +90,7 @@ def load_books(
with ModResourceLoader.clean_and_load_all(props, pm) as loader:
all_metadata = load_all_metadata(props, pm, loader)

_, book_data = Book.load_book_json(loader, props.book)
book_data = Book.load_book_json(loader, props.book)
books = dict[str, tuple[Book, I18n]]()

for lang, i18n in _load_i18n(loader, lang, allow_missing).items():
Expand Down
6 changes: 4 additions & 2 deletions doc/src/hexdoc/core/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
@dataclass(config=DEFAULT_CONFIG, kw_only=True)
class ModResourceLoader:
props: Properties
book_id: ResourceLocation
export_dir: Path | None
resource_dirs: list[PathResourceDir]

Expand Down Expand Up @@ -74,6 +75,7 @@ def load_all(
with ExitStack() as stack:
loader = cls(
props=props,
book_id=props.book,
export_dir=export_dir,
resource_dirs=[
path_resource_dir
Expand Down Expand Up @@ -127,7 +129,7 @@ def load_book_assets(
folder: Literal["categories", "entries", "templates"],
use_resource_pack: bool,
) -> Iterator[tuple[PathResourceDir, ResourceLocation, JSONDict]]:
is_extension = book_id != self.props.book
is_extension = book_id != self.book_id
if is_extension:
yield from self._load_book_assets(
book_id,
Expand All @@ -137,7 +139,7 @@ def load_book_assets(
)

yield from self._load_book_assets(
self.props.book,
self.book_id,
folder,
use_resource_pack=use_resource_pack,
allow_missing=is_extension,
Expand Down
7 changes: 6 additions & 1 deletion doc/src/hexdoc/core/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pathlib import Path
from typing import Any, ClassVar, Literal, Self

from pydantic import field_validator, model_serializer, model_validator
from pydantic import TypeAdapter, field_validator, model_serializer, model_validator
from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler

Expand Down Expand Up @@ -46,6 +46,11 @@ def from_str(cls, raw: str) -> Self:

return cls(**match.groupdict())

@classmethod
def model_validate(cls, value: Any, *, context: Any = None):
ta = TypeAdapter(cls)
return ta.validate_python(value, context=context)

@model_validator(mode="wrap")
@classmethod
def _pre_root(cls, values: Any, handler: ModelWrapValidatorHandler[Self]):
Expand Down
4 changes: 4 additions & 0 deletions doc/src/hexdoc/core/resource_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class PathResourceDir(BaseResourceDir):
def modid(self):
return self._modid

@property
def internal(self):
return not self.external

def set_modid(self, modid: str) -> Self:
self._modid = modid
return self
Expand Down
2 changes: 1 addition & 1 deletion doc/src/hexdoc/hexcasting/hex_book.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def load_hex_book(
book_id=cast_or_raise(data["id"], ResourceLocation),
all_metadata=all_metadata,
)
return Book.model_validate(data, context=context)
return Book.load_all_from_data(data, context)


class PatternMetadata(HexdocModel):
Expand Down
163 changes: 77 additions & 86 deletions doc/src/hexdoc/patchouli/book.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from typing import Any, Literal, Self
from collections import defaultdict
from typing import Any, Literal, Mapping, Self

from pydantic import Field, ValidationInfo, field_validator, model_validator
from pydantic import (
Field,
PrivateAttr,
ValidationInfo,
field_validator,
model_validator,
)

from hexdoc.core.compat import HexVersion
from hexdoc.core.loader import ModResourceLoader
from hexdoc.core.resource import ItemStack, ResLoc, ResourceLocation
from hexdoc.minecraft import I18n, LocalizedStr
from hexdoc.minecraft.i18n import I18nContext
from hexdoc.minecraft import LocalizedStr
from hexdoc.model import HexdocModel
from hexdoc.patchouli.text import BookLinkBases
from hexdoc.utils.deserialize import cast_or_raise
Expand All @@ -32,7 +38,9 @@ class Book(HexdocModel):

# not in book.json
id: ResourceLocation
i18n_data: I18n

_link_bases: BookLinkBases = PrivateAttr(default_factory=dict)
_categories: dict[ResourceLocation, Category] = PrivateAttr(default_factory=dict)

# required
name: LocalizedStr
Expand Down Expand Up @@ -72,114 +80,97 @@ class Book(HexdocModel):
pause_game: bool = False
text_overflow_mode: Literal["overflow", "resize", "truncate"] | None = None

@classmethod
def load_all(cls, data: dict[str, Any], context: BookContext) -> Self:
return cls.model_validate(data, context=context)

@classmethod
def load_book_json(cls, loader: ModResourceLoader, id: ResourceLocation):
resource_dir, data = cls._load_book_json(loader, id)
data = cls._load_book_resource(loader, id)

if HexVersion.get() <= HexVersion.v0_10_x and "extend" in data:
id = ResourceLocation.from_str(cast_or_raise(data["extend"], str))
resource_dir, data = cls._load_book_json(loader, id)
id = loader.book_id = ResourceLocation.model_validate(data["extend"])
return cls._load_book_resource(loader, id)

return resource_dir, data | {"id": id}
return data

@classmethod
def _load_book_json(cls, loader: ModResourceLoader, id: ResourceLocation):
return loader.load_resource(
type="data",
folder="patchouli_books",
id=id / "book",
)

@model_validator(mode="before")
def _pre_root(cls, data: Any, info: ValidationInfo):
if not info.context:
return data
context = cast_or_raise(info.context, I18nContext)

match data:
case {**values}:
return values | {
"i18n_data": context.i18n,
"index_icon": values.get("index_icon") or values.get("model"),
}
case _:
return data
def load_all_from_data(cls, data: Mapping[str, Any], context: BookContext) -> Self:
book = cls.model_validate(data, context=context)
book._load_categories(context)
book._load_entries(context)
return book

@field_validator("use_resource_pack", mode="after")
def _check_use_resource_pack(cls, value: bool):
if HexVersion.get() >= HexVersion.v0_11_x and not value:
raise ValueError(f"use_resource_pack must be True on this version")
return value

@model_validator(mode="after")
def _load_categories_and_entries(self, info: ValidationInfo) -> Self:
if not info.context:
return self
context = cast_or_raise(info.context, BookContext)

# make the macros accessible when rendering the template
self.macros |= context.macros
@classmethod
def _load_book_resource(cls, loader: ModResourceLoader, id: ResourceLocation):
_, data = loader.load_resource("data", "patchouli_books", id / "book")
return data | {"id": id}

self._link_bases: BookLinkBases = {}
@property
def categories(self):
return self._categories

# load categories
self._categories = Category.load_all(context, self.id, self.use_resource_pack)
for id, category in self._categories.items():
self._link_bases[(id, None)] = context.get_link_base(category.resource_dir)
@property
def link_bases(self):
return self._link_bases

if not self._categories:
raise ValueError(
"No categories found. "
"Ensure the paths in your properties file are correct."
)
def _load_categories(self, context: BookContext):
categories = Category.load_all(context, self.id, self.use_resource_pack)

# load entries
found_internal_entries = self._load_all_entries(context)
if not found_internal_entries:
if not categories:
raise ValueError(
"No internal entries found. "
"Ensure the paths in your properties file are correct."
"No categories found, are the paths in your properties file correct?"
)

# we inserted a bunch of entries in no particular order, so sort each category
for category in self._categories.values():
category.entries = sorted_dict(category.entries)

return self
for id, category in categories.items():
self._categories[id] = category
self.link_bases[(id, None)] = context.get_link_base(category.resource_dir)

def _load_all_entries(self, context: BookContext):
found_internal_entries = False
def _load_entries(self, context: BookContext):
internal_entries = defaultdict[ResLoc, dict[ResLoc, Entry]](dict)

for resource_dir, id, data in context.loader.load_book_assets(
self.id,
"entries",
self.use_resource_pack,
book_id=self.id,
folder="entries",
use_resource_pack=self.use_resource_pack,
):
entry = Entry.load(resource_dir, id, data, context)

# i used the entry to insert the entry (pretty sure thanos said that)
if resource_dir.internal:
internal_entries[entry.category_id][entry.id] = entry

link_base = context.get_link_base(resource_dir)
self._link_bases[(id, None)] = link_base
for page in entry.pages:
if page.anchor is not None:
self._link_bases[(id, page.anchor)] = link_base

# i used the entry to insert the entry (pretty sure thanos said that)
if not resource_dir.external:
found_internal_entries = True
self._categories[entry.category_id].entries[entry.id] = entry
if not internal_entries:
raise ValueError(
f"No internal entries found for book {self.id}, is this the correct id?"
)

return found_internal_entries
for category_id, new_entries in internal_entries.items():
category = self._categories[category_id]
category.entries = sorted_dict(category.entries | new_entries)

@property
def categories(self):
# this exists because otherwise Pydantic complains that we're assigning to a
# nonexistent field; it ignores underscore-prefixed fields
return self._categories
@model_validator(mode="before")
@classmethod
def _pre_root(cls, data: dict[Any, Any] | Any):
if isinstance(data, dict) and "index_icon" not in data:
data["index_icon"] = data.get("model")
return data

@property
def link_bases(self):
return self._link_bases
@field_validator("use_resource_pack", mode="after")
def _check_use_resource_pack(cls, value: bool):
if HexVersion.get() >= HexVersion.v0_11_x and not value:
raise ValueError(f"use_resource_pack must be True on this version")
return value

@model_validator(mode="after")
def _post_root(self, info: ValidationInfo):
if not info.context:
return self
context = cast_or_raise(info.context, BookContext)

# make the macros accessible when rendering the template
self.macros |= context.macros

return self

0 comments on commit 8ac0cd3

Please sign in to comment.