Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ authors = [
{name = "Smithed Team", email = "[email protected]"},
]
dependencies = [
"beet @ git+https://github.com/Smithed-MC/beet@fix/overlay-folder-names",
"mecha>=0.95.2",
"beet>=0.113.0b10",
"mecha>=0.102.0b2",
"typer>=0.9.0",
"tokenstream>=1.7.0",
"backports-strenum>=1.2.8",
"rich>=13.6.0",
"pydantic>=2.5.2",
"pydantic>=2.12.0",
]
description = "Smithed's Python client with CLI, Weld and more"
license = "MIT"
name = "smithed"
readme = "README.md"
requires-python = ">= 3.10"
version = "0.19.0"
requires-python = ">= 3.14"
version = "0.20.0"

[project.scripts]
weld = "smithed.weld:cli"
Expand Down
74 changes: 60 additions & 14 deletions smithed/weld/merging/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ class since several methods based on each file are passing similar parameters wi

import logging
from collections import defaultdict
from collections.abc import Callable
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
from importlib import resources
from typing import Iterator, Literal, cast
from typing import Literal, cast

from beet import Context, DataPack, JsonFile, ListOption, NamespaceFile
from beet.contrib.format_json import get_formatter
from beet.contrib.vanilla import Vanilla
from pydantic.v1 import ValidationError
from pydantic import ValidationError

from smithed.type import JsonDict, JsonTypeT
from ..toolchain.process import PackProcessor

from ..models import (
AppendRule,
Expand All @@ -38,9 +37,12 @@ class since several methods based on each file are passing similar parameters wi
ReplaceRule,
Rule,
SmithedJsonFile,
SmithedModel,
ValueSource,
deserialize,
serialize_list_option,
)
from ..toolchain.process import PackProcessor
from .errors import PriorityError
from .parser import append, get, insert, merge, prepend, remove, replace

Expand All @@ -52,6 +54,24 @@ class since several methods based on each file are passing similar parameters wi
)


def get_override(entry: SmithedModel | dict) -> bool:
""" Safely get override attribute from entry that may be dict or model object.

Due to mixing Pydantic V1 (beet's ListOption) and V2 (SmithedModel),
entries may be converted to dicts. This helper handles both cases.
"""
if isinstance(entry, dict):
return entry.get("override", False) or False
return entry.override or False


def get_entry_id(entry: SmithedModel | dict) -> str:
""" Safely get id attribute from entry that may be dict or model object. """
if isinstance(entry, dict):
return entry.get("id", "")
return entry.id


