diff --git a/doc/src/hexdoc/cli/main.py b/doc/src/hexdoc/cli/main.py index bfd68966..78d77671 100644 --- a/doc/src/hexdoc/cli/main.py +++ b/doc/src/hexdoc/cli/main.py @@ -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 diff --git a/doc/src/hexdoc/cli/utils/load.py b/doc/src/hexdoc/cli/utils/load.py index 31eb87ed..ca2c3517 100644 --- a/doc/src/hexdoc/cli/utils/load.py +++ b/doc/src/hexdoc/cli/utils/load.py @@ -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 diff --git a/doc/src/hexdoc/cli/utils/render.py b/doc/src/hexdoc/cli/utils/render.py index 3a321243..6b852ce6 100644 --- a/doc/src/hexdoc/cli/utils/render.py +++ b/doc/src/hexdoc/cli/utils/render.py @@ -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 diff --git a/doc/src/hexdoc/cli/utils/sitemap.py b/doc/src/hexdoc/cli/utils/sitemap.py index d951596c..cf5bf05c 100644 --- a/doc/src/hexdoc/cli/utils/sitemap.py +++ b/doc/src/hexdoc/cli/utils/sitemap.py @@ -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" diff --git a/doc/src/hexdoc/core/__init__.py b/doc/src/hexdoc/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/src/hexdoc/utils/compat.py b/doc/src/hexdoc/core/compat.py similarity index 100% rename from doc/src/hexdoc/utils/compat.py rename to doc/src/hexdoc/core/compat.py diff --git a/doc/src/hexdoc/utils/resource_loader.py b/doc/src/hexdoc/core/loader.py similarity index 97% rename from doc/src/hexdoc/utils/resource_loader.py rename to doc/src/hexdoc/core/loader.py index 6f7c6d89..0a910196 100644 --- a/doc/src/hexdoc/utils/resource_loader.py +++ b/doc/src/hexdoc/core/loader.py @@ -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 diff --git a/doc/src/hexdoc/utils/metadata.py b/doc/src/hexdoc/core/metadata.py similarity index 90% rename from doc/src/hexdoc/utils/metadata.py rename to doc/src/hexdoc/core/metadata.py index 2a7de0a9..1c1f8cad 100644 --- a/doc/src/hexdoc/utils/metadata.py +++ b/doc/src/hexdoc/core/metadata.py @@ -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 diff --git a/doc/src/hexdoc/utils/properties.py b/doc/src/hexdoc/core/properties.py similarity index 82% rename from doc/src/hexdoc/utils/properties.py rename to doc/src/hexdoc/core/properties.py index cd0853b2..c262c654 100644 --- a/doc/src/hexdoc/utils/properties.py +++ b/doc/src/hexdoc/core/properties.py @@ -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") diff --git a/doc/src/hexdoc/utils/resource.py b/doc/src/hexdoc/core/resource.py similarity index 99% rename from doc/src/hexdoc/utils/resource.py rename to doc/src/hexdoc/core/resource.py index 20fbea46..d0adc526 100644 --- a/doc/src/hexdoc/utils/resource.py +++ b/doc/src/hexdoc/core/resource.py @@ -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", ""] diff --git a/doc/src/hexdoc/hexcasting/hex_book.py b/doc/src/hexdoc/hexcasting/hex_book.py index 0a5414fc..cf6ab4e9 100644 --- a/doc/src/hexdoc/hexcasting/hex_book.py +++ b/doc/src/hexdoc/hexcasting/hex_book.py @@ -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 diff --git a/doc/src/hexdoc/hexcasting/hex_recipes.py b/doc/src/hexdoc/hexcasting/hex_recipes.py index 2ee3845f..69e4368b 100644 --- a/doc/src/hexdoc/hexcasting/hex_recipes.py +++ b/doc/src/hexdoc/hexcasting/hex_recipes.py @@ -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 diff --git a/doc/src/hexdoc/hexcasting/page/abstract_hex_pages.py b/doc/src/hexdoc/hexcasting/page/abstract_hex_pages.py index 1d910c0f..8ae481ce 100644 --- a/doc/src/hexdoc/hexcasting/page/abstract_hex_pages.py +++ b/doc/src/hexdoc/hexcasting/page/abstract_hex_pages.py @@ -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 diff --git a/doc/src/hexdoc/hexcasting/page/hex_pages.py b/doc/src/hexdoc/hexcasting/page/hex_pages.py index a24144ed..80aa0f85 100644 --- a/doc/src/hexdoc/hexcasting/page/hex_pages.py +++ b/doc/src/hexdoc/hexcasting/page/hex_pages.py @@ -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 diff --git a/doc/src/hexdoc/hexcasting/pattern.py b/doc/src/hexdoc/hexcasting/pattern.py index 67e915af..cbb66cc7 100644 --- a/doc/src/hexdoc/hexcasting/pattern.py +++ b/doc/src/hexdoc/hexcasting/pattern.py @@ -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): diff --git a/doc/src/hexdoc/minecraft/assets/__init__.py b/doc/src/hexdoc/minecraft/assets/__init__.py index 30eb26c6..9b1722c2 100644 --- a/doc/src/hexdoc/minecraft/assets/__init__.py +++ b/doc/src/hexdoc/minecraft/assets/__init__.py @@ -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, +) diff --git a/doc/src/hexdoc/utils/external.py b/doc/src/hexdoc/minecraft/assets/external.py similarity index 100% rename from doc/src/hexdoc/utils/external.py rename to doc/src/hexdoc/minecraft/assets/external.py diff --git a/doc/src/hexdoc/minecraft/assets/models.py b/doc/src/hexdoc/minecraft/assets/models.py index 791a8a11..539e526e 100644 --- a/doc/src/hexdoc/minecraft/assets/models.py +++ b/doc/src/hexdoc/minecraft/assets/models.py @@ -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 diff --git a/doc/src/hexdoc/minecraft/assets/textures.py b/doc/src/hexdoc/minecraft/assets/textures.py index 48b3072e..76703ddf 100644 --- a/doc/src/hexdoc/minecraft/assets/textures.py +++ b/doc/src/hexdoc/minecraft/assets/textures.py @@ -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=" diff --git a/doc/src/hexdoc/minecraft/i18n.py b/doc/src/hexdoc/minecraft/i18n.py index 5df07b38..9f6b0502 100644 --- a/doc/src/hexdoc/minecraft/i18n.py +++ b/doc/src/hexdoc/minecraft/i18n.py @@ -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 diff --git a/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py b/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py index e8f231cd..621b3a94 100644 --- a/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py +++ b/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py @@ -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 diff --git a/doc/src/hexdoc/minecraft/recipe/ingredients.py b/doc/src/hexdoc/minecraft/recipe/ingredients.py index d5cc7806..e83cd75e 100644 --- a/doc/src/hexdoc/minecraft/recipe/ingredients.py +++ b/doc/src/hexdoc/minecraft/recipe/ingredients.py @@ -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): diff --git a/doc/src/hexdoc/minecraft/recipe/recipes.py b/doc/src/hexdoc/minecraft/recipe/recipes.py index fe48f1cd..49040ce9 100644 --- a/doc/src/hexdoc/minecraft/recipe/recipes.py +++ b/doc/src/hexdoc/minecraft/recipe/recipes.py @@ -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 diff --git a/doc/src/hexdoc/minecraft/tags.py b/doc/src/hexdoc/minecraft/tags.py index 8ce940e2..5a96c914 100644 --- a/doc/src/hexdoc/minecraft/tags.py +++ b/doc/src/hexdoc/minecraft/tags.py @@ -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): diff --git a/doc/src/hexdoc/model/__init__.py b/doc/src/hexdoc/model/__init__.py new file mode 100644 index 00000000..ab544d46 --- /dev/null +++ b/doc/src/hexdoc/model/__init__.py @@ -0,0 +1,8 @@ +__all__ = [ + "HexdocModel", + "ValidationContext", + "DEFAULT_CONFIG", + "init_context", +] + +from .base import DEFAULT_CONFIG, HexdocModel, ValidationContext, init_context diff --git a/doc/src/hexdoc/utils/model.py b/doc/src/hexdoc/model/base.py similarity index 82% rename from doc/src/hexdoc/utils/model.py rename to doc/src/hexdoc/model/base.py index ca3ce86d..0c062ea1 100644 --- a/doc/src/hexdoc/utils/model.py +++ b/doc/src/hexdoc/model/base.py @@ -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("_")) - } diff --git a/doc/src/hexdoc/utils/resource_model.py b/doc/src/hexdoc/model/inline.py similarity index 91% rename from doc/src/hexdoc/utils/resource_model.py rename to doc/src/hexdoc/model/inline.py index 01019105..674905d3 100644 --- a/doc/src/hexdoc/utils/resource_model.py +++ b/doc/src/hexdoc/model/inline.py @@ -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() diff --git a/doc/src/hexdoc/utils/tagged_union.py b/doc/src/hexdoc/model/tagged_union.py similarity index 98% rename from doc/src/hexdoc/utils/tagged_union.py rename to doc/src/hexdoc/model/tagged_union.py index 3912079b..b3361e78 100644 --- a/doc/src/hexdoc/utils/tagged_union.py +++ b/doc/src/hexdoc/model/tagged_union.py @@ -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): diff --git a/doc/src/hexdoc/patchouli/book.py b/doc/src/hexdoc/patchouli/book.py index 9b9b8a14..960006a8 100644 --- a/doc/src/hexdoc/patchouli/book.py +++ b/doc/src/hexdoc/patchouli/book.py @@ -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 diff --git a/doc/src/hexdoc/patchouli/book_context.py b/doc/src/hexdoc/patchouli/book_context.py index 14e15963..21eef522 100644 --- a/doc/src/hexdoc/patchouli/book_context.py +++ b/doc/src/hexdoc/patchouli/book_context.py @@ -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 diff --git a/doc/src/hexdoc/patchouli/category.py b/doc/src/hexdoc/patchouli/category.py index efc65eed..bd2860a1 100644 --- a/doc/src/hexdoc/patchouli/category.py +++ b/doc/src/hexdoc/patchouli/category.py @@ -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 diff --git a/doc/src/hexdoc/patchouli/entry.py b/doc/src/hexdoc/patchouli/entry.py index e7fd9913..ed1cf07d 100644 --- a/doc/src/hexdoc/patchouli/entry.py +++ b/doc/src/hexdoc/patchouli/entry.py @@ -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 diff --git a/doc/src/hexdoc/patchouli/page/abstract_pages.py b/doc/src/hexdoc/patchouli/page/abstract_pages.py index ce9f1f91..c6cadb35 100644 --- a/doc/src/hexdoc/patchouli/page/abstract_pages.py +++ b/doc/src/hexdoc/patchouli/page/abstract_pages.py @@ -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 diff --git a/doc/src/hexdoc/patchouli/page/pages.py b/doc/src/hexdoc/patchouli/page/pages.py index 80dcdb6b..6999c381 100644 --- a/doc/src/hexdoc/patchouli/page/pages.py +++ b/doc/src/hexdoc/patchouli/page/pages.py @@ -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 diff --git a/doc/src/hexdoc/patchouli/text/__init__.py b/doc/src/hexdoc/patchouli/text/__init__.py index f319662d..3ab193a6 100644 --- a/doc/src/hexdoc/patchouli/text/__init__.py +++ b/doc/src/hexdoc/patchouli/text/__init__.py @@ -1,9 +1,6 @@ __all__ = [ "FormatTree", - "HTMLElement", - "HTMLStream", "DEFAULT_MACROS", ] from .formatting import DEFAULT_MACROS, FormatTree -from .html import HTMLElement, HTMLStream diff --git a/doc/src/hexdoc/patchouli/text/formatting.py b/doc/src/hexdoc/patchouli/text/formatting.py index 80159075..ab400f22 100644 --- a/doc/src/hexdoc/patchouli/text/formatting.py +++ b/doc/src/hexdoc/patchouli/text/formatting.py @@ -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 diff --git a/doc/src/hexdoc/plugin/manager.py b/doc/src/hexdoc/plugin/manager.py index a638cc07..126e754a 100644 --- a/doc/src/hexdoc/plugin/manager.py +++ b/doc/src/hexdoc/plugin/manager.py @@ -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 diff --git a/doc/src/hexdoc/utils/__init__.py b/doc/src/hexdoc/utils/__init__.py index 4a4095b0..e69de29b 100644 --- a/doc/src/hexdoc/utils/__init__.py +++ b/doc/src/hexdoc/utils/__init__.py @@ -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 diff --git a/doc/src/hexdoc/utils/cd.py b/doc/src/hexdoc/utils/cd.py index 9667a273..e66134eb 100644 --- a/doc/src/hexdoc/utils/cd.py +++ b/doc/src/hexdoc/utils/cd.py @@ -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") diff --git a/doc/src/hexdoc/utils/deserialize.py b/doc/src/hexdoc/utils/deserialize.py deleted file mode 100644 index 1d8bbc61..00000000 --- a/doc/src/hexdoc/utils/deserialize.py +++ /dev/null @@ -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 `\ foobar` with `\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 diff --git a/doc/src/hexdoc/utils/deserialize/__init__.py b/doc/src/hexdoc/utils/deserialize/__init__.py new file mode 100644 index 00000000..eda5b463 --- /dev/null +++ b/doc/src/hexdoc/utils/deserialize/__init__.py @@ -0,0 +1,6 @@ +__all__ = [ + "cast_or_raise", + "isinstance_or_raise", +] + +from .assertions import cast_or_raise, isinstance_or_raise diff --git a/doc/src/hexdoc/utils/deserialize/assertions.py b/doc/src/hexdoc/utils/deserialize/assertions.py new file mode 100644 index 00000000..388baa9a --- /dev/null +++ b/doc/src/hexdoc/utils/deserialize/assertions.py @@ -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 diff --git a/doc/src/hexdoc/utils/deserialize/json.py b/doc/src/hexdoc/utils/deserialize/json.py new file mode 100644 index 00000000..9bea39e5 --- /dev/null +++ b/doc/src/hexdoc/utils/deserialize/json.py @@ -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 `\ foobar` with `\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 diff --git a/doc/src/hexdoc/utils/toml_placeholders.py b/doc/src/hexdoc/utils/deserialize/toml.py similarity index 98% rename from doc/src/hexdoc/utils/toml_placeholders.py rename to doc/src/hexdoc/utils/deserialize/toml.py index 3003bad0..9fda2c0e 100644 --- a/doc/src/hexdoc/utils/toml_placeholders.py +++ b/doc/src/hexdoc/utils/deserialize/toml.py @@ -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 diff --git a/doc/src/hexdoc/utils/jinja/__init__.py b/doc/src/hexdoc/utils/jinja/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/src/hexdoc/utils/jinja/extensions.py b/doc/src/hexdoc/utils/jinja/extensions.py new file mode 100644 index 00000000..d9fd3626 --- /dev/null +++ b/doc/src/hexdoc/utils/jinja/extensions.py @@ -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]) diff --git a/doc/src/hexdoc/utils/jinja_extensions.py b/doc/src/hexdoc/utils/jinja/macros.py similarity index 72% rename from doc/src/hexdoc/utils/jinja_extensions.py rename to doc/src/hexdoc/utils/jinja/macros.py index 4a05d2d4..5b5f5af3 100644 --- a/doc/src/hexdoc/utils/jinja_extensions.py +++ b/doc/src/hexdoc/utils/jinja/macros.py @@ -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 diff --git a/doc/src/hexdoc/utils/types.py b/doc/src/hexdoc/utils/types.py index 8725a39a..11f3b757 100644 --- a/doc/src/hexdoc/utils/types.py +++ b/doc/src/hexdoc/utils/types.py @@ -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") diff --git a/doc/test/minecraft/test_resource.py b/doc/test/core/test_resource.py similarity index 96% rename from doc/test/minecraft/test_resource.py rename to doc/test/core/test_resource.py index 563ea0b1..f0010672 100644 --- a/doc/test/minecraft/test_resource.py +++ b/doc/test/core/test_resource.py @@ -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]] = [ ( diff --git a/doc/test/patchouli/text/test_formatting.py b/doc/test/patchouli/text/test_formatting.py index 0f72d8d6..8e81bb33 100644 --- a/doc/test/patchouli/text/test_formatting.py +++ b/doc/test/patchouli/text/test_formatting.py @@ -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] = {}):