1717
1818from monai .bundle .config_item import ComponentLocator , ConfigComponent , ConfigExpression , ConfigItem
1919from monai .bundle .reference_resolver import ReferenceResolver
20- from monai .bundle .utils import ID_SEP_KEY , MACRO_KEY
20+ from monai .bundle .utils import ID_REF_KEY , ID_SEP_KEY , MACRO_KEY
2121from monai .config import PathLike
2222from monai .utils import ensure_tuple , look_up_option , optional_import
2323
@@ -87,6 +87,8 @@ class ConfigParser:
8787 suffixes = ("json" , "yaml" , "yml" )
8888 suffix_match = rf".*\.({ '|' .join (suffixes )} )"
8989 path_match = rf"({ suffix_match } $)"
90+ # match relative id names, e.g. "@#data", "@##transform#1"
91+ relative_id_prefix = re .compile (rf"(?:{ ID_REF_KEY } |{ MACRO_KEY } ){ ID_SEP_KEY } +" )
9092 meta_key = "_meta_" # field key to save metadata
9193
9294 def __init__ (
@@ -127,7 +129,7 @@ def __getitem__(self, id: Union[str, int]):
127129 if id == "" :
128130 return self .config
129131 config = self .config
130- for k in str (id ).split (self . ref_resolver . sep ):
132+ for k in str (id ).split (ID_SEP_KEY ):
131133 if not isinstance (config , (dict , list )):
132134 raise ValueError (f"config must be dict or list for key `{ k } `, but got { type (config )} : { config } ." )
133135 indexing = k if isinstance (config , dict ) else int (k )
@@ -151,9 +153,9 @@ def __setitem__(self, id: Union[str, int], config: Any):
151153 self .config = config
152154 self .ref_resolver .reset ()
153155 return
154- keys = str (id ).split (self . ref_resolver . sep )
156+ keys = str (id ).split (ID_SEP_KEY )
155157 # get the last parent level config item and replace it
156- last_id = self . ref_resolver . sep .join (keys [:- 1 ])
158+ last_id = ID_SEP_KEY .join (keys [:- 1 ])
157159 conf_ = self [last_id ]
158160 indexing = keys [- 1 ] if isinstance (conf_ , dict ) else int (keys [- 1 ])
159161 conf_ [indexing ] = config
@@ -192,7 +194,7 @@ def parse(self, reset: bool = True):
192194 """
193195 if reset :
194196 self .ref_resolver .reset ()
195- self .resolve_macro ()
197+ self .resolve_macro_and_relative_ids ()
196198 self ._do_parse (config = self .get ())
197199
198200 def get_parsed_content (self , id : str = "" , ** kwargs ):
@@ -251,28 +253,37 @@ def read_config(self, f: Union[PathLike, Sequence[PathLike], Dict], **kwargs):
251253 content .update (self .load_config_files (f , ** kwargs ))
252254 self .set (config = content )
253255
254- def _do_resolve (self , config : Any ):
256+ def _do_resolve (self , config : Any , id : str = "" ):
255257 """
256- Recursively resolve the config content to replace the macro tokens with target content.
258+ Recursively resolve `self.config` to replace the relative ids with absolute ids, for example,
259+ `@##A` means `A` in the upper level. and replace the macro tokens with target content,
257260 The macro tokens start with "%", can be from another structured file, like:
258- ``{"net": " %default_net"} ``, ``{"net": " %/data/config.json#net"} ``.
261+ ``" %default_net"``, ``" %/data/config.json#net"``.
259262
260263 Args:
261264 config: input config file to resolve.
265+ id: id of the ``ConfigItem``, ``"#"`` in id are interpreted as special characters to
266+ go one level further into the nested structures.
267+ Use digits indexing from "0" for list or other strings for dict.
268+ For example: ``"xform#5"``, ``"net#channels"``. ``""`` indicates the entire ``self.config``.
262269
263270 """
264271 if isinstance (config , (dict , list )):
265272 for k , v in enumerate (config ) if isinstance (config , list ) else config .items ():
266- config [k ] = self ._do_resolve (v )
267- if isinstance (config , str ) and config .startswith (MACRO_KEY ):
268- path , ids = ConfigParser .split_path_id (config [len (MACRO_KEY ) :])
269- parser = ConfigParser (config = self .get () if not path else ConfigParser .load_config_file (path ))
270- return self ._do_resolve (config = deepcopy (parser [ids ]))
273+ sub_id = f"{ id } { ID_SEP_KEY } { k } " if id != "" else k
274+ config [k ] = self ._do_resolve (v , sub_id )
275+ if isinstance (config , str ):
276+ config = self .resolve_relative_ids (id , config )
277+ if config .startswith (MACRO_KEY ):
278+ path , ids = ConfigParser .split_path_id (config [len (MACRO_KEY ) :])
279+ parser = ConfigParser (config = self .get () if not path else ConfigParser .load_config_file (path ))
280+ return self ._do_resolve (config = deepcopy (parser [ids ]))
271281 return config
272282
273- def resolve_macro (self ):
283+ def resolve_macro_and_relative_ids (self ):
274284 """
275- Recursively resolve `self.config` to replace the macro tokens with target content.
285+ Recursively resolve `self.config` to replace the relative ids with absolute ids, for example,
286+ `@##A` means `A` in the upper level. and replace the macro tokens with target content,
276287 The macro tokens are marked as starting with "%", can be from another structured file, like:
277288 ``"%default_net"``, ``"%/data/config.json#net"``.
278289
@@ -292,9 +303,8 @@ def _do_parse(self, config, id: str = ""):
292303
293304 """
294305 if isinstance (config , (dict , list )):
295- subs = enumerate (config ) if isinstance (config , list ) else config .items ()
296- for k , v in subs :
297- sub_id = f"{ id } { self .ref_resolver .sep } { k } " if id != "" else k
306+ for k , v in enumerate (config ) if isinstance (config , list ) else config .items ():
307+ sub_id = f"{ id } { ID_SEP_KEY } { k } " if id != "" else k
298308 self ._do_parse (config = v , id = sub_id )
299309
300310 # copy every config item to make them independent and add them to the resolver
@@ -380,3 +390,41 @@ def split_path_id(cls, src: str) -> Tuple[str, str]:
380390 path_name = result [0 ][0 ] # at most one path_name
381391 _ , ids = src .rsplit (path_name , 1 )
382392 return path_name , ids [len (ID_SEP_KEY ) :] if ids .startswith (ID_SEP_KEY ) else ""
393+
394+ @classmethod
395+ def resolve_relative_ids (cls , id : str , value : str ) -> str :
396+ """
397+ To simplify the reference or macro tokens ID in the nested config content, it's available to use
398+ relative ID name which starts with the `ID_SEP_KEY`, for example, "@#A" means `A` in the same level,
399+ `@##A` means `A` in the upper level.
400+ It resolves the relative ids to absolute ids. For example, if the input data is:
401+
402+ .. code-block:: python
403+
404+ {
405+ "A": 1,
406+ "B": {"key": "@##A", "value1": 2, "value2": "%#value1", "value3": [3, 4, "@#1"]},
407+ }
408+
409+ It will resolve `B` to `{"key": "@A", "value1": 2, "value2": "%B#value1", "value3": [3, 4, "@B#value3#1"]}`.
410+
411+ Args:
412+ id: id name for current config item to compute relative id.
413+ value: input value to resolve relative ids.
414+
415+ """
416+ # get the prefixes like: "@####", "%###", "@#"
417+ prefixes = sorted (set ().union (cls .relative_id_prefix .findall (value )), reverse = True )
418+ current_id = id .split (ID_SEP_KEY )
419+
420+ for p in prefixes :
421+ sym = ID_REF_KEY if ID_REF_KEY in p else MACRO_KEY
422+ length = p [len (sym ) :].count (ID_SEP_KEY )
423+ if length > len (current_id ):
424+ raise ValueError (f"the relative id in `{ value } ` is out of the range of config content." )
425+ if length == len (current_id ):
426+ new = "" # root id is `""`
427+ else :
428+ new = ID_SEP_KEY .join (current_id [:- length ]) + ID_SEP_KEY
429+ value = value .replace (p , sym + new )
430+ return value
0 commit comments