The great refactoring (move to hexdoc package and add reexports)

This commit is contained in:
object-Object 2023-07-24 20:40:28 -04:00
parent 2960568e91
commit 03e7683ae1
53 changed files with 475 additions and 404 deletions

36
.vscode/settings.json vendored
View file

@ -1,36 +0,0 @@
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
"editor.rulers": [88],
},
"[html][jinja-html]": {
"editor.rulers": [120],
},
"python.formatting.provider": "black",
"python.analysis.typeCheckingMode": "strict", // god save us
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingParameterType": "error",
"reportUnknownParameterType": "error",
"reportUnknownArgumentType": "warning",
"reportUnknownLambdaType": "warning",
"reportUnknownVariableType": "none",
"reportUnknownMemberType": "warning",
"reportUnnecessaryComparison": "warning",
"reportMissingTypeArgument": "warning",
"reportUnusedImport": "information",
"reportPrivateUsage": "warning",
"reportUnnecessaryIsInstance": "information",
},
"python.analysis.diagnosticMode": "workspace",
"python.languageServer": "Pylance",
"python.testing.cwd": "${workspaceFolder}/doc",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.analysis.extraPaths": ["${workspaceFolder}/doc/src", "${workspaceFolder}/doc/test"],
"isort.args": [
"--settings", "doc/pyproject.toml"
]
}

11
doc/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"ms-python.vscode-pylance",
"ms-python.isort",
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
"ms-python.black-formatter",
]
}

View file

@ -17,7 +17,7 @@
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"cwd": "${workspaceFolder}/doc", "cwd": "${workspaceFolder}/doc",
"module": "hexcasting.scripts.main", "module": "hexdoc",
"args": [ "args": [
"properties.toml", "properties.toml",
], ],

18
doc/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
"editor.rulers": [88],
},
"python.formatting.provider": "black",
"isort.importStrategy": "fromEnvironment",
"python.languageServer": "Pylance",
"python.analysis.diagnosticMode": "workspace",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"[html][jinja-html]": {
"editor.rulers": [120],
},
}

View file

@ -51,12 +51,8 @@ package = "{src}/main/java/at/petrak/hexcasting"
resources = "{src}/main/resources" resources = "{src}/main/resources"
generated = "{src}/generated/resources" generated = "{src}/generated/resources"
pattern_stubs = [ pattern_stubs = [
# these are tables so we have the option to add extra per-stub configs (eg. regex) "{package}/common/casting/RegisterPatterns.java",
# NOTE: each ^ is like ../ in a file path (^key and ^.key are both valid) "{package}/interop/pehkui/PehkuiInterop.java",
# the parent of an item in an array is the table containing the array, not the array
# so in this case, {^.package} is common.package
{ file = "{^.package}/common/casting/RegisterPatterns.java" },
{ file = "{^.package}/interop/pehkui/PehkuiInterop.java" },
] ]
[fabric] [fabric]
@ -65,7 +61,7 @@ package = "{src}/main/java/at/petrak/hexcasting/fabric"
resources = "{src}/main/resources" resources = "{src}/main/resources"
generated = "{src}/generated/resources" generated = "{src}/generated/resources"
pattern_stubs = [ pattern_stubs = [
{ file = "{^.package}/interop/gravity/GravityApiInterop.java" }, "{package}/interop/gravity/GravityApiInterop.java",
] ]
[forge] [forge]

View file

