"""
Interface to the config_component.xml files. This class inherits from EntryID.py
"""
from CIME.XML.standard_module_setup import *
from CIME.XML.entry_id import EntryID
from CIME.XML.files import Files
from CIME.utils import get_cime_root
logger = logging.getLogger(__name__)
[docs]
class Component(EntryID):
def __init__(self, infile, comp_class):
"""
initialize a Component obect from the component xml file in infile
associate the component class with comp_class if provided.
"""
self._comp_class = comp_class
if infile == "testingonly":
self.filename = infile
return
files = Files()
schema = None
EntryID.__init__(self, infile)
schema = files.get_schema(
"CONFIG_{}_FILE".format(comp_class),
attributes={"version": "{}".format(self.get_version())},
)
if schema is not None:
self.validate_xml_file(infile, schema)
# pylint: disable=arguments-differ
[docs]
def get_value(self, name, attribute=None, resolved=False, subgroup=None):
expect(subgroup is None, "This class does not support subgroups")
return EntryID.get_value(self, name, attribute, resolved)
[docs]
def get_valid_model_components(self):
"""
return a list of all possible valid generic (e.g. atm, clm, ...) model components
from the entries in the model CONFIG_CPL_FILE
"""
components = []
comps_node = self.get_child("entry", {"id": "COMP_CLASSES"})
comps = self.get_default_value(comps_node)
components = comps.split(",")
return components
def _get_value_match(self, node, attributes=None, exact_match=False):
"""
return the best match for the node <values> entries
Note that a component object uses a different matching algorithm than an entryid object
For a component object the _get_value_match used is below and is not the one in entry_id.py
"""
match_value = None
match_max = 0
match_count = 0
match_values = []
expect(not exact_match, " exact_match not implemented in this method")
expect(node is not None, " Empty node in _get_value_match")
values = self.get_optional_child("values", root=node)
if values is None:
return
# determine match_type if there is a tie
# ASSUME a default of "last" if "match" attribute is not there
match_type = self.get(values, "match", default="last")
# use the default_value if present
val_node = self.get_optional_child("default_value", root=node)
if val_node is None:
logger.debug("No default_value for {}".format(self.get(node, "id")))
return val_node
value = self.text(val_node)
if value is not None and len(value) > 0 and value != "UNSET":
match_values.append(value)
for valnode in self.get_children("value", root=values):
# loop through all the keys in valnode (value nodes) attributes
for key, value in self.attrib(valnode).items():
# determine if key is in attributes dictionary
match_count = 0
if attributes is not None and key in attributes:
if re.search(value, attributes[key]):
logger.debug(
"Value {} and key {} match with value {}".format(
value, key, attributes[key]
)
)
match_count += 1
else:
match_count = 0
break
# a match is found
if match_count > 0:
# append the current result
if self.get(values, "modifier") == "additive":
match_values.append(self.text(valnode))
# replace the current result if it already contains the new value
# otherwise append the current result
elif self.get(values, "modifier") == "merge":
if self.text(valnode) in match_values:
del match_values[:]
match_values.append(self.text(valnode))
else:
if match_type == "last":
# take the *last* best match
if match_count >= match_max:
del match_values[:]
match_max = match_count
match_value = self.text(valnode)
elif match_type == "first":
# take the *first* best match
if match_count > match_max:
del match_values[:]
match_max = match_count
match_value = self.text(valnode)
else:
expect(
False,
"match attribute can only have a value of 'last' or 'first'",
)
if len(match_values) > 0:
match_value = " ".join(match_values)
return match_value
# pylint: disable=arguments-differ
[docs]
def get_description(self, compsetname):
if self.get_version() == 3.0:
return self._get_description_v3(compsetname, self._comp_class)
else:
return self._get_description_v2(compsetname)
[docs]
def get_forcing_description(self, compsetname):
if self.get_version() == 3.0:
return self._get_description_v3(compsetname, "forcing")
else:
return ""
def _get_description_v3(self, compsetname, comp_class):
"""
version 3 of the config_component.xml file has the description section at the top of the file
the description field has one attribute 'modifier_mode' which has allowed values
'*' 0 or more modifiers (default)
'1' exactly 1 modifier
'?' 0 or 1 modifiers
'+' 1 or more modifiers
modifiers are fields in the component section of the compsetname following the % symbol.
The desc field can have an attribute which is the component class ('cpl', 'atm', 'lnd' etc)
or it can have an attribute 'option' which provides descriptions of each optional modifier
or (in the config_component_{model}.xml in the driver only) it can have the attribute 'forcing'
component descriptions are matched to the compsetname using a set method
"""
expect(
comp_class is not None, "comp_class argument required for version3 files"
)
comp_class = comp_class.lower()
rootnode = self.get_child("description")
desc = ""
desc_nodes = self.get_children("desc", root=rootnode)
modifier_mode = self.get(rootnode, "modifier_mode")
if modifier_mode is None:
modifier_mode = "*"
expect(
modifier_mode in ("*", "1", "?", "+"),
"Invalid modifier_mode {} in file {}".format(modifier_mode, self.filename),
)
optiondesc = {}
if comp_class == "forcing":
for node in desc_nodes:
forcing = self.get(node, "forcing")
if forcing is not None and compsetname.startswith(forcing + "_"):
expect(
len(desc) == 0,
"Too many matches on forcing field {} in file {}".format(
forcing, self.filename
),
)
desc = self.text(node)
if desc is None:
desc = compsetname.split("_")[0]
return desc
# first pass just make a hash of the option descriptions
for node in desc_nodes:
option = self.get(node, "option")
if option is not None:
optiondesc[option] = self.text(node)
# second pass find a comp_class match
desc = ""
for node in desc_nodes:
compdesc = self.get(node, comp_class)
if compdesc is not None:
opt_parts = [x.rstrip("]") for x in compdesc.split("[%")]
parts = opt_parts.pop(0).split("%")
reqset = set(parts)
fullset = set(parts + opt_parts)
match, complist = self._get_description_match(
compsetname, reqset, fullset, modifier_mode
)
if match:
desc = self.text(node)
for opt in complist:
if opt in optiondesc:
desc += optiondesc[opt]
# cpl and esp components may not have a description
if comp_class not in ["cpl", "esp"]:
expect(
len(desc) > 0,
"No description found for comp_class {} matching compsetname {} in file {}, expected match in {} % {}".format(
comp_class,
compsetname,
self.filename,
list(reqset),
list(opt_parts),
),
)
return desc
def _get_description_match(self, compsetname, reqset, fullset, modifier_mode):
"""
>>> obj = Component('testingonly', 'ATM')
>>> obj._get_description_match("1850_DATM%CRU_FRED",set(["DATM"]), set(["DATM","CRU","HSI"]), "*")
(True, ['DATM', 'CRU'])
>>> obj._get_description_match("1850_DATM%FRED_Barn",set(["DATM"]), set(["DATM","CRU","HSI"]), "*")
(False, None)
>>> obj._get_description_match("1850_DATM_Barn",set(["DATM"]), set(["DATM","CRU","HSI"]), "?")
(True, ['DATM'])
>>> obj._get_description_match("1850_DATM_Barn",set(["DATM"]), set(["DATM","CRU","HSI"]), "1") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
CIMEError: ERROR: Expected exactly one modifer found 0 in ['DATM']
>>> obj._get_description_match("1850_DATM%CRU%HSI_Barn",set(["DATM"]), set(["DATM","CRU","HSI"]), "1") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
CIMEError: ERROR: Expected exactly one modifer found 2 in ['DATM', 'CRU', 'HSI']
>>> obj._get_description_match("1850_CAM50%WCCM%RCO2_Barn",set(["CAM50", "WCCM"]), set(["CAM50","WCCM","RCO2"]), "*")
(True, ['CAM50', 'WCCM', 'RCO2'])
# The following is not allowed because the required WCCM field is missing
>>> obj._get_description_match("1850_CAM50%RCO2_Barn",set(["CAM50", "WCCM"]), set(["CAM50","WCCM","RCO2"]), "*")
(False, None)
>>> obj._get_description_match("1850_CAM50_Barn",set(["CAM50", "WCCM"]), set(["CAM50","WCCM","RCO2"]), "+")
(False, None)
>>> obj._get_description_match("1850_CAM50%WCCM_Barn",set(["CAM50", "WCCM"]), set(["CAM50","WCCM","RCO2"]), "+")
(True, ['CAM50', 'WCCM'])
>>> obj._get_description_match("scn:1850_atm:CAM50%WCCM_Barn",set(["CAM50", "WCCM"]), set(["CAM50","WCCM","RCO2"]), "+")
(True, ['CAM50', 'WCCM'])
"""
match = False
comparts = compsetname.split("_")
matchcomplist = None
for comp in comparts:
if ":" in comp:
comp = comp.split(":")[1]
complist = comp.split("%")
cset = set(complist)
if cset == reqset or (cset > reqset and cset <= fullset):
if modifier_mode == "1":
expect(
len(complist) == 2,
"Expected exactly one modifer found {} in {}".format(
len(complist) - 1, complist
),
)
elif modifier_mode == "+":
expect(
len(complist) >= 2,
"Expected one or more modifers found {} in {}".format(
len(complist) - 1, list(reqset)
),
)
elif modifier_mode == "?":
expect(
len(complist) <= 2,
"Expected 0 or one modifers found {} in {}".format(
len(complist) - 1, complist
),
)
expect(
not match,
"Found multiple matches in file {} for {}".format(
self.filename, comp
),
)
match = True
matchcomplist = complist
# found a match
return match, matchcomplist
def _get_description_v2(self, compsetname):
rootnode = self.get_child("description")
desc = ""
desc_nodes = self.get_children("desc", root=rootnode)
for node in desc_nodes:
compsetmatch = self.get(node, "compset")
if compsetmatch is not None and re.search(compsetmatch, compsetname):
desc += self.text(node)
return desc
[docs]
def print_values(self):
"""
print values for help and description in target config_component.xml file
"""
helpnode = self.get_child("help")
helptext = self.text(helpnode)
logger.info(" {}".format(helptext))
entries = self.get_children("entry")
for entry in entries:
name = self.get(entry, "id")
text = self.text(self.get_child("desc", root=entry))
logger.info(" {:20s} : {}".format(name, text))
[docs]
def return_values(self):
"""
return a list of hashes from target config_component.xml file
This routine is used by external tools in https://github.com/NCAR/CESM_xml2html
"""
entry_dict = dict()
items = list()
helpnode = self.get_optional_child("help")
if helpnode:
helptext = self.text(helpnode)
else:
helptext = ""
entries = self.get_children("entry")
for entry in entries:
item = dict()
name = self.get(entry, "id")
datatype = self.text(self.get_child("type", root=entry))
valid_values = self.get_valid_values(name)
default_value = self.get_default_value(node=entry)
group = self.text(self.get_child("group", root=entry))
filename = self.text(self.get_child("file", root=entry))
text = self.text(self.get_child("desc", root=entry))
item = {
"name": name,
"datatype": datatype,
"valid_values": valid_values,
"value": default_value,
"group": group,
"filename": filename,
"desc": text.encode("utf-8"),
}
items.append(item)
entry_dict = {"items": items}
return helptext, entry_dict