Refactor most of the core logic out of utils

This commit is contained in:
object-Object 2023-10-10 00:58:55 -04:00
parent 5b26c1d549
commit 98a68e1f15
50 changed files with 297 additions and 289 deletions

View file

@ -8,10 +8,10 @@ from typing import Annotated, Union
import typer
from hexdoc.core.loader import ModResourceLoader
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import I18n
from hexdoc.minecraft.assets.textures import AnimatedTexture, Texture
from hexdoc.utils import ModResourceLoader
from hexdoc.utils.resource import ResourceLocation
from .utils.load import load_book, load_books, load_common_data
from .utils.render import create_jinja_env, render_book

View file

@ -2,13 +2,14 @@ import logging
import subprocess
from pathlib import Path
from hexdoc.core.loader import ModResourceLoader
from hexdoc.core.metadata import HexdocMetadata
from hexdoc.core.properties import Properties
from hexdoc.hexcasting.hex_book import load_hex_book
from hexdoc.minecraft import I18n
from hexdoc.minecraft.assets.textures import Texture
from hexdoc.minecraft.assets import Texture
from hexdoc.patchouli import Book
from hexdoc.plugin import PluginManager
from hexdoc.utils import ModResourceLoader, Properties
from hexdoc.utils.metadata import HexdocMetadata
from .logging import setup_logging

View file

@ -14,20 +14,20 @@ from jinja2 import (
)
from jinja2.sandbox import SandboxedEnvironment
from hexdoc.core.metadata import HexdocMetadata
from hexdoc.core.properties import Properties
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import I18n
from hexdoc.minecraft.assets.textures import AnimatedTexture, Texture
from hexdoc.patchouli import Book
from hexdoc.utils import Properties
from hexdoc.utils.jinja_extensions import (
IncludeRawExtension,
from hexdoc.utils.jinja.extensions import IncludeRawExtension
from hexdoc.utils.jinja.macros import (
hexdoc_block,
hexdoc_localize,
hexdoc_texture,
hexdoc_wrap,
)
from hexdoc.utils.metadata import HexdocMetadata
from hexdoc.utils.path import write_to_path
from hexdoc.utils.resource import ResourceLocation
from .sitemap import MARKER_NAME, SitemapMarker

View file

@ -4,7 +4,7 @@ from pathlib import Path
from pydantic import Field, TypeAdapter
from hexdoc.utils import DEFAULT_CONFIG, HexdocModel
from hexdoc.model import DEFAULT_CONFIG, HexdocModel
from hexdoc.utils.path import write_to_path
MARKER_NAME = ".sitemap-marker.json"

View file

View file

@ -12,12 +12,12 @@ from typing import Any, Callable, Literal, Self, TypeVar, overload
from pydantic.dataclasses import dataclass
from hexdoc.plugin.manager import PluginManager
from hexdoc.model import DEFAULT_CONFIG, HexdocModel, ValidationContext
from hexdoc.plugin import PluginManager
from hexdoc.utils.deserialize.json import JSONDict, decode_json_dict
from hexdoc.utils.iterators import must_yield_something
from hexdoc.utils.path import strip_suffixes, write_to_path
from .deserialize import JSONDict, decode_json_dict
from .iterators import must_yield_something
from .model import DEFAULT_CONFIG, HexdocModel, ValidationContext
from .path import strip_suffixes, write_to_path
from .properties import Properties
from .resource import PathResourceDir, ResourceLocation, ResourceType

View file

@ -3,9 +3,9 @@ from typing import Self
from pydantic import model_validator
from hexdoc.minecraft.assets.textures import Texture, TextureContext
from hexdoc.minecraft.assets import Texture, TextureContext
from hexdoc.model import HexdocModel
from .model import HexdocModel
from .properties import NoTrailingSlashHttpUrl
from .resource import ResourceLocation

View file

@ -3,15 +3,15 @@ from __future__ import annotations
import logging
import re
from pathlib import Path
from typing import Annotated, Any, Self
from typing import Annotated, Any, Self, dataclass_transform
from pydantic import AfterValidator, Field, HttpUrl, field_validator
from pydantic import AfterValidator, Field, HttpUrl, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from .cd import RelativePath, relative_path_root
from .model import StripHiddenModel
from .resource import ResourceDir, ResourceLocation
from .toml_placeholders import load_toml_with_placeholders
from hexdoc.core.resource import ResourceDir, ResourceLocation
from hexdoc.model import HexdocModel
from hexdoc.utils.cd import RelativePath, relative_path_root
from hexdoc.utils.deserialize.toml import load_toml_with_placeholders
NoTrailingSlashHttpUrl = Annotated[
str,
@ -20,6 +20,22 @@ NoTrailingSlashHttpUrl = Annotated[
]
@dataclass_transform()
class StripHiddenModel(HexdocModel):
"""Base model which removes all keys starting with _ before validation."""
@model_validator(mode="before")
def _pre_root_strip_hidden(cls, values: Any) -> Any:
if not isinstance(values, dict):
return values
return {
key: value
for key, value in values.items()
if not (isinstance(key, str) and key.startswith("_"))
}
class EnvironmentVariableProps(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")

View file

@ -19,11 +19,10 @@ from pydantic import ValidationInfo, field_validator, model_serializer, model_va
from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler
from hexdoc.model import DEFAULT_CONFIG, HexdocModel
from hexdoc.plugin import PluginManager
from hexdoc.utils.cd import RelativePath, relative_path_root
from .model import DEFAULT_CONFIG, HexdocModel
ResourceType = Literal["assets", "data", ""]

View file

@ -4,14 +4,16 @@ from typing import Any, Mapping
from pydantic import Field, model_validator
from hexdoc.core.compat import HexVersion
from hexdoc.core.loader import ModResourceLoader
from hexdoc.core.metadata import HexdocMetadata
from hexdoc.core.properties import PatternStubProps
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import I18n, Tag
from hexdoc.model import HexdocModel, init_context
from hexdoc.patchouli import Book, BookContext
from hexdoc.plugin import PluginManager
from hexdoc.utils import HexdocModel, ModResourceLoader, ResourceLocation, init_context
from hexdoc.utils.compat import HexVersion
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.metadata import HexdocMetadata
from hexdoc.utils.properties import PatternStubProps
from .pattern import Direction, PatternInfo

View file

@ -2,11 +2,12 @@ from typing import Any, Literal
from pydantic import model_validator
from hexdoc.core.compat import HexVersion
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import LocalizedItem, Recipe
from hexdoc.minecraft.recipe import ItemIngredient, ItemIngredientList
from hexdoc.utils import HexdocModel, ResourceLocation, TypeTaggedUnion
from hexdoc.utils.compat import HexVersion
from hexdoc.utils.tagged_union import NoValue
from hexdoc.model import HexdocModel
from hexdoc.model.tagged_union import NoValue, TypeTaggedUnion
# ingredients

View file

@ -3,10 +3,10 @@ from typing import Any
from pydantic import ValidationInfo, model_validator
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.minecraft.i18n import I18nContext
from hexdoc.patchouli.page import PageWithText
from hexdoc.utils import ResourceLocation
from hexdoc.utils.deserialize import cast_or_raise
from ..pattern import RawPatternInfo

View file

@ -2,10 +2,10 @@ from typing import Any, Self
from pydantic import ValidationInfo, model_validator
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.minecraft.recipe import CraftingRecipe
from hexdoc.patchouli.page import PageWithText, PageWithTitle
from hexdoc.utils import ResourceLocation
from hexdoc.utils.deserialize import cast_or_raise
from ..hex_book import HexContext

View file

@ -3,7 +3,8 @@ from typing import Annotated, Any
from pydantic import BeforeValidator
from hexdoc.utils import HexdocModel, ResourceLocation
from hexdoc.core.resource import ResourceLocation
from hexdoc.model import HexdocModel
class Direction(Enum):

View file

@ -1,7 +1,21 @@
__all__ = [
"TextureContext",
"TAG_TEXTURE",
"AnimatedTexture",
"ItemWithGaslightingTexture",
"ItemWithTexture",
"MISSING_TEXTURE",
"TAG_TEXTURE",
"TagWithTexture",
"Texture",
"TextureContext",
]
from .textures import MISSING_TEXTURE, TAG_TEXTURE, TextureContext
from .textures import (
MISSING_TEXTURE,
TAG_TEXTURE,
AnimatedTexture,
ItemWithGaslightingTexture,
ItemWithTexture,
TagWithTexture,
Texture,
TextureContext,
)

View file

@ -1,6 +1,7 @@
from typing import Literal
from hexdoc.utils import HexdocModel, ResourceLocation
from hexdoc.core.resource import ResourceLocation
from hexdoc.model import HexdocModel
ItemDisplayPosition = Literal[
"thirdperson_righthand",
@ -13,6 +14,8 @@ ItemDisplayPosition = Literal[
"fixed",
]
# TODO
class ItemModel(HexdocModel, extra="ignore"):
parent: ResourceLocation

View file

@ -7,13 +7,14 @@ from typing import Literal, Self
from pydantic import Field, model_validator
from hexdoc.minecraft.i18n import I18nContext, LocalizedStr
from hexdoc.utils import HexdocModel, ResourceLocation
from hexdoc.utils.external import fetch_minecraft_textures
from hexdoc.utils.properties import Properties
from hexdoc.utils.resource import ItemStack
from hexdoc.utils.resource_loader import ModResourceLoader
from hexdoc.utils.resource_model import InlineItemModel, InlineModel
from hexdoc.core.loader import ModResourceLoader
from hexdoc.core.properties import Properties
from hexdoc.core.resource import ItemStack, ResourceLocation
from hexdoc.model import HexdocModel
from hexdoc.model.inline import InlineItemModel, InlineModel
from ..i18n import I18nContext, LocalizedStr
from .external import fetch_minecraft_textures
# 16x16 hashtag icon for tags
TAG_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAC4jAAAuIwF4pT92AAAANUlEQVQ4y2NgGJRAXV39v7q6+n9cfGTARKllFBvAiOxMUjTevHmTkSouGPhAHA0DWnmBrgAANLIZgSXEQxIAAAAASUVORK5CYII="

View file

@ -9,10 +9,12 @@ from typing import Any, Callable, Self
from pydantic import ValidationInfo, model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from hexdoc.utils import HexdocModel, ItemStack, ModResourceLoader, ResourceLocation
from hexdoc.utils.compat import HexVersion
from hexdoc.utils.deserialize import cast_or_raise, decode_and_flatten_json_dict
from hexdoc.utils.resource_loader import LoaderContext
from hexdoc.core.compat import HexVersion
from hexdoc.core.loader import LoaderContext, ModResourceLoader
from hexdoc.core.resource import ItemStack, ResourceLocation
from hexdoc.model import HexdocModel
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.deserialize.json import decode_and_flatten_json_dict
@total_ordering

View file

@ -1,5 +1,7 @@
from hexdoc.utils import ModResourceLoader, ResourceLocation, TypeTaggedUnion
from hexdoc.utils.resource_model import InlineIDModel
from hexdoc.core.loader import ModResourceLoader
from hexdoc.core.resource import ResourceLocation
from hexdoc.model.inline import InlineIDModel
from hexdoc.model.tagged_union import TypeTaggedUnion
from .ingredients import ItemResult

View file

@ -2,16 +2,17 @@ from typing import Annotated, Any, Iterator
from pydantic import AfterValidator, BeforeValidator, ValidationError, ValidationInfo
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft.assets.textures import (
ItemWithTexture,
TagWithTexture,
TextureContext,
)
from hexdoc.minecraft.tags import Tag
from hexdoc.utils import HexdocModel, NoValue, TypeTaggedUnion
from hexdoc.model import HexdocModel
from hexdoc.model.tagged_union import NoValue, TypeTaggedUnion
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.iterators import listify
from hexdoc.utils.resource import ResourceLocation
class ItemIngredient(TypeTaggedUnion, type=None):

View file

@ -2,7 +2,7 @@ from typing import Iterator
from pydantic import field_validator
from hexdoc.utils.compat import HexVersion
from hexdoc.core.compat import HexVersion
from .abstract_recipes import CraftingRecipe
from .ingredients import ItemIngredientList

View file

@ -4,8 +4,10 @@ from typing import Iterator, Self
from pydantic import Field
from hexdoc.utils import HexdocModel, LoaderContext, ResourceLocation
from hexdoc.utils.deserialize import decode_json_dict
from hexdoc.core.loader import LoaderContext
from hexdoc.core.resource import ResourceLocation
from hexdoc.model import HexdocModel
from hexdoc.utils.deserialize.json import decode_json_dict
class OptionalTagValue(HexdocModel, frozen=True):

View file

@ -0,0 +1,8 @@
__all__ = [
"HexdocModel",
"ValidationContext",
"DEFAULT_CONFIG",
"init_context",
]
from .base import DEFAULT_CONFIG, HexdocModel, ValidationContext, init_context

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from contextvars import ContextVar
from typing import TYPE_CHECKING, Any, Self, dataclass_transform
from pydantic import BaseModel, ConfigDict, model_validator
from pydantic import BaseModel, ConfigDict
from pydantic.config import ConfigDict
from hexdoc.utils.contextmanagers import set_contextvar
@ -80,19 +80,3 @@ class HexdocModel(HexdocBaseModel):
context: ValidationContext | None = None,
) -> Self:
...
@dataclass_transform()
class StripHiddenModel(HexdocModel):
"""Base model which removes all keys starting with _ before validation."""
@model_validator(mode="before")
def _pre_root_strip_hidden(cls, values: Any) -> Any:
if not isinstance(values, dict):
return values
return {
key: value
for key, value in values.items()
if not (isinstance(key, str) and key.startswith("_"))
}

View file

@ -7,10 +7,12 @@ from typing import Any, Self, dataclass_transform
from pydantic import ValidationInfo, model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from .deserialize import JSONDict, cast_or_raise
from .model import HexdocModel, ValidationContext
from .resource import ItemStack, PathResourceDir, ResourceLocation
from .resource_loader import LoaderContext, ModResourceLoader
from hexdoc.core.loader import LoaderContext, ModResourceLoader
from hexdoc.core.resource import ItemStack, PathResourceDir, ResourceLocation
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.deserialize.json import JSONDict
from .base import HexdocModel, ValidationContext
@dataclass_transform()

View file

@ -6,11 +6,11 @@ import more_itertools
from pydantic import ValidationInfo, model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from hexdoc.core.resource import ResourceLocation
from hexdoc.plugin.manager import PluginManagerContext
from hexdoc.utils.deserialize import cast_or_raise
from .model import HexdocModel
from .resource import ResourceLocation
from .base import HexdocModel
class NoValueType(Enum):

View file

@ -2,20 +2,15 @@ from typing import Any, Literal, Self
from pydantic import Field, 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.model import HexdocModel
from hexdoc.patchouli.text.formatting import BookLinkBases
from hexdoc.utils import (
Color,
HexdocModel,
ItemStack,
ModResourceLoader,
ResLoc,
ResourceLocation,
)
from hexdoc.utils.compat import HexVersion
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.types import sorted_dict
from hexdoc.utils.types import Color, sorted_dict
from .book_context import BookContext
from .category import Category

View file

@ -2,11 +2,10 @@ from typing import Self
from pydantic import Field, model_validator
from hexdoc.core.metadata import MetadataContext
from hexdoc.core.resource import PathResourceDir, ResourceLocation
from hexdoc.minecraft import Tag
from hexdoc.plugin.manager import PluginManagerContext
from hexdoc.utils import ResourceLocation
from hexdoc.utils.metadata import MetadataContext
from hexdoc.utils.resource import PathResourceDir
from .text.formatting import FormattingContext

View file

@ -2,9 +2,10 @@ from typing import Self
from pydantic import Field
from hexdoc.core.loader import LoaderContext
from hexdoc.core.resource import ItemStack, ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.utils import ItemStack, LoaderContext, ResourceLocation
from hexdoc.utils.resource_model import IDModel
from hexdoc.model.inline import IDModel
from hexdoc.utils.types import Sortable, sorted_dict
from .entry import Entry

View file

@ -2,14 +2,14 @@ from typing import Iterable, Iterator
from pydantic import Field, ValidationInfo, model_validator
from hexdoc.core.resource import ItemStack, ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.minecraft.recipe.abstract_recipes import CraftingRecipe
from hexdoc.model.inline import IDModel
from hexdoc.patchouli.page.abstract_pages import PageWithTitle
from hexdoc.patchouli.text.formatting import FormatTree
from hexdoc.utils import Color, ItemStack, ResourceLocation
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.resource_model import IDModel
from hexdoc.utils.types import Sortable
from hexdoc.utils.types import Color, Sortable
from .book_context import BookContext
from .page.pages import CraftingPage, Page

View file

@ -3,8 +3,9 @@ from typing import Any, ClassVar, Self
from pydantic import model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.utils import ResourceLocation, TypeTaggedUnion
from hexdoc.model.tagged_union import TypeTaggedUnion
from ..text import FormatTree

View file

@ -2,10 +2,10 @@ from typing import Any, Self
from pydantic import model_validator
from hexdoc.core.resource import Entity, ItemStack, ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.minecraft.assets.textures import ItemWithTexture, Texture
from hexdoc.minecraft.recipe import CraftingRecipe
from hexdoc.utils import Entity, ItemStack, ResourceLocation
from ..text import FormatTree
from .abstract_pages import Page, PageWithText, PageWithTitle

View file

@ -1,9 +1,6 @@
__all__ = [
"FormatTree",
"HTMLElement",
"HTMLStream",
"DEFAULT_MACROS",
]
from .formatting import DEFAULT_MACROS, FormatTree
from .html import HTMLElement, HTMLStream

View file

@ -12,12 +12,12 @@ from pydantic import Field, ValidationInfo, model_validator
from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler
from hexdoc.core.loader import LoaderContext
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import LocalizedStr
from hexdoc.minecraft.i18n import I18n, I18nContext
from hexdoc.utils import DEFAULT_CONFIG, HexdocModel
from hexdoc.model import DEFAULT_CONFIG, HexdocModel
from hexdoc.utils.deserialize import cast_or_raise
from hexdoc.utils.resource import ResourceLocation
from hexdoc.utils.resource_loader import LoaderContext
from hexdoc.utils.types import TryGetEnum
from .html import HTMLElement, HTMLStream

View file

@ -6,7 +6,7 @@ from typing import Callable, Generic, Iterator, ParamSpec, Sequence, TypeVar
import pluggy
from hexdoc.utils.model import ValidationContext
from hexdoc.model import ValidationContext
from .specs import HEXDOC_PROJECT_NAME, HookPackages, PluginSpec

View file

@ -1,32 +0,0 @@
__all__ = [
"HexdocModel",
"InternallyTaggedUnion",
"Color",
"ValidationContext",
"DEFAULT_CONFIG",
"NoValue",
"NoValueType",
"TagValue",
"Properties",
"Entity",
"ItemStack",
"ResLoc",
"ResourceLocation",
"ModResourceLoader",
"TypeTaggedUnion",
"LoaderContext",
"init_context",
]
from .model import DEFAULT_CONFIG, HexdocModel, ValidationContext, init_context
from .properties import Properties
from .resource import Entity, ItemStack, ResLoc, ResourceLocation
from .resource_loader import LoaderContext, ModResourceLoader
from .tagged_union import (
InternallyTaggedUnion,
NoValue,
NoValueType,
TagValue,
TypeTaggedUnion,
)
from .types import Color

View file

@ -4,7 +4,7 @@ from typing import Annotated
from pydantic import AfterValidator
from hexdoc.utils.contextmanagers import set_contextvar
from .contextmanagers import set_contextvar
_relative_path_root = ContextVar[Path]("_relative_path_root")

View file

@ -1,116 +0,0 @@
import logging
import re
from typing import Any, TypeGuard, TypeVar, get_origin
import pyjson5
_T = TypeVar("_T")
_T_cov = TypeVar("_T_cov", covariant=True)
_DEFAULT_MESSAGE_SHORT = "Expected any of {expected}, got {actual}"
_DEFAULT_MESSAGE_LONG = "Expected any of {expected}, got {actual}: {value}"
def isinstance_or_raise(
val: Any,
class_or_tuple: type[_T] | tuple[type[_T], ...],
message: str | None = None,
) -> TypeGuard[_T]:
"""Usage: `assert isinstance_or_raise(val, str)`
message placeholders: `{expected}`, `{actual}`, `{value}`
"""
# convert generic types into the origin type
if not isinstance(class_or_tuple, tuple):
class_or_tuple = (class_or_tuple,)
ungenericed_classes = tuple(get_origin(t) or t for t in class_or_tuple)
if not isinstance(val, ungenericed_classes):
# just in case the caller messed up the message formatting
subs = {
"expected": list(class_or_tuple),
"actual": type(val),
"value": val,
}
if logging.getLogger(__name__).getEffectiveLevel() >= logging.WARNING:
default_message = _DEFAULT_MESSAGE_SHORT
else:
default_message = _DEFAULT_MESSAGE_LONG
if message is None:
raise TypeError(default_message.format(**subs))
try:
raise TypeError(message.format(**subs))
except KeyError:
raise TypeError(default_message.format(**subs))
return True
def cast_or_raise(
val: Any,
class_or_tuple: type[_T] | tuple[type[_T], ...],
message: str | None = None,
) -> _T:
assert isinstance_or_raise(val, class_or_tuple, message)
return val
JSONDict = dict[str, "JSONValue"]
JSONValue = JSONDict | list["JSONValue"] | str | int | float | bool | None
def decode_json_dict(data: str | bytes) -> JSONDict:
match data:
case str():
decoded = pyjson5.decode(data)
case _:
decoded = pyjson5.decode_utf8(data)
assert isinstance_or_raise(decoded, dict)
return decoded
# implement pkpcpbp's flattening in python
# https://github.com/gamma-delta/PKPCPBP/blob/786194a590f/src/main/java/at/petrak/pkpcpbp/filters/JsonUtil.java
def decode_and_flatten_json_dict(data: str) -> dict[str, str]:
# replace `\<LF> foobar` with `\<LF>foobar`
data = re.sub(r"\\\n\s*", "\\\n", data)
# decode and flatten
decoded = decode_json_dict(data)
return _flatten_inner(decoded, "")
def _flatten_inner(obj: JSONDict, prefix: str) -> dict[str, str]:
out: dict[str, str] = {}
for key_stub, value in obj.items():
if not prefix:
key = key_stub
elif not key_stub:
key = prefix
elif prefix[-1] in ":_-/":
key = prefix + key_stub
else:
key = f"{prefix}.{key_stub}"
match value:
case dict():
_update_disallow_duplicates(out, _flatten_inner(value, key))
case str():
_update_disallow_duplicates(out, {key: value})
case _:
raise TypeError(value)
return out
def _update_disallow_duplicates(base: dict[str, _T_cov], new: dict[str, _T_cov]):
for key, value in new.items():
if key in base:
raise ValueError(f"Duplicate key {key}\nold=`{base[key]}`\nnew=`{value}`")
base[key] = value

View file

@ -0,0 +1,6 @@
__all__ = [
"cast_or_raise",
"isinstance_or_raise",
]
from .assertions import cast_or_raise, isinstance_or_raise

View file

@ -0,0 +1,55 @@
import logging
from typing import Any, TypeGuard, TypeVar, get_origin
_T = TypeVar("_T")
_DEFAULT_MESSAGE_SHORT = "Expected any of {expected}, got {actual}"
_DEFAULT_MESSAGE_LONG = "Expected any of {expected}, got {actual}: {value}"
def isinstance_or_raise(
val: Any,
class_or_tuple: type[_T] | tuple[type[_T], ...],
message: str | None = None,
) -> TypeGuard[_T]:
"""Usage: `assert isinstance_or_raise(val, str)`
message placeholders: `{expected}`, `{actual}`, `{value}`
"""
# convert generic types into the origin type
if not isinstance(class_or_tuple, tuple):
class_or_tuple = (class_or_tuple,)
ungenericed_classes = tuple(get_origin(t) or t for t in class_or_tuple)
if not isinstance(val, ungenericed_classes):
# just in case the caller messed up the message formatting
subs = {
"expected": list(class_or_tuple),
"actual": type(val),
"value": val,
}
if logging.getLogger(__name__).getEffectiveLevel() >= logging.WARNING:
default_message = _DEFAULT_MESSAGE_SHORT
else:
default_message = _DEFAULT_MESSAGE_LONG
if message is None:
raise TypeError(default_message.format(**subs))
try:
raise TypeError(message.format(**subs))
except KeyError:
raise TypeError(default_message.format(**subs))
return True
def cast_or_raise(
val: Any,
class_or_tuple: type[_T] | tuple[type[_T], ...],
message: str | None = None,
) -> _T:
assert isinstance_or_raise(val, class_or_tuple, message)
return val

View file

@ -0,0 +1,64 @@
import re
from typing import TypeVar
import pyjson5
from .assertions import isinstance_or_raise
_T_co = TypeVar("_T_co", covariant=True)
JSONDict = dict[str, "JSONValue"]
JSONValue = JSONDict | list["JSONValue"] | str | int | float | bool | None
def decode_json_dict(data: str | bytes) -> JSONDict:
match data:
case str():
decoded = pyjson5.decode(data)
case _:
decoded = pyjson5.decode_utf8(data)
assert isinstance_or_raise(decoded, dict)
return decoded
# implement pkpcpbp's flattening in python
# https://github.com/gamma-delta/PKPCPBP/blob/786194a590f/src/main/java/at/petrak/pkpcpbp/filters/JsonUtil.java
def decode_and_flatten_json_dict(data: str) -> dict[str, str]:
# replace `\<LF> foobar` with `\<LF>foobar`
data = re.sub(r"\\\n\s*", "\\\n", data)
# decode and flatten
decoded = decode_json_dict(data)
return _flatten_inner(decoded, "")
def _flatten_inner(obj: JSONDict, prefix: str) -> dict[str, str]:
out: dict[str, str] = {}
for key_stub, value in obj.items():
if not prefix:
key = key_stub
elif not key_stub:
key = prefix
elif prefix[-1] in ":_-/":
key = prefix + key_stub
else:
key = f"{prefix}.{key_stub}"
match value:
case dict():
_update_disallow_duplicates(out, _flatten_inner(value, key))
case str():
_update_disallow_duplicates(out, {key: value})
case _:
raise TypeError(value)
return out
def _update_disallow_duplicates(base: dict[str, _T_co], new: dict[str, _T_co]):
for key, value in new.items():
if key in base:
raise ValueError(f"Duplicate key {key}\nold=`{base[key]}`\nnew=`{value}`")
base[key] = value

View file

@ -4,7 +4,7 @@ import tomllib
from pathlib import Path
from typing import Callable, TypeVar
from .deserialize import cast_or_raise
from .assertions import cast_or_raise
# TODO: there's (figuratively) literally no comments in this file

View file

View file

@ -0,0 +1,20 @@
from jinja2 import nodes
from jinja2.ext import Extension
from jinja2.parser import Parser
from markupsafe import Markup
# https://stackoverflow.com/a/64392515
class IncludeRawExtension(Extension):
tags = {"include_raw"}
def parse(self, parser: Parser) -> nodes.Node:
lineno = parser.stream.expect("name:include_raw").lineno
template = parser.parse_expression()
result = self.call_method("_render", [template], lineno=lineno)
return nodes.Output([result], lineno=lineno)
def _render(self, filename: str) -> Markup:
assert self.environment.loader is not None
source = self.environment.loader.get_source(self.environment, filename)
return Markup(source[0])

View file

@ -1,38 +1,17 @@
from typing import Any
from jinja2 import nodes, pass_context
from jinja2.ext import Extension
from jinja2.parser import Parser
from jinja2 import pass_context
from jinja2.runtime import Context
from markupsafe import Markup
from pydantic import ConfigDict, validate_call
from hexdoc.core.properties import Properties
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft import I18n, LocalizedStr
from hexdoc.minecraft.assets.textures import Texture
from hexdoc.minecraft.assets import Texture
from hexdoc.patchouli import Book, FormatTree
from hexdoc.patchouli.book import Book
from hexdoc.patchouli.text import HTMLStream
from hexdoc.patchouli.text.formatting import BookLinkBases, FormatTree
from hexdoc.utils.resource import ResourceLocation
from . import Properties
from .deserialize import cast_or_raise
# https://stackoverflow.com/a/64392515
class IncludeRawExtension(Extension):
tags = {"include_raw"}
def parse(self, parser: Parser) -> nodes.Node:
lineno = parser.stream.expect("name:include_raw").lineno
template = parser.parse_expression()
result = self.call_method("_render", [template], lineno=lineno)
return nodes.Output([result], lineno=lineno)
def _render(self, filename: str) -> Markup:
assert self.environment.loader is not None
source = self.environment.loader.get_source(self.environment, filename)
return Markup(source[0])
from hexdoc.patchouli.text.formatting import BookLinkBases, HTMLStream
from hexdoc.utils.deserialize import cast_or_raise
@pass_context

View file

@ -6,7 +6,7 @@ from typing import Any, Mapping, Protocol, TypeVar
from pydantic import field_validator, model_validator
from pydantic.dataclasses import dataclass
from .model import DEFAULT_CONFIG
from hexdoc.model import DEFAULT_CONFIG
_T = TypeVar("_T")

View file

@ -1,6 +1,6 @@
import pytest
from hexdoc.utils import ItemStack, ResLoc, ResourceLocation
from hexdoc.core.resource import ItemStack, ResLoc, ResourceLocation
resource_locations: list[tuple[str, ResourceLocation, str]] = [
(

View file

@ -2,6 +2,7 @@
from argparse import Namespace
from typing import cast
from hexdoc.core.resource import ResourceLocation
from hexdoc.minecraft.i18n import I18n
from hexdoc.patchouli.text import DEFAULT_MACROS, FormatTree
from hexdoc.patchouli.text.formatting import (
@ -12,8 +13,7 @@ from hexdoc.patchouli.text.formatting import (
ParagraphStyle,
SpecialStyleType,
)
from hexdoc.utils.jinja_extensions import hexdoc_block
from hexdoc.utils.resource import ResourceLocation
from hexdoc.utils.jinja.macros import hexdoc_block
def format_with_mocks(test_str: str, macros: dict[str, str] = {}):