@ -1,11 +1,10 @@
# project metadata
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "HexDoc" # TODO: i'm pretty sure i had funnier ideas than this name = "HexDoc"
version = "0.1.0" version = "0.1.0"
authors = [ authors = [
{ name="Alwinfy" }, { name="Alwinfy" },
@ -16,45 +15,108 @@ requires-python = ">=3.11"
dependencies = [ dependencies = [
"typing_extensions~=4.7.0", "typing_extensions~=4.7.0",
"typed-argument-parser~=1.8.0", "typed-argument-parser~=1.8.0",
"pydantic==2.0", "pydantic~=2.0",
"Jinja2~=3.1.2",
] ]
[project.optional-dependencies]
dev = [
"black==23.7.0",
"isort==5.12.0",
"pytest==7.3.1",
"syrupy==4.0.2",
]
[project.scripts]
hexdoc = "hexdoc.scripts.hexdoc:main"
[project.entry-points."hexdoc.Page"] [project.entry-points."hexdoc.Page"]
hexdoc-patchouli = "patchouli.page.pages" hexdoc-patchouli = "hexdoc.patchouli.page.pages"
hexdoc-hexcasting = "hexcasting.hex_pages" hexdoc-hexcasting = "hexdoc.hexcasting.page.hex_pages"
hexdoc-abstract-hexcasting = "hexcasting.abstract_hex_pages"
[project.entry-points."hexdoc.Recipe"] [project.entry-points."hexdoc.Recipe"]
hexdoc-minecraft = "minecraft.recipe.recipes" hexdoc-minecraft = "hexdoc.minecraft.recipe.recipes"
hexdoc-hexcasting = "hexcasting.hex_recipes" hexdoc-hexcasting = "hexdoc.hexcasting.hex_recipes"
[project.entry-points."hexdoc.ItemIngredient"] [project.entry-points."hexdoc.ItemIngredient"]
hexdoc-minecraft = "minecraft.recipe.ingredients" hexdoc-minecraft = "hexdoc.minecraft.recipe.ingredients"
hexdoc-hexcasting = "hexcasting.hex_recipes" hexdoc-hexcasting = "hexdoc.hexcasting.hex_recipes"
# Hatch settings (the build backend)
[tool.hatch.metadata]
allow-direct-references = true # TODO: remove when we switch to Pydantic
[tool.hatch.build] [tool.hatch.build]
packages = ["src/common", "src/hexcasting", "src/minecraft", "src/patchouli"] packages = ["src/hexdoc"]
# tests, formatting, and (TODO:) type checking
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = [ addopts = ["--import-mode=importlib"]
"--import-mode=importlib"
]
markers = [ markers = [
"filename: name of file for fixtures to create", "filename: name of file for fixtures to create",
"file_contents: data for fixtures to write to files", "file_contents: data for fixtures to write to files",
"fixture_data: other misc data", "fixture_data: other misc data",
] ]
[tool.coverage.report] [tool.coverage.report]
include_namespace_packages = true include_namespace_packages = true
[tool.isort] [tool.isort]
profile = "black" profile = "black"
combine_as_imports = true combine_as_imports = true
[tool.pyright]
pythonVersion = "3.11"
pythonPlatform = "All"
# mostly we use strict mode
# but pyright doesn't allow decreasing error severity in strict mode
# so we need to manually specify all of the strict mode overrides so we can do that :/
typeCheckingMode = "basic"
strictDictionaryInference = true
strictListInference = true
strictSetInference = true
reportAssertAlwaysTrue = "error"
reportConstantRedefinition = "error"
reportDeprecated = "error"
reportDuplicateImport = "error"
reportFunctionMemberAccess = "error"
reportIncompatibleMethodOverride = "error"
reportIncompatibleVariableOverride = "error"
reportIncompleteStub = "error"
reportInconsistentConstructor = "error"
reportInvalidStringEscapeSequence = "error"
reportInvalidStubStatement = "error"
reportInvalidTypeVarUse = "error"
reportMatchNotExhaustive = "error"
reportMissingParameterType = "error"
reportMissingTypeStubs = "error"
reportOverlappingOverload = "error"
reportSelfClsParameterName = "error"
reportTypeCommentUsage = "error"
reportUnknownParameterType = "error"
reportUnnecessaryCast = "error"
reportUnnecessaryContains = "error"
reportUnsupportedDunderAll = "error"
reportUntypedBaseClass = "error"
reportUntypedClassDecorator = "error"
reportUntypedFunctionDecorator = "error"
reportUntypedNamedTuple = "error"
reportUnusedClass = "error"
reportUnusedExpression = "error"
reportUnusedFunction = "error"
reportUnusedVariable = "error"
reportWildcardImportFromLibrary = "error"
reportMissingTypeArgument = "warning"
reportPrivateUsage = "warning"
reportUnknownArgumentType = "warning"
reportUnknownLambdaType = "warning"
reportUnknownMemberType = "warning"
reportUnnecessaryComparison = "warning"
reportUnnecessaryIsInstance = "warning"
reportUnusedImport = "warning"
reportUnknownVariableType = "none"

View file

@ -1,6 +0,0 @@
-e . # install package locally as editable
black==22.10.0 # formatting
isort==5.12.0 # formatting 2
pytest==7.3.1 # testing framework
syrupy==4.0.2 # snapshot tests
beautifulsoup4==4.12.2 # html pretty print so the snapshot diffs are actually usable

View file

@ -1,9 +0,0 @@
__all__ = [
"HexBook",
"HexContext",
"AnyHexContext",
"HexBookModel",
]
from .hex_book import AnyHexContext, HexBook, HexBookModel, HexContext

View file

@ -1,68 +0,0 @@
import re
from enum import Enum
from pathlib import Path
from typing import Annotated, Any, Generator
from pydantic import BeforeValidator
from common.model import HexDocModel
from minecraft.resource import ResourceLocation
class Direction(Enum):
NORTH_EAST = 0
EAST = 1
SOUTH_EAST = 2
SOUTH_WEST = 3
WEST = 4
NORTH_WEST = 5
@classmethod
def validate(cls, value: str | int | Any):
match value:
case str():
return cls[value]
case int():
return cls(value)
case _:
return value
DirectionField = Annotated[Direction, BeforeValidator(Direction.validate)]
class RawPatternInfo(HexDocModel[Any]):
startdir: DirectionField
signature: str
is_per_world: bool = False
q: int | None = None
r: int | None = None
class PatternInfo(RawPatternInfo):
id: ResourceLocation
@property
def name(self):
return self.id.path
class PatternStubFile(HexDocModel[Any]):
file: Path
def load_patterns(
self,
modid: str,
pattern_re: re.Pattern[str],
) -> Generator[PatternInfo, None, None]:
# TODO: add Gradle task to generate json with this data. this is dumb and fragile.
pattern_data = self.file.read_text("utf-8")
for match in pattern_re.finditer(pattern_data):
signature, startdir, name, is_per_world = match.groups()
yield PatternInfo(
startdir=Direction[startdir],
signature=signature,
is_per_world=bool(is_per_world),
id=ResourceLocation(modid, name),
)

View file

@ -0,0 +1,13 @@
__all__ = [
"AnyHexContext",
"HexBook",
"HexBookType",
"HexContext",
"Direction",
"PatternInfo",
"RawPatternInfo",
]
from .hex_book import AnyHexContext, HexBook, HexBookType, HexContext
from .pattern import Direction, PatternInfo, RawPatternInfo

View file

@ -1,11 +1,12 @@
from pathlib import Path
from typing import Any, Generic, TypeVar from typing import Any, Generic, TypeVar
from common.model import AnyContext from hexdoc.patchouli import AnyBookContext, Book, BookContext
from common.properties import Properties from hexdoc.properties import Properties
from hexcasting.pattern import PatternInfo from hexdoc.resource import ResourceLocation
from minecraft.resource import ResourceLocation from hexdoc.utils import AnyContext
from patchouli.book import Book
from patchouli.context import AnyBookContext, BookContext from .pattern import Direction, PatternInfo
class HexContext(BookContext): class HexContext(BookContext):
@ -15,7 +16,7 @@ class HexContext(BookContext):
AnyHexContext = TypeVar("AnyHexContext", bound=HexContext) AnyHexContext = TypeVar("AnyHexContext", bound=HexContext)
class HexBookModel( class HexBookType(
Generic[AnyContext, AnyBookContext, AnyHexContext], Generic[AnyContext, AnyBookContext, AnyHexContext],
Book[AnyHexContext, AnyHexContext], Book[AnyHexContext, AnyHexContext],
): ):
@ -28,7 +29,7 @@ class HexBookModel(
signatures = dict[str, PatternInfo]() # just for duplicate checking signatures = dict[str, PatternInfo]() # just for duplicate checking
for stub in props.pattern_stubs: for stub in props.pattern_stubs:
# for each stub, load all the patterns in the file # for each stub, load all the patterns in the file
for pattern in stub.load_patterns(props.modid, props.pattern_regex): for pattern in cls.load_patterns(stub, props):
# check for duplicates, because why not # check for duplicates, because why not
if duplicate := ( if duplicate := (
patterns.get(pattern.id) or signatures.get(pattern.signature) patterns.get(pattern.id) or signatures.get(pattern.signature)
@ -45,5 +46,19 @@ class HexBookModel(
"patterns": patterns, "patterns": patterns,
} }
@classmethod
def load_patterns(cls, path: Path, props: Properties):
# TODO: add Gradle task to generate json with this data. this is dumb and fragile.
stub_text = path.read_text("utf-8")
for match in props.pattern_regex.finditer(stub_text):
signature, startdir, name, is_per_world = match.groups()
yield PatternInfo(
startdir=Direction[startdir],
signature=signature,
is_per_world=bool(is_per_world),
id=ResourceLocation(props.modid, name),
)
HexBook = HexBookModel[HexContext, HexContext, HexContext]
# type alias for convenience
HexBook = HexBookType[HexContext, HexContext, HexContext]

View file

@ -1,15 +1,15 @@
from typing import Any, Literal from typing import Any, Literal
from common.model import HexDocModel from hexdoc.minecraft import LocalizedItem, Recipe
from hexcasting.hex_book import HexContext from hexdoc.minecraft.recipe import (
from minecraft.i18n import LocalizedItem
from minecraft.recipe import (
ItemIngredient, ItemIngredient,
MinecraftItemIdIngredient, MinecraftItemIdIngredient,
MinecraftItemTagIngredient, MinecraftItemTagIngredient,
Recipe,
) )
from minecraft.resource import ResourceLocation from hexdoc.resource import ResourceLocation
from hexdoc.utils import HexDocModel
from .hex_book import HexContext
# ingredients # ingredients

View file

@ -0,0 +1,20 @@
__all__ = [
"PageWithOpPattern",
"PageWithPattern",
"BrainsweepPage",
"CraftingMultiPage",
"LookupPatternPage",
"ManualOpPatternPage",
"ManualPatternNosigPage",
"ManualRawPatternPage",
]
from .abstract_hex_pages import PageWithOpPattern, PageWithPattern
from .hex_pages import (
BrainsweepPage,
CraftingMultiPage,
LookupPatternPage,
ManualOpPatternPage,
ManualPatternNosigPage,
ManualRawPatternPage,
)

View file

@ -3,12 +3,12 @@ from typing import Any, cast
from pydantic import ValidationInfo, model_validator from pydantic import ValidationInfo, model_validator
from hexcasting.pattern import RawPatternInfo from hexdoc.minecraft import LocalizedStr
from minecraft.i18n import LocalizedStr from hexdoc.patchouli.page import PageWithText
from minecraft.resource import ResourceLocation from hexdoc.resource import ResourceLocation
from patchouli.page import PageWithText
from .hex_book import AnyHexContext, HexContext from ..hex_book import AnyHexContext, HexContext
from ..pattern import RawPatternInfo
# TODO: make anchor required (breaks because of Greater Sentinel) # TODO: make anchor required (breaks because of Greater Sentinel)

View file

@ -2,15 +2,14 @@ from typing import Any, cast
from pydantic import ValidationInfo, model_validator from pydantic import ValidationInfo, model_validator
from minecraft.i18n import LocalizedStr from hexdoc.minecraft import LocalizedStr
from minecraft.recipe import CraftingRecipe from hexdoc.minecraft.recipe import CraftingRecipe
from minecraft.resource import ResourceLocation from hexdoc.patchouli.page import PageWithText, PageWithTitle
from patchouli.page import PageWithText from hexdoc.resource import ResourceLocation
from patchouli.page.abstract_pages import PageWithTitle
from ..hex_book import HexContext
from ..hex_recipes import BrainsweepRecipe
from .abstract_hex_pages import PageWithOpPattern, PageWithPattern from .abstract_hex_pages import PageWithOpPattern, PageWithPattern
from .hex_book import HexContext
from .hex_recipes import BrainsweepRecipe
class LookupPatternPage( class LookupPatternPage(

View file

@ -0,0 +1,45 @@
from enum import Enum
from typing import Annotated, Any
from pydantic import BeforeValidator
from hexdoc.resource import ResourceLocation
from hexdoc.utils import HexDocModel
class Direction(Enum):
NORTH_EAST = 0
EAST = 1
SOUTH_EAST = 2
SOUTH_WEST = 3
WEST = 4
NORTH_WEST = 5
@classmethod
def validate(cls, value: str | int | Any):
match value:
case str():
return cls[value]
case int():
return cls(value)
case _:
return value
DirectionField = Annotated[Direction, BeforeValidator(Direction.validate)]
class RawPatternInfo(HexDocModel[Any]):
startdir: DirectionField
signature: str
is_per_world: bool = False
q: int | None = None
r: int | None = None
class PatternInfo(RawPatternInfo):
id: ResourceLocation
@property
def name(self):
return self.id.path

View file

@ -0,0 +1,9 @@
__all__ = [
"I18n",
"LocalizedItem",
"LocalizedStr",
"Recipe",
]
from .i18n import I18n, LocalizedItem, LocalizedStr
from .recipe import Recipe

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import InitVar
from functools import total_ordering from functools import total_ordering
from pathlib import Path
from typing import Any, Callable, Self, cast from typing import Any, Callable, Self, cast
from pydantic import ValidationInfo, model_validator from pydantic import ValidationInfo, model_validator
@ -9,10 +9,10 @@ from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler from pydantic.functional_validators import ModelWrapValidatorHandler
from typing_extensions import TypedDict from typing_extensions import TypedDict
from common.deserialize import isinstance_or_raise, load_json from hexdoc.properties import Properties
from common.model import DEFAULT_CONFIG, HexDocModel from hexdoc.resource import ItemStack, ResourceLocation
from common.properties import Properties from hexdoc.utils import DEFAULT_CONFIG, HexDocModel
from minecraft.resource import ItemStack, ResourceLocation from hexdoc.utils.deserialize import isinstance_or_raise, load_json
class I18nContext(TypedDict): class I18nContext(TypedDict):
@ -94,12 +94,12 @@ class LocalizedItem(LocalizedStr):
class I18n: class I18n:
"""Handles localization of strings.""" """Handles localization of strings."""
props: Properties props: InitVar[Properties]
enabled: bool enabled: bool
lookup: dict[str, LocalizedStr] | None = None lookup: dict[str, LocalizedStr] | None = None
def __post_init__(self): def __post_init__(self, props: Properties):
# skip loading the files if we don't need to # skip loading the files if we don't need to
self.lookup = None self.lookup = None
if not self.enabled: if not self.enabled:
@ -109,8 +109,9 @@ class I18n:
# TODO: load ALL of the i18n files, return dict[str, _Lookup] | None # TODO: load ALL of the i18n files, return dict[str, _Lookup] | None
# or maybe dict[(str, str), LocalizedStr] # or maybe dict[(str, str), LocalizedStr]
# we could also use that to ensure all i18n files have the same set of keys # we could also use that to ensure all i18n files have the same set of keys
path = self.dir / self.props.i18n.filename lang_dir = props.resources_dir / "assets" / props.modid / "lang"
raw_lookup = load_json(path) | (self.props.i18n.extra or {}) path = lang_dir / props.i18n.filename
raw_lookup = load_json(path) | (props.i18n.extra or {})
# validate and insert # validate and insert
self.lookup = {} self.lookup = {}
@ -121,11 +122,6 @@ class I18n:
value=raw_value.replace("%%", "%"), value=raw_value.replace("%%", "%"),
) )
@property
def dir(self) -> Path:
"""eg. `resources/assets/hexcasting/lang`"""
return self.props.resources_dir / "assets" / self.props.modid / "lang"
def localize( def localize(
self, self,
*keys: str, *keys: str,

View file

@ -1,19 +1,19 @@
__all__ = [ __all__ = [
"CraftingRecipe",
"ItemIngredient",
"MinecraftItemTagIngredient",
"MinecraftItemIdIngredient",
"ItemResult",
"Recipe", "Recipe",
"recipes", "ItemIngredient",
"ItemIngredientOrList",
"MinecraftItemIdIngredient",
"MinecraftItemTagIngredient",
"CraftingRecipe", "CraftingRecipe",
"CraftingShapedRecipe", "CraftingShapedRecipe",
"CraftingShapelessRecipe", "CraftingShapelessRecipe",
"ItemResult",
] ]
from .abstract_recipes import Recipe from .abstract_recipes import Recipe
from .ingredients import ( from .ingredients import (
ItemIngredient, ItemIngredient,
ItemIngredientOrList,
MinecraftItemIdIngredient, MinecraftItemIdIngredient,
MinecraftItemTagIngredient, MinecraftItemTagIngredient,
) )

View file

@ -2,13 +2,12 @@ from typing import Any, Self, cast
from pydantic import ValidationInfo, model_validator from pydantic import ValidationInfo, model_validator
from common.deserialize import load_json from hexdoc.properties import AnyPropsContext
from common.tagged_union import TypeTaggedUnion from hexdoc.resource import ResourceLocation, TypeTaggedUnion
from minecraft.resource import ResourceLocation from hexdoc.utils.deserialize import load_json
from patchouli.context import AnyBookContext
class Recipe(TypeTaggedUnion[AnyBookContext], group="hexdoc.Recipe", type=None): class Recipe(TypeTaggedUnion[AnyPropsContext], group="hexdoc.Recipe", type=None):
id: ResourceLocation id: ResourceLocation
group: str | None = None group: str | None = None
@ -31,7 +30,7 @@ class Recipe(TypeTaggedUnion[AnyBookContext], group="hexdoc.Recipe", type=None):
id = values id = values
# load the recipe # load the recipe
context = cast(AnyBookContext, info.context) context = cast(AnyPropsContext, info.context)
for recipe_dir in context["props"].recipe_dirs: for recipe_dir in context["props"].recipe_dirs:
# TODO: should this use id.namespace somewhere? # TODO: should this use id.namespace somewhere?
path = recipe_dir / f"{id.path}.json" path = recipe_dir / f"{id.path}.json"

View file

@ -0,0 +1,23 @@
from typing import Any
from hexdoc.resource import ResourceLocation, TypeTaggedUnion
from hexdoc.utils import AnyContext, NoValue
class ItemIngredient(
TypeTaggedUnion[AnyContext],
group="hexdoc.ItemIngredient",
type=None,
):
pass
ItemIngredientOrList = ItemIngredient[AnyContext] | list[ItemIngredient[AnyContext]]
class MinecraftItemIdIngredient(ItemIngredient[Any], type=NoValue):
item: ResourceLocation
class MinecraftItemTagIngredient(ItemIngredient[Any], type=NoValue):
tag: ResourceLocation

View file

@ -1,30 +1,31 @@
from common.model import HexDocModel from typing import Any
from minecraft.i18n import LocalizedItem
from patchouli.context import BookContext
from hexdoc.utils import HexDocModel
from ..i18n import LocalizedItem
from .abstract_recipes import Recipe from .abstract_recipes import Recipe
from .ingredients import ItemIngredientOrList from .ingredients import ItemIngredientOrList
class ItemResult(HexDocModel[BookContext]): class ItemResult(HexDocModel[Any]):
item: LocalizedItem item: LocalizedItem
count: int | None = None count: int | None = None
class CraftingShapedRecipe( class CraftingShapedRecipe(
Recipe[BookContext], Recipe[Any],
type="minecraft:crafting_shaped", type="minecraft:crafting_shaped",
): ):
pattern: list[str] pattern: list[str]
key: dict[str, ItemIngredientOrList[BookContext]] key: dict[str, ItemIngredientOrList[Any]]
result: ItemResult result: ItemResult
class CraftingShapelessRecipe( class CraftingShapelessRecipe(
Recipe[BookContext], Recipe[Any],
type="minecraft:crafting_shapeless", type="minecraft:crafting_shapeless",
): ):
ingredients: list[ItemIngredientOrList[BookContext]] ingredients: list[ItemIngredientOrList[Any]]
result: ItemResult result: ItemResult

View file

@ -0,0 +1,16 @@
__all__ = [
"Book",
"Category",
"Entry",
"Page",
"FormatTree",
"AnyBookContext",
"BookContext",
]
from .book import Book
from .category import Category
from .entry import Entry
from .model import AnyBookContext, BookContext
from .page import Page
from .text import FormatTree

View file

@ -2,16 +2,15 @@ from typing import Any, Generic, Literal, Self, cast
from pydantic import Field, ValidationInfo, model_validator from pydantic import Field, ValidationInfo, model_validator
from common.deserialize import isinstance_or_raise, load_json from hexdoc.minecraft import I18n, LocalizedStr
from common.model import AnyContext, HexDocModel from hexdoc.properties import Properties
from common.properties import Properties from hexdoc.resource import ItemStack, ResLoc, ResourceLocation
from common.types import Color from hexdoc.utils import AnyContext, Color, HexDocModel
from minecraft.i18n import I18n, LocalizedStr from hexdoc.utils.deserialize import isinstance_or_raise, load_json
from minecraft.resource import ItemStack, ResLoc, ResourceLocation
from .category import Category from .category import Category
from .context import AnyBookContext, BookContext
from .entry import Entry from .entry import Entry
from .model import AnyBookContext, BookContext
from .text import DEFAULT_MACROS, FormatTree from .text import DEFAULT_MACROS, FormatTree

View file

@ -3,17 +3,17 @@ from typing import Self
from pydantic import Field from pydantic import Field
from common.properties import Properties from hexdoc.minecraft import LocalizedStr
from common.types import Sortable, sorted_dict from hexdoc.properties import Properties
from minecraft.i18n import LocalizedStr from hexdoc.resource import ItemStack, ResourceLocation
from minecraft.resource import ItemStack, ResourceLocation from hexdoc.utils.types import Sortable, sorted_dict
from .context import BookContext, BookModelFile
from .entry import Entry from .entry import Entry
from .model import BookContext, BookFileModel
from .text import FormatTree from .text import FormatTree
class Category(BookModelFile[BookContext, BookContext], Sortable): class Category(BookFileModel[BookContext, BookContext], Sortable):
"""Category with pages and localizations. """Category with pages and localizations.
See: https://vazkiimods.github.io/Patchouli/docs/reference/category-json See: https://vazkiimods.github.io/Patchouli/docs/reference/category-json

View file

@ -3,16 +3,17 @@ from typing import cast
from pydantic import Field, ValidationInfo, model_validator from pydantic import Field, ValidationInfo, model_validator
from common.properties import Properties from hexdoc.minecraft import LocalizedStr
from common.types import Color, Sortable from hexdoc.properties import Properties
from minecraft.i18n import LocalizedStr from hexdoc.resource import ItemStack, ResourceLocation
from minecraft.resource import ItemStack, ResourceLocation from hexdoc.utils import Color
from hexdoc.utils.types import Sortable
from .context import BookContext, BookModelFile from .model import BookContext, BookFileModel
from .page import Page from .page.pages import Page
class Entry(BookModelFile[BookContext, BookContext], Sortable): class Entry(BookFileModel[BookContext, BookContext], Sortable):
"""Entry json file, with pages and localizations. """Entry json file, with pages and localizations.
See: https://vazkiimods.github.io/Patchouli/docs/reference/entry-json See: https://vazkiimods.github.io/Patchouli/docs/reference/entry-json

View file

@ -4,10 +4,11 @@ from typing import Any, Generic, TypeVar, cast, dataclass_transform
from pydantic import ValidationInfo, model_validator from pydantic import ValidationInfo, model_validator
from common.model import AnyContext, HexDocModelFile from hexdoc.properties import Properties
from common.properties import Properties from hexdoc.resource import ResourceLocation
from minecraft.resource import ResourceLocation from hexdoc.utils import AnyContext, HexDocFileModel
from patchouli.text import FormatContext
from .text.formatting import FormatContext
class BookContext(FormatContext): class BookContext(FormatContext):
@ -18,9 +19,9 @@ AnyBookContext = TypeVar("AnyBookContext", bound=BookContext)
@dataclass_transform() @dataclass_transform()
class BookModelFile( class BookFileModel(
Generic[AnyContext, AnyBookContext], Generic[AnyContext, AnyBookContext],
HexDocModelFile[AnyBookContext], HexDocFileModel[AnyBookContext],
ABC, ABC,
): ):
id: ResourceLocation id: ResourceLocation

View file

@ -2,17 +2,17 @@ __all__ = [
"Page", "Page",
"PageWithText", "PageWithText",
"PageWithTitle", "PageWithTitle",
"CraftingPage",
"EmptyPage",
"EntityPage",
"ImagePage",
"LinkPage",
"MultiblockPage",
"QuestPage",
"RelationsPage",
"SmeltingPage",
"SpotlightPage",
"TextPage", "TextPage",
"ImagePage",
"CraftingPage",
"SmeltingPage",
"MultiblockPage",
"EntityPage",
"SpotlightPage",
"LinkPage",
"RelationsPage",
"QuestPage",
"EmptyPage",
] ]
from .abstract_pages import Page, PageWithText, PageWithTitle from .abstract_pages import Page, PageWithText, PageWithTitle

View file

@ -3,11 +3,11 @@ from typing import Any, ClassVar, Self
from pydantic import model_validator from pydantic import model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler from pydantic.functional_validators import ModelWrapValidatorHandler
from common.tagged_union import TagValue, TypeTaggedUnion from hexdoc.minecraft import LocalizedStr
from minecraft.i18n import LocalizedStr from hexdoc.resource import ResourceLocation, TypeTaggedUnion
from minecraft.resource import ResourceLocation from hexdoc.utils import TagValue
from ..context import AnyBookContext from ..model import AnyBookContext
from ..text import FormatTree from ..text import FormatTree

View file

@ -1,10 +1,10 @@
from typing import Any from typing import Any
from minecraft.i18n import LocalizedItem, LocalizedStr from hexdoc.minecraft import LocalizedItem, LocalizedStr
from minecraft.recipe import CraftingRecipe from hexdoc.minecraft.recipe import CraftingRecipe
from minecraft.resource import Entity, ItemStack, ResourceLocation from hexdoc.resource import Entity, ItemStack, ResourceLocation
from patchouli.context import BookContext
from ..model import BookContext
from ..text import FormatTree from ..text import FormatTree
from .abstract_pages import Page, PageWithText, PageWithTitle from .abstract_pages import Page, PageWithText, PageWithTitle

View file

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

View file

@ -12,10 +12,11 @@ from pydantic import ValidationInfo, model_validator
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler from pydantic.functional_validators import ModelWrapValidatorHandler
from common.model import DEFAULT_CONFIG, HexDocModel from hexdoc.minecraft import LocalizedStr
from common.properties import Properties from hexdoc.minecraft.i18n import I18nContext
from common.types import TryGetEnum from hexdoc.properties import PropsContext
from minecraft.i18n import I18nContext, LocalizedStr from hexdoc.utils import DEFAULT_CONFIG, HexDocModel
from hexdoc.utils.types import TryGetEnum
from .html import HTMLElement, HTMLStream from .html import HTMLElement, HTMLStream
@ -251,9 +252,8 @@ class _CloseTag(HexDocModel[Any], frozen=True):
_FORMAT_RE = re.compile(r"\$\(([^)]*)\)") _FORMAT_RE = re.compile(r"\$\(([^)]*)\)")
class FormatContext(I18nContext): class FormatContext(I18nContext, PropsContext):
macros: dict[str, str] macros: dict[str, str]
props: Properties
@dataclass(config=DEFAULT_CONFIG) @dataclass(config=DEFAULT_CONFIG)

View file

@ -5,7 +5,7 @@ from dataclasses import dataclass
from html import escape from html import escape
from typing import IO, Any from typing import IO, Any
from minecraft.i18n import LocalizedStr from hexdoc.minecraft import LocalizedStr
def attributes_to_str(attributes: dict[str, Any]): def attributes_to_str(attributes: dict[str, Any]):

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import re import re
from pathlib import Path from pathlib import Path
from typing import Annotated, Any, Self from typing import Annotated, Any, Self, TypeVar
from pydantic import ( from pydantic import (
AfterValidator, AfterValidator,
@ -9,11 +11,11 @@ from pydantic import (
HttpUrl, HttpUrl,
field_validator, field_validator,
) )
from typing_extensions import TypedDict
from common.model import HexDocModel from hexdoc.resource import ResourceLocation
from common.toml_placeholders import load_toml from hexdoc.utils.model import HexDocModel
from hexcasting.pattern import PatternStubFile from hexdoc.utils.toml_placeholders import load_toml
from minecraft.resource import ResourceLocation
NoTrailingSlashHttpUrl = Annotated[ NoTrailingSlashHttpUrl = Annotated[
str, str,
@ -27,7 +29,7 @@ class PlatformProps(HexDocModel[Any]):
generated: Path generated: Path
src: Path src: Path
package: Path package: Path
pattern_stubs: list[PatternStubFile] | None = None pattern_stubs: list[Path] | None = None
class I18nProps(HexDocModel[Any]): class I18nProps(HexDocModel[Any]):
@ -112,7 +114,7 @@ class Properties(HexDocModel[Any]):
return platforms return platforms
@property @property
def pattern_stubs(self) -> list[PatternStubFile]: def pattern_stubs(self) -> list[Path]:
return [ return [
stub stub
for platform in self.platforms for platform in self.platforms
@ -132,3 +134,10 @@ class Properties(HexDocModel[Any]):
f"default_recipe_dir must be a valid index of recipe_dirs (expected <={num_dirs - 1}, got {value})" f"default_recipe_dir must be a valid index of recipe_dirs (expected <={num_dirs - 1}, got {value})"
) )
return value return value
class PropsContext(TypedDict):
props: Properties
AnyPropsContext = TypeVar("AnyPropsContext", bound=PropsContext)

View file

@ -1,5 +1,9 @@
# pyright: reportPrivateUsage=false # pyright: reportPrivateUsage=false
# this file is used by basically everything
# so if it's in literally any namespace, everything fucking dies from circular deps
# basically, just leave it here
import re import re
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Self from typing import Any, ClassVar, Self
@ -8,7 +12,13 @@ from pydantic import field_validator, model_serializer, model_validator
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from pydantic.functional_validators import ModelWrapValidatorHandler from pydantic.functional_validators import ModelWrapValidatorHandler
from common.model import DEFAULT_CONFIG from hexdoc.utils import (
DEFAULT_CONFIG,
AnyContext,
InternallyTaggedUnion,
NoValueType,
TagValue,
)
def _make_regex(count: bool = False, nbt: bool = False) -> re.Pattern[str]: def _make_regex(count: bool = False, nbt: bool = False) -> re.Pattern[str]:
@ -118,3 +128,22 @@ class Entity(BaseResourceLocation, regex=_make_regex(nbt=True)):
if self.nbt is not None: if self.nbt is not None:
s += self.nbt s += self.nbt
return s return s
class TypeTaggedUnion(InternallyTaggedUnion[AnyContext], key="type", value=None):
type: ResourceLocation | None
def __init_subclass__(
cls,
*,
group: str | None = None,
type: TagValue | None,
) -> None:
super().__init_subclass__(group=group, value=type)
match type:
case str():
cls.type = ResourceLocation.from_str(type)
case NoValueType():
cls.type = None
case None:
pass

View file

@ -1,7 +1,6 @@
# because Tap.add_argument isn't typed, for some reason # because Tap.add_argument isn't typed, for some reason
# pyright: reportUnknownMemberType=false # pyright: reportUnknownMemberType=false
import sys
from pathlib import Path from pathlib import Path
from jinja2 import Environment, FileSystemLoader, StrictUndefined from jinja2 import Environment, FileSystemLoader, StrictUndefined
@ -9,12 +8,9 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined
# from jinja2.sandbox import SandboxedEnvironment # from jinja2.sandbox import SandboxedEnvironment
from tap import Tap from tap import Tap
from common.jinja_extensions import IncludeRawExtension, hexdoc_block, hexdoc_wrap from hexdoc.hexcasting import HexBook
from common.properties import Properties from hexdoc.properties import Properties
from hexcasting.hex_book import HexBook from hexdoc.utils.jinja_extensions import IncludeRawExtension, hexdoc_block, hexdoc_wrap
if sys.version_info < (3, 11):
raise RuntimeError("Minimum Python version: 3.11")
def strip_empty_lines(text: str) -> str: def strip_empty_lines(text: str) -> str:
@ -33,7 +29,11 @@ class Args(Tap):
self.add_argument("-o", "--output_file", required=False) self.add_argument("-o", "--output_file", required=False)
def main(args: Args) -> None: def main(args: Args | None = None) -> None:
# allow passing Args for test cases, but parse by default
if args is None:
args = Args().parse_args()
# load the properties and book # load the properties and book
props = Properties.load(args.properties_file) props = Properties.load(args.properties_file)
book = HexBook.load(*HexBook.prepare(props)) book = HexBook.load(*HexBook.prepare(props))
@ -74,4 +74,4 @@ def main(args: Args) -> None:
# entry point: just read the CLI args and pass them to the actual logic # entry point: just read the CLI args and pass them to the actual logic
if __name__ == "__main__": if __name__ == "__main__":
main(Args().parse_args()) main()

View file

@ -0,0 +1,22 @@
__all__ = [
"HexDocModel",
"FrozenHexDocModel",
"HexDocFileModel",
"InternallyTaggedUnion",
"Color",
"AnyContext",
"DEFAULT_CONFIG",
"NoValue",
"NoValueType",
"TagValue",
]
from .model import (
DEFAULT_CONFIG,
AnyContext,
FrozenHexDocModel,
HexDocFileModel,
HexDocModel,
)
from .tagged_union import InternallyTaggedUnion, NoValue, NoValueType, TagValue
from .types import Color

View file

@ -6,6 +6,7 @@ _T = TypeVar("_T")
_DEFAULT_MESSAGE = "Expected any of {expected}, got {actual}: {value}" _DEFAULT_MESSAGE = "Expected any of {expected}, got {actual}: {value}"
# there may well be a better way to do this but i don't know what it is # there may well be a better way to do this but i don't know what it is
def isinstance_or_raise( def isinstance_or_raise(
val: Any, val: Any,

View file

@ -5,9 +5,9 @@ from jinja2.ext import Extension
from jinja2.parser import Parser from jinja2.parser import Parser
from markupsafe import Markup from markupsafe import Markup
from minecraft.i18n import LocalizedStr from hexdoc.minecraft import LocalizedStr
from patchouli.text.formatting import FormatTree from hexdoc.patchouli import FormatTree
from patchouli.text.html import HTMLStream from hexdoc.patchouli.text import HTMLStream
# https://stackoverflow.com/a/64392515 # https://stackoverflow.com/a/64392515

View file

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar, dataclass_transfo
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from typing_extensions import TypedDict from typing_extensions import TypedDict
from common.deserialize import load_json from .deserialize import load_json
if TYPE_CHECKING: if TYPE_CHECKING:
from pydantic.root_model import Model from pydantic.root_model import Model
@ -53,7 +53,7 @@ class FrozenHexDocModel(Generic[AnyContext], HexDocModel[AnyContext]):
@dataclass_transform() @dataclass_transform()
class HexDocModelFile(HexDocModel[AnyContext]): class HexDocFileModel(HexDocModel[AnyContext]):
@classmethod @classmethod
def load(cls, path: Path, context: AnyContext) -> Self: def load(cls, path: Path, context: AnyContext) -> Self:
data = load_json(path) | {"__path": path} data = load_json(path) | {"__path": path}

View file

@ -10,10 +10,11 @@ from pkg_resources import iter_entry_points
from pydantic import ValidationInfo, model_validator from pydantic import ValidationInfo, model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler from pydantic.functional_validators import ModelWrapValidatorHandler
from minecraft.resource import ResourceLocation
from .model import AnyContext, HexDocModel from .model import AnyContext, HexDocModel
# from hexdoc.minecraft import ResourceLocation
if TYPE_CHECKING: if TYPE_CHECKING:
from pydantic.root_model import Model from pydantic.root_model import Model
@ -154,13 +155,13 @@ class InternallyTaggedUnion(HexDocModel[AnyContext]):
context: AnyContext | None = None, context: AnyContext | None = None,
) -> Model: ) -> Model:
# resolve forward references, because apparently we need to do this # resolve forward references, because apparently we need to do this
if cls not in _rebuilt_models: # if cls not in _rebuilt_models:
_rebuilt_models.add(cls) # _rebuilt_models.add(cls)
cls.model_rebuild( # cls.model_rebuild(
_types_namespace={ # _types_namespace={
"ResourceLocation": ResourceLocation, # "ResourceLocation": ResourceLocation,
} # }
) # )
return super().model_validate( return super().model_validate(
obj, obj,
@ -232,22 +233,3 @@ class InternallyTaggedUnion(HexDocModel[AnyContext]):
f"Failed to match {cls} with {cls._tag_key}={tag_value} to any of {tag_types}: {data}", f"Failed to match {cls} with {cls._tag_key}={tag_value} to any of {tag_types}: {data}",
exceptions, exceptions,
) )
class TypeTaggedUnion(InternallyTaggedUnion[AnyContext], key="type", value=None):
type: ResourceLocation | None
def __init_subclass__(
cls,
*,
group: str | None = None,
type: TagValue | None,
) -> None:
super().__init_subclass__(group=group, value=type)
match type:
case str():
cls.type = ResourceLocation.from_str(type)
case NoValueType():
cls.type = None
case None:
pass

View file

@ -1,11 +1,10 @@
import datetime import datetime
import re import re
import tomllib
from pathlib import Path from pathlib import Path
from typing import Callable, TypeVar from typing import Callable, TypeVar
import tomllib from .deserialize import isinstance_or_raise
from common.deserialize import isinstance_or_raise
# TODO: there's (figuratively) literally no comments in this file # TODO: there's (figuratively) literally no comments in this file

View file

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

View file

@ -1,24 +0,0 @@
from common.tagged_union import NoValue, TypeTaggedUnion
from minecraft.resource import ResourceLocation
from patchouli.context import AnyBookContext, BookContext
class ItemIngredient(
TypeTaggedUnion[AnyBookContext],
group="hexdoc.ItemIngredient",
type=None,
):
pass
ItemIngredientOrList = (
ItemIngredient[AnyBookContext] | list[ItemIngredient[AnyBookContext]]
)
class MinecraftItemIdIngredient(ItemIngredient[BookContext], type=NoValue):
item: ResourceLocation
class MinecraftItemTagIngredient(ItemIngredient[BookContext], type=NoValue):
tag: ResourceLocation

View file

@ -1,7 +0,0 @@
__all__ = ["Book", "Category", "Entry", "Page", "Style", "FormatTree"]
from .book import Book
from .category import Category
from .entry import Entry
from .page import Page
from .text import FormatTree, Style

View file

@ -1,8 +0,0 @@
__all__ = [
"FormatTree",
"Style",
"DEFAULT_MACROS",
"FormatContext",
]
from .formatting import DEFAULT_MACROS, FormatContext, FormatTree, Style

View file

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

View file

@ -1,34 +1,13 @@
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Iterator
import pytest import pytest
from bs4 import BeautifulSoup as bs
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from syrupy.extensions.amber import AmberSnapshotExtension
from syrupy.types import SerializedData
from hexcasting.scripts.main import Args, main from hexdoc.scripts.hexdoc import Args, main
_RUN = ["hexdoc"]
def prettify(data: SerializedData) -> str:
return bs(data, features="html.parser").prettify()
class NoDiffSnapshotEx(AmberSnapshotExtension):
def diff_snapshots(
self, serialized_data: SerializedData, snapshot_data: SerializedData
) -> SerializedData:
return "no diff"
def diff_lines(
self, serialized_data: SerializedData, snapshot_data: SerializedData
) -> Iterator[str]:
yield from ["no diff"]
_RUN = [sys.executable, "-m" "hexcasting.scripts.main"]
_ARGV = ["properties.toml", "-o"] _ARGV = ["properties.toml", "-o"]
@ -53,28 +32,3 @@ def test_cmd(tmp_path: Path, snapshot: SnapshotAssertion):
def test_stdout(capsys: pytest.CaptureFixture[str], snapshot: SnapshotAssertion): def test_stdout(capsys: pytest.CaptureFixture[str], snapshot: SnapshotAssertion):
main(Args().parse_args(["properties.toml"])) main(Args().parse_args(["properties.toml"]))
assert capsys.readouterr() == snapshot assert capsys.readouterr() == snapshot
# def test_book_text(snapshot: SnapshotAssertion):
# def test_field(data_class: Any, field: Field[Any]):
# value = getattr(data_class, field.name, None)
# if isinstance(value, (LocalizedStr, FormatTree)):
# assert value == snapshot
# props = Properties.load(Path("properties.toml"))
# book = HexBook.load(HexBookState(props))
# for field in fields(book):
# test_field(book, field)
# for category in book.categories.values():
# for field in fields(category):
# test_field(category, field)
# for entry in category.entries:
# for field in fields(entry):
# test_field(entry, field)
# for page in entry.pages:
# for field in fields(page):
# test_field(page, field)

View file

@ -1,6 +1,6 @@
import pytest import pytest
from common.types import Color from hexdoc.utils.types import Color
colors: list[str] = [ colors: list[str] = [
"#0099FF", "#0099FF",

View file

@ -1,6 +1,6 @@
# pyright: reportPrivateUsage=false # pyright: reportPrivateUsage=false
from patchouli.text import DEFAULT_MACROS, FormatTree from hexdoc.patchouli.text import DEFAULT_MACROS, FormatTree
from patchouli.text.formatting import ( from hexdoc.patchouli.text.formatting import (
CommandStyle, CommandStyle,
FunctionStyle, FunctionStyle,
FunctionStyleType, FunctionStyleType,