33import re
44import coloredlogs
55import yaml
6- from os .path import join , isfile
6+ from os .path import join , isfile , basename
77from yamlcore import CoreDumper , CoreLoader
88
99
@@ -54,21 +54,44 @@ def hash_yaml(section_to_hash):
5454 return hash_digest
5555
5656
57- def patch_config (logger , base_config , patch ):
57+ def patch_config (logger , base_config , patch , patch_name = "patch" , origin_map = None , verbose = False ):
58+ # Initialize origin map if it wasn't passed in
59+ if origin_map is None :
60+ origin_map = {}
61+
62+ # Helper to recursively claim ownership of keys in the origin map
63+ def _record_origins (obj , path_prefix , source_name ):
64+ if hasattr (obj , "model_fields_set" ):
65+ for k in obj .model_fields_set :
66+ _record_origins (getattr (obj , k ), f"{ path_prefix } .{ k } " if path_prefix else k , source_name )
67+ if obj .model_extra is not None :
68+ for k , val in obj .model_extra .items ():
69+ _record_origins (val , f"{ path_prefix } .{ k } " if path_prefix else k , source_name )
70+ elif isinstance (obj , dict ):
71+ for k , val in obj .items ():
72+ _record_origins (val , f"{ path_prefix } .{ k } " if path_prefix else k , source_name )
73+ else :
74+ # Leaves and lists get recorded directly
75+ origin_map [path_prefix ] = source_name
76+
5877 if not patch :
5978 # Empty patch, possibly an empty file or one with all comments
6079 return base_config
6180
81+ # If this is the very first run, populate the origin map with the base config
82+ if not origin_map :
83+ _record_origins (base_config , "" , "base_config" )
84+
6285 # Merge configs.
6386 def _recursive_update (base , new , config_option ):
6487 if base is None :
88+ _record_origins (new , config_option , patch_name )
6589 return new
6690 if new is None :
6791 return base
6892
69- # assert type(base) is type(new)
70-
7193 if hasattr (base , "merge" ):
94+ origin_map [config_option ] = patch_name
7295 return base .merge (new )
7396
7497 if hasattr (base , "model_fields_set" ):
@@ -80,61 +103,87 @@ def _recursive_update(base, new, config_option):
80103 result [base_key ] = base_value
81104 for new_key in new .model_fields_set :
82105 new_value = getattr (new , new_key )
106+ full_path = f"{ config_option } .{ new_key } " if config_option else new_key
83107 if new_key in result :
84108 result [new_key ] = _recursive_update (
85109 result [new_key ],
86110 new_value ,
87- f" { config_option } . { new_key } " if config_option else new_key ,
111+ full_path ,
88112 )
89113 else :
90114 result [new_key ] = new_value
115+ _record_origins (new_value , full_path , patch_name )
116+
91117 if new .model_extra is not None :
92118 for new_key , new_value in new .model_extra .items ():
119+ full_path = f"{ config_option } .{ new_key } " if config_option else new_key
93120 if new_key in result :
94121 result [new_key ] = _recursive_update (
95122 result [new_key ],
96123 new_value ,
97- f" { config_option } . { new_key } " if config_option else new_key ,
124+ full_path ,
98125 )
99126 else :
100127 result [new_key ] = new_value
128+ _record_origins (new_value , full_path , patch_name )
101129 return type (base )(** result )
102130
103131 if isinstance (base , list ):
132+ # We treat list appends differently, no "conflict" per se, just an addition
104133 return base + new
105134
106135 if isinstance (base , dict ):
107136 result = dict ()
108137 for key , base_value in base .items ():
138+ full_path = f"{ config_option } .{ key } " if config_option else key
109139 if key in new :
110140 new_value = new [key ]
111141 result [key ] = _recursive_update (
112142 base_value ,
113143 new_value ,
114- f" { config_option } . { key } " if config_option else key ,
144+ full_path ,
115145 )
116146 else :
117147 result [key ] = base_value
118148 for new_key , new_value in new .items ():
119149 if new_key not in base :
150+ full_path = f"{ config_option } .{ new_key } " if config_option else new_key
120151 result [new_key ] = new_value
152+ _record_origins (new_value , full_path , patch_name )
121153 return result
122154
123155 if base == new :
124156 return base
125157
126- base_str = yaml .dump (base ).strip ().removesuffix ("..." ).strip ()
127- new_str = yaml .dump (new ).strip ().removesuffix ("..." ).strip ()
128- change_str = (
129- f"\n ```\n { base_str } \n ```↓\n ```\n { new_str } \n ```"
130- if "\n " in base_str + new_str
131- else f"`{ base_str } ` → `{ new_str } `"
132- )
133- logger .warning (f"patch conflict: { config_option } : { change_str } " )
158+ # --> WE HAVE A CONFLICT <--
159+ previous_source = origin_map .get (config_option , "base_config" )
160+
161+ # Clean up long paths to just the filenames
162+ prev_file = basename (previous_source )
163+ new_file = basename (patch_name )
164+
165+ # Strip out Pydantic '.root' noise from the config key
166+ clean_option = config_option .replace (".root" , "" )
167+
168+ if verbose :
169+ base_str = yaml .dump (base ).strip ().removesuffix ("..." ).strip ()
170+ new_str = yaml .dump (new ).strip ().removesuffix ("..." ).strip ()
171+ change_str = (
172+ f"\n ```\n { base_str } \n ```↓\n ```\n { new_str } \n ```"
173+ if "\n " in base_str + new_str
174+ else f"`{ base_str } ` → `{ new_str } `"
175+ )
176+
177+ # Use a much tighter logging format
178+ logger .info (
179+ f"conflict: { clean_option } : { change_str } ({ prev_file } -> { new_file } )"
180+ )
134181
182+ # Claim ownership of the newly overwritten key
183+ origin_map [config_option ] = patch_name
135184 return new
136185
137- return _recursive_update (base_config , patch , None )
186+ return _recursive_update (base_config , patch , "" )
138187
139188
140189class PathHighlightingFormatter (coloredlogs .ColoredFormatter ):
0 commit comments