@dataclass
class ConflictsHandler:
ctx: Context
Expand Down Expand Up @@ -97,17 +117,17 @@ def __call__(

current_entries = smithed_current.smithed.entries()

if len(current_entries) > 0 and current_entries[0].override:
if len(current_entries) > 0 and get_override(current_entries[0]):
logger.critical(
f"Overriding base file at `{path}` with {current_entries[0].id}"
f"Overriding base file at `{path}` with {get_entry_id(current_entries[0])}"
)
self.overrides.add(path)
return True

conflict_entries = smithed_conflict.smithed.entries()
if len(conflict_entries) > 0 and conflict_entries[0].override:
if len(conflict_entries) > 0 and get_override(conflict_entries[0]):
logger.critical(
f"Overriding base file at `{path}` with {conflict_entries[0].id}"
f"Overriding base file at `{path}` with {get_entry_id(conflict_entries[0])}"
)
self.overrides.add(path)
current.data = conflict.data
Expand Down Expand Up @@ -150,8 +170,8 @@ def __call__(
current_entries.extend(conflict_entries)

# Save back to current file
raw: JsonDict = deserialize(smithed_current)
current.data["__smithed__"] = raw["__smithed__"]
# Use serialize_list_option to avoid __root__ in the output
current.data["__smithed__"] = serialize_list_option(smithed_current.smithed)

current.data = normalize_quotes(current.data)

Expand All @@ -162,8 +182,12 @@ def parse_smithed_file(
) -> SmithedJsonFile | Literal[False]:
"""Parses a smithed file and returns the parsed file or False if invalid."""

# Preprocess data to remove __root__ fields that may have been created
# by ListOption (Pydantic V1) serialization
data = self.clean_list_option_data(file.data)

try:
obj = SmithedJsonFile.parse_obj(file.data)
obj = SmithedJsonFile.model_validate(data)
except ValidationError:
logger.error("Failed to parse smithed file ", exc_info=True)
return False
Expand All @@ -180,6 +204,28 @@ def parse_smithed_file(

return obj

def clean_list_option_data(self, data: JsonDict) -> JsonDict:
"""Remove __root__ fields from __smithed__ entries.

ListOption (Pydantic V1) serializes with __root__ field, which causes
validation errors in Pydantic V2 models with extra="forbid".
"""
data = data.copy()

if "__smithed__" in data:
smithed = data["__smithed__"]
if isinstance(smithed, list):
cleaned = []
for entry in smithed:
if isinstance(entry, dict) and "__root__" in entry:
# Extract the actual data from __root__
cleaned.append(entry["__root__"] if entry["__root__"] else {})
else:
cleaned.append(entry)
data["__smithed__"] = cleaned

return data

def grab_vanilla(self, path: str, json_file_type: type[NamespaceFile]) -> JsonDict|None:
"""Grabs the vanilla file to load as the current file (aka the base)."""

Expand All @@ -198,9 +244,9 @@ def process(self):
logger.info(f"Resolving {json_file_type.__name__}: {path!r}")

namespace_file = self.ctx.data[json_file_type]
smithed_file = SmithedJsonFile.parse_obj(
namespace_file[path].data # type: ignore
)
# Clean data before parsing to remove __root__ fields
data = self.clean_list_option_data(namespace_file[path].data) # type: ignore
smithed_file = SmithedJsonFile.model_validate(data)

if smithed_file.smithed.entries():
processed = self.process_file(smithed_file)
Expand Down
32 changes: 20 additions & 12 deletions smithed/weld/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .conditions import Condition, ConditionInverted, ConditionPackCheck
from .main import SmithedJsonFile, SmithedModel, deserialize
from .main import SmithedJsonFile, SmithedModel, deserialize, serialize_list_option
from .priority import Priority
from .rules import (
AdditiveRule,
Expand All @@ -14,24 +14,32 @@
)
from .sources import ReferenceSource, Source, ValueSource

# Rebuild models to resolve forward references after all imports
ConditionInverted.model_rebuild()
ConditionPackCheck.model_rebuild()
SmithedModel.model_rebuild()
SmithedJsonFile.model_rebuild()

__all__ = [
"deserialize",
"AdditiveRule",
"MergeRule",
"AppendRule",
"PrependRule",
"InsertRule",
"ReplaceRule",
"RemoveRule",
"Rule",
"RuleHelper",
"Condition",
"ConditionInverted",
"ConditionPackCheck",
"InsertRule",
"MergeRule",
"PrependRule",
"Priority",
"ReferenceSource",
"ValueSource",
"Source",
"SmithedModel",
"RemoveRule",
"ReplaceRule",
"Rule",
"RuleHelper",
"SmithedJsonFile",
"SmithedModel",
"Source",
"ValueSource",
"deserialize",
"serialize_list_option",
]

2 changes: 1 addition & 1 deletion smithed/weld/models/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import TypeVar

from pydantic.v1 import BaseModel as _BaseModel
from pydantic import BaseModel as _BaseModel

T = TypeVar("T")

Expand Down
3 changes: 0 additions & 3 deletions smithed/weld/models/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@ class ConditionPackCheck(BaseModel):
class ConditionInverted(BaseModel):
type: Literal["inverted", "weld:inverted", "smithed:inverted"]
conditions: list["Condition"]


ConditionInverted.update_forward_refs()
44 changes: 38 additions & 6 deletions smithed/weld/models/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Any

from beet import ListOption
from pydantic.v1 import Field, root_validator
from pydantic import Field, model_validator

from ..merging.parser import get
from .base import BaseModel
Expand All @@ -15,7 +15,34 @@


def deserialize(model: BaseModel, defaults: bool = True):
return json.loads(model.json(by_alias=True, exclude_defaults=not defaults))
""" Serialize a Pydantic model to dict.

Uses Pydantic V2 API (model_dump_json) with fallback to V1 (json).
"""
try:
# Pydantic V2
return json.loads(model.model_dump_json(
by_alias=True,
exclude_defaults=not defaults
))
except AttributeError:
# Pydantic V1 fallback
return json.loads(model.json(by_alias=True, exclude_defaults=not defaults))


def serialize_list_option(list_option: ListOption, defaults: bool = True) -> list:
""" Serialize a ListOption to a list of dicts.

Avoids the __root__ field issue by directly serializing the entries.
"""
result = []
for entry in list_option.entries():
if isinstance(entry, BaseModel):
result.append(deserialize(entry, defaults))
else:
# Already a dict
result.append(entry)
return result


class SmithedModel(BaseModel, extra="forbid"):
Expand All @@ -25,24 +52,29 @@ class SmithedModel(BaseModel, extra="forbid"):
priority: Priority | None = None
rules: list[Rule] = []

@root_validator
@model_validator(mode="before")
def push_down_priorities(cls, values: dict[str, Any]) -> dict[str, Any]:
"""Push down top-level priority to every rule.

If a rule has a priority defined, it will not be overwritten.
"""

rules: list[Rule] = values.get("rules") # type: ignore
rules: list[Rule] | None = values.get("rules") # type: ignore
priority: Priority | None = values.get("priority") # type: ignore

if rules is None:
rules = []
values["rules"] = rules

if priority is None:
priority = Priority()

for rule in rules:
if rule.priority is None:
rule.priority = priority

values.pop("priority")
if "priority" in values:
values.pop("priority")

return values

Expand All @@ -55,7 +87,7 @@ class SmithedJsonFile(BaseModel, extra="allow"):
default_factory=ListOption, alias="__smithed__"
)

@root_validator
@model_validator(mode="before")
def convert_type(cls, values: dict[str, ListOption[SmithedModel]]):
if smithed := values.get("smithed"):
for model in smithed.entries():
Expand Down
4 changes: 2 additions & 2 deletions smithed/weld/models/priority.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Literal

from beet import ListOption
from pydantic.v1 import validator
from pydantic import field_validator

from .base import BaseModel

Expand All @@ -16,6 +16,6 @@ class Priority(BaseModel):
before: ListOption[str] = ListOption()
after: ListOption[str] = ListOption()

@validator("before", "after")
@field_validator("before", "after")
def convert_fields(cls, value: ListOption[str]):
return ListOption(__root__=list(dict.fromkeys(value.entries())))
4 changes: 2 additions & 2 deletions smithed/weld/models/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from typing import Annotated, Literal

from pydantic.v1 import Field, validator
from pydantic import Field, field_validator

from .base import BaseModel
from .conditions import Condition
Expand All @@ -19,7 +19,7 @@ class BaseRule(BaseModel):
conditions: list[Condition] = []
priority: Priority | None = None

@validator("type")
@field_validator("type")
def fix_type(cls, value: str):
if value.startswith("smithed:"):
return value.replace("smithed:", "weld:")
Expand Down
4 changes: 2 additions & 2 deletions smithed/weld/models/sources.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Annotated, Any, Literal

from pydantic.v1 import Field, validator
from pydantic import Field, field_validator

from .base import BaseModel


class _Source(BaseModel):
type: str

@validator("type")
@field_validator("type")
def fix_type(cls, value: str):
if value.startswith("smithed:"):
return value.replace("smithed:", "weld:")
Expand Down
2 changes: 1 addition & 1 deletion smithed/weld/webapp/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import NamedTuple

from pydantic.v1 import BaseModel
from pydantic import BaseModel
from streamlit.delta_generator import DeltaGenerator


Expand Down
Loading