Minor refactors and some work on bundling book data
This commit is contained in:
parent
ba13bf3836
commit
71258eb72b
12 changed files with 87 additions and 50 deletions
|
@ -37,8 +37,8 @@ show_landing_text = true
|
|||
hexcasting = "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources"
|
||||
|
||||
[i18n]
|
||||
lang = "en_us"
|
||||
filename = "{lang}.json"
|
||||
default_lang = "en_us"
|
||||
filename = "{default_lang}.json"
|
||||
[i18n.extra]
|
||||
"item.minecraft.amethyst_shard" = "Amethyst Shard"
|
||||
"item.minecraft.budding_amethyst" = "Budding Amethyst"
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build]
|
||||
packages = ["src/hexdoc"]
|
||||
|
||||
|
||||
[project]
|
||||
name = "HexDoc"
|
||||
name = "hexdoc"
|
||||
version = "0.1.0"
|
||||
authors = [
|
||||
{ name="Alwinfy" },
|
||||
|
@ -26,13 +23,17 @@ dependencies = [
|
|||
dev = [
|
||||
"black==23.7.0",
|
||||
"isort==5.12.0",
|
||||
"pytest==7.3.1",
|
||||
"syrupy==4.0.2",
|
||||
"pytest~=7.3.1",
|
||||
"syrupy~=4.0.2",
|
||||
"hatchling",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
hexdoc = "hexdoc.hexdoc:main"
|
||||
|
||||
[project.entry-points."hexdoc.book_data"]
|
||||
"hexcasting:thehexbook" = "hexdoc._book_data"
|
||||
|
||||
[project.entry-points."hexdoc.Page"]
|
||||
hexdoc-patchouli = "hexdoc.patchouli.page.pages"
|
||||
hexdoc-hexcasting = "hexdoc.hexcasting.page.hex_pages"
|
||||
|
@ -46,6 +47,10 @@ hexdoc-minecraft = "hexdoc.minecraft.recipe.ingredients"
|
|||
hexdoc-hexcasting = "hexdoc.hexcasting.hex_recipes"
|
||||
|
||||
|
||||
[tool.hatch.build]
|
||||
packages = ["src/hexdoc"]
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = ["--import-mode=importlib"]
|
||||
markers = [
|
||||
|
@ -103,10 +108,6 @@ reportUntypedBaseClass = "error"
|
|||
reportUntypedClassDecorator = "error"
|
||||
reportUntypedFunctionDecorator = "error"
|
||||
reportUntypedNamedTuple = "error"
|
||||
reportUnusedClass = "error"
|
||||
reportUnusedExpression = "error"
|
||||
reportUnusedFunction = "error"
|
||||
reportUnusedVariable = "error"
|
||||
reportWildcardImportFromLibrary = "error"
|
||||
|
||||
reportMissingTypeArgument = "warning"
|
||||
|
@ -116,6 +117,10 @@ reportUnknownLambdaType = "warning"
|
|||
reportUnknownMemberType = "warning"
|
||||
reportUnnecessaryComparison = "warning"
|
||||
reportUnnecessaryIsInstance = "warning"
|
||||
reportUnusedClass = "warning"
|
||||
reportUnusedExpression = "warning"
|
||||
reportUnusedFunction = "warning"
|
||||
reportUnusedImport = "warning"
|
||||
reportUnusedVariable = "warning"
|
||||
|
||||
reportUnknownVariableType = "none"
|
||||
|
|
1
doc/src/hexdoc/_book_data/__init__.py
Normal file
1
doc/src/hexdoc/_book_data/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
BOOK_DATA_PATH = "data.json"
|
0
doc/src/hexdoc/_templates/__init__.py
Normal file
0
doc/src/hexdoc/_templates/__init__.py
Normal file
|
@ -16,7 +16,7 @@ from hexdoc.utils import (
|
|||
Properties,
|
||||
ResourceLocation,
|
||||
)
|
||||
from hexdoc.utils.deserialize import isinstance_or_raise, load_json
|
||||
from hexdoc.utils.deserialize import isinstance_or_raise, load_json_dict
|
||||
|
||||
|
||||
class I18nContext(TypedDict):
|
||||
|
@ -115,7 +115,7 @@ class I18n:
|
|||
# we could also use that to ensure all i18n files have the same set of keys
|
||||
lang_dir = props.resources_dir / "assets" / props.modid / "lang"
|
||||
path = lang_dir / props.i18n.filename
|
||||
raw_lookup = load_json(path) | (props.i18n.extra or {})
|
||||
raw_lookup = load_json_dict(path) | (props.i18n.extra or {})
|
||||
|
||||
# validate and insert
|
||||
self.lookup = {}
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any, Self, cast
|
|||
from pydantic import ValidationInfo, model_validator
|
||||
|
||||
from hexdoc.utils import AnyPropsContext, ResourceLocation, TypeTaggedUnion
|
||||
from hexdoc.utils.deserialize import load_json
|
||||
from hexdoc.utils.deserialize import load_json_dict
|
||||
|
||||
|
||||
class Recipe(TypeTaggedUnion[AnyPropsContext], group="hexdoc.Recipe", type=None):
|
||||
|
@ -35,7 +35,7 @@ class Recipe(TypeTaggedUnion[AnyPropsContext], group="hexdoc.Recipe", type=None)
|
|||
path = recipe_dir / f"{id.path}.json"
|
||||
if recipe_dir == context["props"].default_recipe_dir:
|
||||
# only load from one file
|
||||
values = load_json(path) | {"id": id}
|
||||
values = load_json_dict(path) | {"id": id}
|
||||
elif not path.exists():
|
||||
# this is to ensure the recipe at least exists on all platforms
|
||||
# because we've had issues with that before (eg. Hexal's Mote Nexus)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from importlib import resources
|
||||
from importlib.metadata import entry_points
|
||||
from typing import Any, Generic, Literal, Self, cast
|
||||
|
||||
from pydantic import Field, ValidationInfo, model_validator
|
||||
|
@ -12,7 +14,7 @@ from hexdoc.utils import (
|
|||
ResLoc,
|
||||
ResourceLocation,
|
||||
)
|
||||
from hexdoc.utils.deserialize import isinstance_or_raise, load_json
|
||||
from hexdoc.utils.deserialize import isinstance_or_raise, load_json_dict
|
||||
|
||||
from .book_models import AnyBookContext, BookContext
|
||||
from .category import Category
|
||||
|
@ -33,8 +35,7 @@ class Book(Generic[AnyContext, AnyBookContext], HexDocModel[AnyBookContext]):
|
|||
"""
|
||||
|
||||
# not in book.json
|
||||
context: AnyBookContext = Field(default_factory=dict)
|
||||
categories: dict[ResourceLocation, Category] = Field(default_factory=dict)
|
||||
i18n_data: I18n
|
||||
|
||||
# required
|
||||
name: LocalizedStr
|
||||
|
@ -65,51 +66,73 @@ class Book(Generic[AnyContext, AnyBookContext], HexDocModel[AnyBookContext]):
|
|||
custom_book_item: ItemStack | None = None
|
||||
show_toasts: bool = True
|
||||
use_blocky_font: bool = False
|
||||
do_i18n: bool = Field(default=False, alias="i18n")
|
||||
i18n: bool = False
|
||||
macros: dict[str, str] = Field(default_factory=dict)
|
||||
pause_game: bool = False
|
||||
text_overflow_mode: Literal["overflow", "resize", "truncate"] | None = None
|
||||
extend: str | None = None
|
||||
"""NOTE: currently this WILL NOT load values from the target book!"""
|
||||
extend: ResourceLocation | None = None
|
||||
allow_extensions: bool = True
|
||||
|
||||
@classmethod
|
||||
def load(cls, data: dict[str, Any], context: AnyBookContext):
|
||||
return cls.model_validate(data, context=context)
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, props: Properties) -> tuple[dict[str, Any], BookContext]:
|
||||
# read the raw dict from the json file
|
||||
path = props.book_dir / "book.json"
|
||||
data = load_json(path)
|
||||
assert isinstance_or_raise(data, dict[str, Any])
|
||||
data = load_json_dict(path)
|
||||
|
||||
# NOW we can convert the actual book data
|
||||
return data, {
|
||||
"i18n": I18n(props, data["i18n"]),
|
||||
# set up the deserialization context object
|
||||
assert isinstance_or_raise(data["i18n"], bool)
|
||||
assert isinstance_or_raise(data["macros"], dict)
|
||||
context: BookContext = {
|
||||
"props": props,
|
||||
"i18n": I18n(props, data["i18n"]),
|
||||
"macros": DEFAULT_MACROS | data["macros"],
|
||||
}
|
||||
|
||||
return data, context
|
||||
|
||||
@classmethod
|
||||
def load(cls, data: dict[str, Any], context: AnyBookContext) -> Self:
|
||||
return cls.model_validate(data, context=context)
|
||||
|
||||
@classmethod
|
||||
def from_id(cls, book_id: ResourceLocation) -> Self:
|
||||
# load the module for the given book id using the entry point
|
||||
# TODO: this is untested because it needs to change for 0.11 anyway :/
|
||||
books = entry_points(group="hexdoc.book_data")
|
||||
book_module = books[str(book_id)].load()
|
||||
|
||||
# read and validate the actual data file
|
||||
book_path = resources.files(book_module) / book_module.BOOK_DATA_PATH
|
||||
return cls.model_validate_json(book_path.read_text("utf-8"))
|
||||
|
||||
@model_validator(mode="before")
|
||||
def _pre_root(cls, data: dict[str, Any], info: ValidationInfo) -> dict[str, Any]:
|
||||
context = cast(AnyBookContext, info.context)
|
||||
if not context:
|
||||
return data
|
||||
|
||||
return data | {
|
||||
"i18n_data": context["i18n"],
|
||||
}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _post_root(self, info: ValidationInfo) -> Self:
|
||||
"""Loads categories and entries."""
|
||||
context = cast(AnyBookContext, info.context)
|
||||
if not context:
|
||||
return self
|
||||
self.context = context
|
||||
|
||||
# categories
|
||||
self.categories = Category.load_all(context)
|
||||
# load categories
|
||||
self._categories: dict[ResourceLocation, Category] = Category.load_all(context)
|
||||
|
||||
# entries
|
||||
# load entries
|
||||
for path in context["props"].entries_dir.rglob("*.json"):
|
||||
# i used the entry to insert the entry (pretty sure thanos said that)
|
||||
entry = Entry.load(path, context)
|
||||
self.categories[entry.category_id].entries.append(entry)
|
||||
# i used the entry to insert the entry (pretty sure thanos said that)
|
||||
self._categories[entry.category_id].entries.append(entry)
|
||||
|
||||
# we inserted a bunch of entries in no particular order, so sort each category
|
||||
for category in self.categories.values():
|
||||
for category in self._categories.values():
|
||||
category.entries.sort()
|
||||
|
||||
return self
|
||||
|
@ -120,5 +143,5 @@ class Book(Generic[AnyContext, AnyBookContext], HexDocModel[AnyBookContext]):
|
|||
return self.model if self.index_icon_ is None else self.index_icon_
|
||||
|
||||
@property
|
||||
def props(self) -> Properties:
|
||||
return self.context["props"]
|
||||
def categories(self):
|
||||
return self._categories
|
||||
|
|
|
@ -216,10 +216,14 @@ class ParagraphStyle(Style, frozen=True):
|
|||
return out.element("p", **self.attributes)
|
||||
|
||||
|
||||
def is_external_link(value: str) -> bool:
|
||||
return value.startswith(("https:", "http:"))
|
||||
|
||||
|
||||
def _format_href(value: str) -> str:
|
||||
if not value.startswith(("http:", "https:")):
|
||||
return "#" + value.replace("#", "@")
|
||||
return value
|
||||
if is_external_link(value):
|
||||
return value
|
||||
return f"#{value.replace('#', '@')}"
|
||||
|
||||
|
||||
class FunctionStyle(Style, frozen=True):
|
||||
|
|
|
@ -38,7 +38,7 @@ JSONDict = dict[str, "JSONValue"]
|
|||
JSONValue = JSONDict | list["JSONValue"] | str | int | float | bool | None
|
||||
|
||||
|
||||
def load_json(path: Path) -> JSONDict:
|
||||
def load_json_dict(path: Path) -> JSONDict:
|
||||
data: JSONValue = json.loads(path.read_text("utf-8"))
|
||||
assert isinstance_or_raise(data, dict)
|
||||
return data
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar, dataclass_transfo
|
|||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from .deserialize import load_json
|
||||
from .deserialize import load_json_dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.root_model import Model
|
||||
|
@ -56,5 +56,5 @@ class FrozenHexDocModel(Generic[AnyContext], HexDocModel[AnyContext]):
|
|||
class HexDocFileModel(HexDocModel[AnyContext]):
|
||||
@classmethod
|
||||
def load(cls, path: Path, context: AnyContext) -> Self:
|
||||
data = load_json(path) | {"__path": path}
|
||||
data = load_json_dict(path) | {"__path": path}
|
||||
return cls.model_validate(data, context=context)
|
||||
|
|
|
@ -33,7 +33,7 @@ class PlatformProps(HexDocModel[Any]):
|
|||
|
||||
|
||||
class I18nProps(HexDocModel[Any]):
|
||||
lang: str
|
||||
default_lang: str
|
||||
filename: str
|
||||
extra: dict[str, str] | None = None
|
||||
|
||||
|
@ -78,7 +78,7 @@ class Properties(HexDocModel[Any]):
|
|||
|
||||
@property
|
||||
def lang(self):
|
||||
return self.i18n.lang
|
||||
return self.i18n.default_lang
|
||||
|
||||
@property
|
||||
def book_dir(self) -> Path:
|
||||
|
|
|
@ -35,12 +35,16 @@ class BaseResourceLocation:
|
|||
cls._from_str_regex = regex
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, raw: str) -> Self:
|
||||
def from_str(cls, raw: str, default_namespace: str | None = None) -> Self:
|
||||
match = cls._from_str_regex.fullmatch(raw)
|
||||
if match is None:
|
||||
raise ValueError(f"Invalid {cls.__name__} string: {raw}")
|
||||
|
||||
return cls(**match.groupdict())
|
||||
groups = match.groupdict()
|
||||
if not groups.get("namespace") and default_namespace is not None:
|
||||
groups["namespace"] = default_namespace
|
||||
|
||||
return cls(**groups)
|
||||
|
||||
@model_validator(mode="wrap")
|
||||
@classmethod
|
||||
|
|
Loading…
Reference in a new issue