Pydantic ResLoc
This commit is contained in:
parent
31188b7332
commit
0a949a2b56
3 changed files with 66 additions and 84 deletions
|
@ -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"]
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue