1414import re
1515import warnings
1616from collections .abc import Sequence
17- from typing import Any
17+ from typing import Any , Iterator
1818
1919from monai .bundle .config_item import ConfigComponent , ConfigExpression , ConfigItem
2020from monai .bundle .utils import ID_REF_KEY , ID_SEP_KEY
@@ -101,7 +101,7 @@ def get_item(self, id: str, resolve: bool = False, **kwargs: Any) -> ConfigItem
101101 """
102102 if resolve and id not in self .resolved_content :
103103 self ._resolve_one_item (id = id , ** kwargs )
104- id = str ( id ). replace ( "#" , self .sep )
104+ id = self .normalize_id ( id )
105105 return self .items .get (id )
106106
107107 def _resolve_one_item (
@@ -122,7 +122,7 @@ def _resolve_one_item(
122122 if the `id` is not in the config content, must be a `ConfigItem` object.
123123
124124 """
125- id = str ( id ). replace ( "#" , self .sep )
125+ id = self .normalize_id ( id )
126126 if id in self .resolved_content :
127127 return self .resolved_content [id ]
128128 try :
@@ -192,6 +192,44 @@ def get_resolved_content(self, id: str, **kwargs: Any) -> ConfigExpression | str
192192 """
193193 return self ._resolve_one_item (id = id , ** kwargs )
194194
195+ @classmethod
196+ def normalize_id (cls , id : str | int ) -> str :
197+ """
198+ Normalize the id string to consistently use `cls.sep`.
199+
200+ Args:
201+ id: id string to be normalized.
202+
203+ """
204+ return str (id ).replace ("#" , cls .sep ) # backward compatibility `#` is the old separator
205+
206+ @classmethod
207+ def split_id (cls , id : str | int , last : bool = False ) -> list [str ]:
208+ """
209+ Split the id string into a tuple of strings.
210+
211+ Args:
212+ id: id string to be split.
213+ last: whether to split the rightmost part of the id. default is False (split all parts).
214+ """
215+ if not last :
216+ return cls .normalize_id (id ).split (cls .sep )
217+ res = cls .normalize_id (id ).rsplit (cls .sep , 1 )
218+ return ["" .join (res [:- 1 ]), res [- 1 ]]
219+
220+ @classmethod
221+ def iter_subconfigs (cls , id : str , config : Any ) -> Iterator [tuple [str , str , Any ]]:
222+ """
223+ Iterate over the sub-configs of the input config.
224+
225+ Args:
226+ id: id string of the current input config.
227+ config: input config to be iterated.
228+ """
229+ for k , v in config .items () if isinstance (config , dict ) else enumerate (config ):
230+ sub_id = f"{ id } { cls .sep } { k } " if id != "" else f"{ k } "
231+ yield k , sub_id , v
232+
195233 @classmethod
196234 def match_refs_pattern (cls , value : str ) -> dict [str , int ]:
197235 """
@@ -204,7 +242,7 @@ def match_refs_pattern(cls, value: str) -> dict[str, int]:
204242 """
205243 refs : dict [str , int ] = {}
206244 # regular expression pattern to match "@XXX" or "@XXX#YYY"
207- value = str ( value ). replace ( "#" , cls .sep )
245+ value = cls .normalize_id ( value )
208246 result = cls .id_matcher .findall (value )
209247 value_is_expr = ConfigExpression .is_expression (value )
210248 for item in result :
@@ -227,7 +265,7 @@ def update_refs_pattern(cls, value: str, refs: dict) -> str:
227265
228266 """
229267 # regular expression pattern to match "@XXX" or "@XXX#YYY"
230- value = str ( value ). replace ( "#" , cls .sep )
268+ value = cls .normalize_id ( value )
231269 result = cls .id_matcher .findall (value )
232270 # reversely sort the matched references by length
233271 # and handle the longer first in case a reference item is substring of another longer item
@@ -273,8 +311,7 @@ def find_refs_in_config(cls, config: Any, id: str, refs: dict[str, int] | None =
273311 refs_ [id ] = refs_ .get (id , 0 ) + count
274312 if not isinstance (config , (list , dict )):
275313 return refs_
276- for k , v in config .items () if isinstance (config , dict ) else enumerate (config ):
277- sub_id = f"{ id } { cls .sep } { k } " if id != "" else f"{ k } "
314+ for _ , sub_id , v in cls .iter_subconfigs (id , config ):
278315 if ConfigComponent .is_instantiable (v ) or ConfigExpression .is_expression (v ) and sub_id not in refs_ :
279316 refs_ [sub_id ] = 1
280317 refs_ = cls .find_refs_in_config (v , sub_id , refs_ )
@@ -298,8 +335,7 @@ def update_config_with_refs(cls, config: Any, id: str, refs: dict | None = None)
298335 if not isinstance (config , (list , dict )):
299336 return config
300337 ret = type (config )()
301- for idx , v in config .items () if isinstance (config , dict ) else enumerate (config ):
302- sub_id = f"{ id } { cls .sep } { idx } " if id != "" else f"{ idx } "
338+ for idx , sub_id , v in cls .iter_subconfigs (id , config ):
303339 if ConfigComponent .is_instantiable (v ) or ConfigExpression .is_expression (v ):
304340 updated = refs_ [sub_id ]
305341 if ConfigComponent .is_instantiable (v ) and updated is None :
0 commit comments