Pydantic ResLoc

This commit is contained in:
object-Object 2023-06-29 02:01:08 -04:00
parent 31188b7332
commit 0a949a2b56
3 changed files with 66 additions and 84 deletions

View file

@ -14,10 +14,8 @@ authors = [
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
# better CLI argument parsing
"typed-argument-parser>=1.8.0",
# dict-to-dataclass (TODO: switch to Pydantic)
"dacite@git+https://github.com/mciszczon/dacite@f298260c6aedc1097c7567b1b0a61298a0ddf2a8",
"pydantic==2.0b3",
]
[project.entry-points."hexdoc.Page"]

View file

@ -3,17 +3,23 @@
from common import dacite_patch as _ # isort: skip
import json
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, TypeVar
import tomllib
from dacite import Config, from_dict
from pydantic import ConfigDict
from common.dacite_patch import handle_metadata
from common.toml_placeholders import TOMLDict, fill_placeholders
from common.types import Castable, JSONDict, JSONValue, isinstance_or_raise
DEFAULT_CONFIG = ConfigDict(
strict=True,
extra="forbid",
)
_T_Input = TypeVar("_T_Input")
_T_Dataclass = TypeVar("_T_Dataclass")

View file

@ -3,66 +3,70 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Self
from pydantic import field_validator, model_validator, validator
from pydantic.dataclasses import dataclass
from common.deserialize import DEFAULT_CONFIG
from common.types import isinstance_or_raise
_RESOURCE_LOCATION_RE = re.compile(r"(?:([0-9a-z_\-.]+):)?([0-9a-z_\-./]+)")
_ITEM_STACK_SUFFIX_RE = re.compile(r"(?:#([0-9]+))?({.*})?")
_ENTITY_SUFFIX_RE = re.compile(r"({.*})?")
def _make_re(count: bool = False, nbt: bool = False) -> re.Pattern[str]:
pattern = r"(?:([0-9a-z_\-.]+):)?([0-9a-z_\-./]+)"
if count:
pattern += r"(?:#([0-9]+))?"
if nbt:
pattern += r"({.*})?"
return re.compile(pattern)
def _match(
pat: re.Pattern[str],
fullmatch: bool,
string: str,
pos: int = 0,
) -> re.Match[str] | None:
if fullmatch:
return pat.fullmatch(string, pos)
return pat.match(string, pos)
_RESOURCE_LOCATION_RE = _make_re()
_ITEM_STACK_RE = _make_re(count=True, nbt=True)
_ENTITY_RE = _make_re(nbt=True)
@dataclass(repr=False, frozen=True)
class BaseResourceLocation:
@dataclass(config=DEFAULT_CONFIG, repr=False, frozen=True)
class BaseResourceLocation(ABC):
"""Represents a Minecraft resource location / namespaced ID."""
namespace: str
path: str
@classmethod
def _parse_str(
cls,
raw: str,
fullmatch: bool = True,
) -> tuple[tuple[Any, ...], re.Match[str]]:
assert isinstance_or_raise(raw, str)
match = _match(_RESOURCE_LOCATION_RE, fullmatch, raw)
if match is None:
raise ValueError(f"invalid resource location: {raw}")
namespace, path = match.groups()
if namespace is None:
namespace = "minecraft"
return (namespace, path), match
@classmethod
def from_str(cls, raw: str | Self) -> Self:
@classmethod # TODO: model_validator
def from_str(cls, raw: Self | str) -> Self:
if isinstance(raw, BaseResourceLocation):
return raw
parts, _ = cls._parse_str(raw, fullmatch=True)
return cls(*parts)
return cls(*cls._match_groups(raw))
@classmethod
def _match_groups(cls, raw: str) -> tuple[str, ...]:
assert isinstance_or_raise(raw, str) # TODO: remove
match = cls._fullmatch(raw)
if match is None:
raise ValueError(f"Invalid {cls.__name__} string: {raw}")
namespace, *rest = match.groups()
return (namespace or "minecraft", *rest)
@classmethod
@abstractmethod
def _fullmatch(cls, string: str) -> re.Match[str] | None:
...
def __repr__(self) -> str:
return f"{self.namespace}:{self.path}"
@dataclass(repr=False, frozen=True)
@dataclass(config=DEFAULT_CONFIG, repr=False, frozen=True)
class ResourceLocation(BaseResourceLocation):
@classmethod
def _fullmatch(cls, string: str) -> re.Match[str] | None:
return _RESOURCE_LOCATION_RE.fullmatch(string)
@classmethod
def from_file(cls, modid: str, base_dir: Path, path: Path) -> ResourceLocation:
resource_path = path.relative_to(base_dir).with_suffix("").as_posix()
@ -77,43 +81,31 @@ class ResourceLocation(BaseResourceLocation):
ResLoc = ResourceLocation
@dataclass(repr=False, frozen=True)
@dataclass(config=DEFAULT_CONFIG, repr=False, frozen=True)
class ItemStack(BaseResourceLocation):
"""Represents an item with optional count and NBT tags.
"""Represents an item with optional count and NBT.
Does not inherit from ResourceLocation.
Inherits from BaseResourceLocation, not ResourceLocation.
"""
count: int | None = None
nbt: str | None = None
@field_validator("count", mode="before") # TODO: move this into _match_groups?
def convert_count(cls, count: str | int | None):
if isinstance(count, str):
return int(count)
return count
@classmethod
def _parse_str(
cls,
raw: str,
fullmatch: bool = True,
) -> tuple[tuple[Any, ...], re.Match[str]]:
rl_parts, rl_match = super()._parse_str(raw, fullmatch=False)
match = _match(_ITEM_STACK_SUFFIX_RE, fullmatch, raw, rl_match.end())
if match is None:
raise ValueError(f"invalid ItemStack String: {raw}")
count, nbt = match.groups()
if count is not None:
count = int(count)
return rl_parts + (count, nbt), match
@property
def id(self) -> ResourceLocation:
return ResourceLocation(self.namespace, self.path)
def _fullmatch(cls, string: str) -> re.Match[str] | None:
return _ITEM_STACK_RE.fullmatch(string)
def i18n_key(self, root: str = "item") -> str:
return f"{root}.{self.namespace}.{self.path}"
def __repr__(self) -> str:
s = str(self.id)
s = super().__repr__()
if self.count is not None:
s += f"#{self.count}"
if self.nbt is not None:
@ -125,31 +117,17 @@ class ItemStack(BaseResourceLocation):
class Entity(BaseResourceLocation):
"""Represents an entity with optional NBT.
Does not inherit from ResourceLocation.
Inherits from BaseResourceLocation, not ResourceLocation.
"""
nbt: str | None = None
@classmethod
def _parse_str(
cls,
raw: str,
fullmatch: bool = True,
) -> tuple[tuple[Any, ...], re.Match[str]]:
rl_parts, rl_match = super()._parse_str(raw, fullmatch=False)
match = _match(_ENTITY_SUFFIX_RE, fullmatch, raw, rl_match.end())
if match is None:
raise ValueError(f"invalid Entity: {raw}")
return rl_parts + (match[1],), match
@property
def id(self) -> ResourceLocation:
return ResourceLocation(self.namespace, self.path)
def _fullmatch(cls, string: str) -> re.Match[str] | None:
return _ENTITY_RE.fullmatch(string)
def __repr__(self) -> str:
s = str(self.id)
s = super().__repr__()
if self.nbt is not None:
s += self.nbt
return s