Skip to content

Commit

Permalink
nonbreaking updates
Browse files Browse the repository at this point in the history
Signed-off-by: Wenqi Li <wenqil@nvidia.com>
  • Loading branch information
wyli committed Feb 18, 2022
1 parent 7847855 commit bbe6428
Showing 1 changed file with 50 additions and 65 deletions.
115 changes: 50 additions & 65 deletions monai/apps/manifest/reference_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import re
import warnings
from typing import Any, Dict, List, Optional, Sequence, Set, Union
from typing import Any, Dict, Optional, Sequence, Set

from monai.apps.manifest.config_item import ConfigComponent, ConfigExpression, ConfigItem

Expand All @@ -27,36 +27,36 @@ class ReferenceResolver:
the reference string may also contain a ``#`` character to refer to a substructure by
key indexing for a dictionary or integer indexing for a list.
A typical workflow of resolving references is as follows:
In this class, resolving references is essentially substitution of the reference strings with the
corresponding python objects. A typical workflow of resolving references is as follows:
- Add multiple ``ConfigItem`` objects to the ``ReferenceResolver`` by ``add_item()``.
- Call ``resolve()`` to automatically resolve the references. This is done recursively by:
- Call ``get_resolved_content()`` to automatically resolve the references. This is done (recursively) by:
- Convert the items to objects, for those do not have references to other items.
- If this it is instantiable, instantiate it and cache the class instance in ``resolved_content``.
- If this it is an expression, evaluate it and save the value in ``resolved_content``.
- Replace the reference strings with the actual objects.
- Call ``get_resolved_content()`` to get the items with reference strings replaced by the corresponding objects.
- If it is instantiable, instantiate it and cache the class instance in ``resolved_content``.
- If it is an expression, evaluate it and save the value in ``resolved_content``.
- Substitute the reference strings with the corresponding objects.
Args:
items: ``ConfigItem``s to resolve, this could be added later with ``add_item()``.
"""

def __init__(self, items: Optional[Sequence[ConfigItem]] = None):
# save the items in a dictionary with the `id` as key
# save the items in a dictionary with the `ConfigItem.id` as key
self.items = {} if items is None else {i.get_id(): i for i in items}
self.resolved_content: Dict[str, Any] = {}

def add_item(self, item: ConfigItem):
"""
Add a config item to the resolver.
Add a ``ConfigItem`` to the resolver.
Args:
item: a config item to resolve.
item: a ``ConfigItem``.
"""
id = item.get_id()
if len(id) == 0:
if id == "":
raise ValueError("id should not be empty when resolving reference.")
if id in self.items:
warnings.warn(f"id '{id}' is already added.")
Expand All @@ -67,8 +67,8 @@ def get_item(self, id: str, resolve: bool = False):
"""
Get the ``ConfigItem`` by id.
If `resolve=True` and the returned item will be resolved, that is,
all the reference strings are replaced by the corresponding ``ConfigItem`` objects.
If ``resolve=True``, the returned item will be resolved, that is,
all the reference strings are substituted by the corresponding ``ConfigItem`` objects.
Args:
id: id of the expected config item.
Expand All @@ -81,13 +81,13 @@ def get_item(self, id: str, resolve: bool = False):

def _resolve_one_item(self, id: str, waiting_list: Optional[Set[str]] = None):
"""
Resolve one config item with specified id name, save the resolved result in `resolved_content`.
Resolve one ``ConfigItem`` of ``id``, cache the resolved result in ``resolved_content``.
If it has unresolved references, recursively resolve the referring items first.
Args:
id: id name of expected config item to resolve.
waiting_list: list of the ids of items wait to resolve references.
it's used to detect circular references when resolving references like:
id: id name of ``ConfigItem`` to be resolved.
waiting_list: set of ids pending to be resolved.
It's used to detect circular references such as:
`{"name": "A", "dep": "@B"}` and `{"name": "B", "dep": "@A"}`.
"""
Expand All @@ -105,7 +105,7 @@ def _resolve_one_item(self, id: str, waiting_list: Optional[Set[str]] = None):
if d in waiting_list:
raise ValueError(f"detected circular references for id='{d}' in the config content.")

if len(ref_ids) > 0:
if ref_ids:
# # check whether the component has any unresolved references
for ref_id in ref_ids:
if ref_id not in self.resolved_content:
Expand All @@ -128,8 +128,7 @@ def _resolve_one_item(self, id: str, waiting_list: Optional[Set[str]] = None):

def get_resolved_content(self, id: str):
"""
Get the resolved content with specified id name.
If not resolved yet, try to resolve it first.
Get the resolved ``ConfigItem`` by id. If there are unresolved references, try to resolve them first.
Args:
id: id name of the expected item.
Expand All @@ -140,31 +139,29 @@ def get_resolved_content(self, id: str):
return self.resolved_content[id]

@staticmethod
def match_refs_pattern(value: str) -> List[str]:
def match_refs_pattern(value: str) -> Set[str]:
"""
Match regular expression for the input string to find the references.
The reference part starts with "@", like: "@XXX#YYY#ZZZ".
The reference string starts with ``"@"``, like: ``"@XXX#YYY#ZZZ"``.
Args:
value: input value to match regular expression.
"""
refs: List[str] = []
refs: Set[str] = set()
# regular expression pattern to match "@XXX" or "@XXX#YYY"
result = re.compile(r"@\w*[\#\w]*").findall(value)
for item in result:
if ConfigExpression.is_expression(value) or value == item:
# only check when string starts with "$" or the whole content is "@XXX"
ref_obj_id = item[1:]
if ref_obj_id not in refs:
refs.append(ref_obj_id)
refs.add(item[1:])
return refs

@staticmethod
def update_refs_pattern(value: str, refs: Dict) -> str:
"""
Match regular expression for the input string to update content with the references.
The reference part starts with "@", like: "@XXX#YYY#ZZZ".
The reference part starts with ``"@"``, like: ``"@XXX#YYY#ZZZ"``.
References dictionary must contain the referring IDs as keys.
Args:
Expand All @@ -178,7 +175,6 @@ def update_refs_pattern(value: str, refs: Dict) -> str:
ref_id = item[1:]
if ref_id not in refs:
raise KeyError(f"can not find expected ID '{ref_id}' in the references.")

if ConfigExpression.is_expression(value):
# replace with local code, will be used in the `evaluate` logic with `locals={"refs": ...}`
value = value.replace(item, f"refs['{ref_id}']")
Expand All @@ -188,10 +184,10 @@ def update_refs_pattern(value: str, refs: Dict) -> str:
return value

@staticmethod
def find_refs_in_config(config: Union[Dict, List, str], id: str, refs: Optional[List[str]] = None) -> List[str]:
def find_refs_in_config(config, id: str, refs: Optional[Set[str]] = None) -> Set[str]:
"""
Recursively search all the content of input config item to get the ids of references.
References mean: the IDs of other config items used as "@XXX" in this config item, or the
References mean: the IDs of other config items (``"@XXX"`` in this config item), or the
sub-item in the config is `instantiable`, or the sub-item in the config is `expression`.
For `dict` and `list`, recursively check the sub-items.
Expand All @@ -201,23 +197,22 @@ def find_refs_in_config(config: Union[Dict, List, str], id: str, refs: Optional[
refs: list of the ID name of found references, default to `None`.
"""
refs_: List[str] = [] if refs is None else refs
refs_: Set[str] = refs or set()
if isinstance(config, str):
refs_ += ReferenceResolver.match_refs_pattern(value=config)

if isinstance(config, (list, dict)):
subs = enumerate(config) if isinstance(config, list) else config.items()
for k, v in subs:
sub_id = f"{id}#{k}" if len(id) > 0 else f"{k}"
if ConfigComponent.is_instantiable(v) or ConfigExpression.is_expression(v):
refs_.append(sub_id)
refs_ = ReferenceResolver.find_refs_in_config(v, sub_id, refs_)
return refs_.union(ReferenceResolver.match_refs_pattern(value=config))
if not isinstance(config, (list, dict)):
return refs_
for k, v in config.items() if isinstance(config, dict) else enumerate(config):
sub_id = f"{id}#{k}" if id != "" else f"{k}"
if ConfigComponent.is_instantiable(v) or ConfigExpression.is_expression(v):
refs_.add(sub_id)
refs_ = ReferenceResolver.find_refs_in_config(v, sub_id, refs_)
return refs_

@staticmethod
def update_config_with_refs(config: Union[Dict, List, str], id: str, refs: Optional[Dict] = None):
def update_config_with_refs(config, id: str, refs: Optional[Dict] = None):
"""
With all the references in `refs`, update the input config content with references
With all the references in ``refs``, update the input config content with references
and return the new config.
Args:
Expand All @@ -226,27 +221,17 @@ def update_config_with_refs(config: Union[Dict, List, str], id: str, refs: Optio
refs: all the referring content with ids, default to `None`.
"""
refs_: Dict = {} if refs is None else refs
refs_: Dict = refs or {}
if isinstance(config, str):
config = ReferenceResolver.update_refs_pattern(config, refs_)
if isinstance(config, list):
# all the items in the list should be replaced with the references
ret_list: List = []
for i, v in enumerate(config):
sub_id = f"{id}#{i}" if len(id) > 0 else f"{i}"
if ConfigComponent.is_instantiable(v) or ConfigExpression.is_expression(v):
ret_list.append(refs_[sub_id])
else:
ret_list.append(ReferenceResolver.update_config_with_refs(v, sub_id, refs_))
return ret_list
if isinstance(config, dict):
# all the items in the dict should be replaced with the references
ret_dict: Dict = {}
for k, v in config.items():
sub_id = f"{id}#{k}" if len(id) > 0 else f"{k}"
if ConfigComponent.is_instantiable(v) or ConfigExpression.is_expression(v):
ret_dict[k] = refs_[sub_id]
else:
ret_dict[k] = ReferenceResolver.update_config_with_refs(v, sub_id, refs_)
return ret_dict
return config
return ReferenceResolver.update_refs_pattern(config, refs_)
if not isinstance(config, (list, dict)):
return config
ret = type(config)()
for idx, v in config.items() if isinstance(config, dict) else enumerate(config):
sub_id = f"{id}#{idx}" if id != "" else f"{idx}"
if ConfigComponent.is_instantiable(v) or ConfigExpression.is_expression(v):
updated = ReferenceResolver.update_config_with_refs(v, sub_id, refs_)
else:
updated = ReferenceResolver.update_config_with_refs(v, sub_id, refs_)
ret.update({idx: updated}) if isinstance(ret, dict) else ret.append(updated)
return ret

0 comments on commit bbe6428

Please sign in to comment.