@@ -13,18 +13,17 @@ class since several methods based on each file are passing similar parameters wi
1313
1414import logging
1515from collections import defaultdict
16- from collections .abc import Callable
16+ from collections .abc import Callable , Iterator
1717from dataclasses import dataclass , field
1818from importlib import resources
19- from typing import Iterator , Literal , cast
19+ from typing import Literal , cast
2020
2121from beet import Context , DataPack , JsonFile , ListOption , NamespaceFile
2222from beet .contrib .format_json import get_formatter
2323from beet .contrib .vanilla import Vanilla
24- from pydantic . v1 import ValidationError
24+ from pydantic import ValidationError
2525
2626from smithed .type import JsonDict , JsonTypeT
27- from ..toolchain .process import PackProcessor
2827
2928from ..models import (
3029 AppendRule ,
@@ -38,9 +37,12 @@ class since several methods based on each file are passing similar parameters wi
3837 ReplaceRule ,
3938 Rule ,
4039 SmithedJsonFile ,
40+ SmithedModel ,
4141 ValueSource ,
4242 deserialize ,
43+ serialize_list_option ,
4344)
45+ from ..toolchain .process import PackProcessor
4446from .errors import PriorityError
4547from .parser import append , get , insert , merge , prepend , remove , replace
4648
@@ -52,6 +54,24 @@ class since several methods based on each file are passing similar parameters wi
5254)
5355
5456
57+ def get_override (entry : SmithedModel | dict ) -> bool :
58+ """ Safely get override attribute from entry that may be dict or model object.
59+
60+ Due to mixing Pydantic V1 (beet's ListOption) and V2 (SmithedModel),
61+ entries may be converted to dicts. This helper handles both cases.
62+ """
63+ if isinstance (entry , dict ):
64+ return entry .get ("override" , False ) or False
65+ return entry .override or False
66+
67+
68+ def get_entry_id (entry : SmithedModel | dict ) -> str :
69+ """ Safely get id attribute from entry that may be dict or model object. """
70+ if isinstance (entry , dict ):
71+ return entry .get ("id" , "" )
72+ return entry .id
73+
74+
5575@dataclass
5676class ConflictsHandler :
5777 ctx : Context
@@ -97,17 +117,17 @@ def __call__(
97117
98118 current_entries = smithed_current .smithed .entries ()
99119
100- if len (current_entries ) > 0 and current_entries [0 ]. override :
120+ if len (current_entries ) > 0 and get_override ( current_entries [0 ]) :
101121 logger .critical (
102- f"Overriding base file at `{ path } ` with { current_entries [0 ]. id } "
122+ f"Overriding base file at `{ path } ` with { get_entry_id ( current_entries [0 ]) } "
103123 )
104124 self .overrides .add (path )
105125 return True
106126
107127 conflict_entries = smithed_conflict .smithed .entries ()
108- if len (conflict_entries ) > 0 and conflict_entries [0 ]. override :
128+ if len (conflict_entries ) > 0 and get_override ( conflict_entries [0 ]) :
109129 logger .critical (
110- f"Overriding base file at `{ path } ` with { conflict_entries [0 ]. id } "
130+ f"Overriding base file at `{ path } ` with { get_entry_id ( conflict_entries [0 ]) } "
111131 )
112132 self .overrides .add (path )
113133 current .data = conflict .data
@@ -150,8 +170,8 @@ def __call__(
150170 current_entries .extend (conflict_entries )
151171
152172 # Save back to current file
153- raw : JsonDict = deserialize ( smithed_current )
154- current .data ["__smithed__" ] = raw [ "__smithed__" ]
173+ # Use serialize_list_option to avoid __root__ in the output
174+ current .data ["__smithed__" ] = serialize_list_option ( smithed_current . smithed )
155175
156176 current .data = normalize_quotes (current .data )
157177
@@ -162,8 +182,12 @@ def parse_smithed_file(
162182 ) -> SmithedJsonFile | Literal [False ]:
163183 """Parses a smithed file and returns the parsed file or False if invalid."""
164184
185+ # Preprocess data to remove __root__ fields that may have been created
186+ # by ListOption (Pydantic V1) serialization
187+ data = self .clean_list_option_data (file .data )
188+
165189 try :
166- obj = SmithedJsonFile .parse_obj ( file . data )
190+ obj = SmithedJsonFile .model_validate ( data )
167191 except ValidationError :
168192 logger .error ("Failed to parse smithed file " , exc_info = True )
169193 return False
@@ -180,6 +204,28 @@ def parse_smithed_file(
180204
181205 return obj
182206
207+ def clean_list_option_data (self , data : JsonDict ) -> JsonDict :
208+ """Remove __root__ fields from __smithed__ entries.
209+
210+ ListOption (Pydantic V1) serializes with __root__ field, which causes
211+ validation errors in Pydantic V2 models with extra="forbid".
212+ """
213+ data = data .copy ()
214+
215+ if "__smithed__" in data :
216+ smithed = data ["__smithed__" ]
217+ if isinstance (smithed , list ):
218+ cleaned = []
219+ for entry in smithed :
220+ if isinstance (entry , dict ) and "__root__" in entry :
221+ # Extract the actual data from __root__
222+ cleaned .append (entry ["__root__" ] if entry ["__root__" ] else {})
223+ else :
224+ cleaned .append (entry )
225+ data ["__smithed__" ] = cleaned
226+
227+ return data
228+
183229 def grab_vanilla (self , path : str , json_file_type : type [NamespaceFile ]) -> JsonDict | None :
184230 """Grabs the vanilla file to load as the current file (aka the base)."""
185231
@@ -198,9 +244,9 @@ def process(self):
198244 logger .info (f"Resolving { json_file_type .__name__ } : { path !r} " )
199245
200246 namespace_file = self .ctx .data [json_file_type ]
201- smithed_file = SmithedJsonFile . parse_obj (
202- namespace_file [path ].data # type: ignore
203- )
247+ # Clean data before parsing to remove __root__ fields
248+ data = self . clean_list_option_data ( namespace_file [path ].data ) # type: ignore
249+ smithed_file = SmithedJsonFile . model_validate ( data )
204250
205251 if smithed_file .smithed .entries ():
206252 processed = self .process_file (smithed_file )
0 commit comments