Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 80 additions & 23 deletions CIME/XML/namelist_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,44 +89,80 @@ def __init__(self, infile, files=None):
super(NamelistDefinition, self).__init__(infile, schema=schema)

self._attributes = {}
self._entry_nodes = []
self._entry_ids = []
Comment on lines -92 to -93
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I have removed _entry_nodes from NamelistDefinition: this wasn't being used, and would be problematic now that there could be the same node with different names, since the name might not match the id.

I have also renamed _entry_ids to _var_names, since this no longer necessarily gives the "id" attribute but instead may give the rename.

self._var_names = []
self._valid_values = {}
self._entry_types = {}
self._group_names = CaseInsensitiveDict({})
self._nodes = {}

def set_node_values(self, name, node):
self._entry_nodes.append(node)
self._entry_ids.append(name)
self._nodes[name] = node
self._entry_types[name] = self._get_type(node)
self._valid_values[name] = self._get_valid_values(node)
self._group_names[name] = self.get_group_name(node)

def set_nodes(self, skip_groups=None):
def _set_node_values(self, names, node):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I have made this function private. I have changed its interface (changing the name argument to now be a list of names - which will often be a list of a single item, but could be more) and want to make sure it isn't being used externally (which, as far as I can tell, it isn't)… and it seems like it's not meant to be used externally.

# names is a list of names; typically it has a single item, the "id", but not
# always: in the case of a multi_variable_entry, it will be a list of all of the
# names mapped to by this entry
for name in names:
self._var_names.append(name)
self._nodes[name] = node
self._entry_types[name] = self._get_type(node)
self._valid_values[name] = self._get_valid_values(node)
self._group_names[name] = self.get_group_name(node)

def set_nodes(self, skip_groups=None, multi_variable_mappings=None):
"""
populates the object data types for all nodes that are not part of the skip_groups array
returns nodes that do not have attributes of `skip_default_entry` or `per_stream_entry`
Populates the object data types for all nodes that are not part of the skip_groups
array.

If multi_variable_mappings is provided, it should be a dictionary mapping ids in
the namelist definition file to a list of final names in the namelist. There must
be an entry in this dictionary for any namelist_definition entry that has the
multi_variable_entry attribute set to true. (The mapping can be an empty list for
a variable that will not have any appearances in the final namelist.)

Returns nodes that do not have attributes of `skip_default_entry`,
`per_stream_entry` or `multi_variable_entry`.
"""
if multi_variable_mappings is None:
multi_variable_mappings = {}
default_nodes = []
for node in self.get_children("entry"):
name = self.get(node, "id")
this_id = self.get(node, "id")
skip_default_entry = self.get(node, "skip_default_entry") == "true"
per_stream_entry = self.get(node, "per_stream_entry") == "true"
multi_variable_entry = self.get(node, "multi_variable_entry") == "true"
self._check_multi_variable_validity(
this_id, multi_variable_mappings, multi_variable_entry, per_stream_entry
)

# The reason we need add_default False for a multi_variable_entry is that the
# returned default_nodes are just the nodes themselves, without the mappings
# in multi_variable_mappings - so users of these default_nodes examine their
# ids, which is problematic for a multi_variable_entry. The implication is
# that add_default will need to be called explicitly for the final names in a
# multi_variable_entry.
add_default = (
not skip_default_entry
and not per_stream_entry
and not multi_variable_entry
)
Comment on lines +135 to +145
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I don't return variables with multi_variable_entry in the list of defaults because use of these default nodes relies on the id matching the name. My original hope was that I could change the id in the node, but my sense from reading some code is that any modification of nodes assumes you want to write back to their originating file. If I'm wrong about that and it's actually possible to modify the nodes in memory without modifying the original xml file, I'd be happy to hear about that. Without that ability, I think returning these in the default list would require a change to how this default list is encoded and used, such as making it a dictionary mapping a name to a node, but it felt like that would be a pretty invasive change.

At least for my purposes, it's not a big deal that these variables don't appear in the default list: We need to know all of the names in buildnml anyway in order to add them to the multi_variable_mappings argument, so it's easy enough to loop through them and explicitly call add_default on each of them.


if multi_variable_entry:
# Note that we have already confirmed that id is in
# multi_variable_mappings via _check_multi_variable_validity
names = multi_variable_mappings[this_id]
else:
names = [this_id]

if skip_groups:
group_name = self.get_group_name(node)

if not group_name in skip_groups:
self.set_node_values(name, node)
self._set_node_values(names, node)

if not skip_default_entry and not per_stream_entry:
if add_default:
default_nodes.append(node)
else:
self.set_node_values(name, node)
self._set_node_values(names, node)

if not skip_default_entry and not per_stream_entry:
if add_default:
default_nodes.append(node)

return default_nodes
Expand Down Expand Up @@ -160,6 +196,30 @@ def _get_valid_values(self, node):
valid_values = valid_values.split(",")
return valid_values

@staticmethod
def _check_multi_variable_validity(
this_id, multi_variable_mappings, multi_variable_entry, per_stream_entry
):
if multi_variable_entry:
# We could probably make this combination work, but currently it probably
# won't work correctly because get_per_stream_entries has
# `entries.append(self.get(node, "id"))` rather than taking into account the
# mapping of a single id to multiple names in the final namelist.
expect(
not per_stream_entry,
f"Cannot have both multi_variable_entry and per_stream_entry for {this_id}",
)
expect(
this_id in multi_variable_mappings,
f"{this_id} has the multi_variable_entry attribute but does not appear in multi_variable_mappings",
)

if this_id in multi_variable_mappings:
expect(
multi_variable_entry,
f"{this_id} appears in multi_variable_mappings but does not have the multi_variable_entry attribute",
)

def get_group(self, name):
return self._group_names[name]

Expand All @@ -175,9 +235,6 @@ def get_attributes(self):
"""Return this object's attributes dictionary"""
return self._attributes

def get_entry_nodes(self):
return self._entry_nodes

def get_per_stream_entries(self):
entries = []
nodes = self.get_children("entry")
Expand Down Expand Up @@ -386,15 +443,15 @@ def _expect_variable_in_definition(self, name, variable_template):
case insensitve match"""

expect(
name in self._entry_ids,
name in self._var_names,
(variable_template + " is not in the namelist definition.").format(
str(name)
),
)

def _user_modifiable_in_variable_definition(self, name):
# Is name user modifiable?
node = self.get_optional_child("entry", attributes={"id": name})
node = self._nodes[name]
user_modifiable_only_by_xml = self.get(node, "modify_via_xml")
if user_modifiable_only_by_xml is not None:
expect(
Expand Down
2 changes: 2 additions & 0 deletions CIME/data/config/xml_schemas/entry_id_namelist.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<xs:attribute name="modify_via_xml" type="xs:string"/>
<xs:attribute name="skip_default_entry" type="xs:boolean"/>
<xs:attribute name="per_stream_entry" type="xs:boolean"/>
<xs:attribute name="multi_variable_entry" type="xs:boolean"/>

<!-- simple elements -->
<xs:element name="type" type="xs:string"/>
Expand All @@ -32,6 +33,7 @@
<xs:attribute ref="modify_via_xml"/>
<xs:attribute ref="skip_default_entry"/>
<xs:attribute ref="per_stream_entry"/>
<xs:attribute ref="multi_variable_entry"/>
</xs:complexType>
</xs:element>

Expand Down
24 changes: 19 additions & 5 deletions CIME/nmlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __init__(self, case, definition_files, files=None):
self._namelist = Namelist()

# entries for which we should potentially call add_default (variables that do not
# set skip_default_entry)
# set skip_default_entry, per_stream_entry or multi_variable_entry)
self._default_nodes = []

# Define __enter__ and __exit__ so that we can use this as a context manager
Expand All @@ -117,8 +117,11 @@ def init_defaults(
skip_entry_loop=False,
skip_default_for_groups=None,
set_group_name=None,
multi_variable_mappings=None,
):
"""Return array of names of all definition nodes
"""
Return array of names of all definition nodes for which defaults are automatically
added (excluding nodes that don't have defaults automatically added)

infiles should be a list of file paths, each one giving namelist settings that
take precedence over the default values. Often there will be only one file in this
Expand All @@ -129,6 +132,15 @@ def init_defaults(
groups. This is often paired with later conditional calls to
add_defaults_for_group.

If multi_variable_mappings is provided, it should be a dictionary mapping ids in
the namelist definition file to a list of final names in the namelist. There must
be an entry in this dictionary for any namelist_definition entry that has the
multi_variable_entry attribute set to true. (The mapping can be an empty list for
a variable that will not have any appearances in the final namelist.) Note that
any variable in this list will *not* appear in the list of names for which
defaults are automatically added; instead, add_default will need to be called
explicitly for each of the final names.

"""
if skip_default_for_groups is None:
skip_default_for_groups = []
Expand All @@ -137,7 +149,9 @@ def init_defaults(
self.new_instance()

# Determine the array of entry nodes that will be acted upon
self._default_nodes = self._definition.set_nodes(skip_groups=skip_groups)
self._default_nodes = self._definition.set_nodes(
skip_groups=skip_groups, multi_variable_mappings=multi_variable_mappings
)

# Add attributes to definition object
self._definition.add_attributes(config)
Expand Down Expand Up @@ -176,8 +190,8 @@ def rename_group(self, group, newgroup):
def add_defaults_for_group(self, group):
"""Call add_default for namelist variables in the given group

This still skips variables that have attributes of skip_default_entry or
per_stream_entry.
This still skips variables that have attributes of skip_default_entry,
per_stream_entry or multi_variable_entry.

This must be called after init_defaults. It is often paired with use of
skip_default_for_groups in the init_defaults call.
Expand Down
85 changes: 77 additions & 8 deletions CIME/tests/test_unit_xml_namelist_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class TestXMLNamelistDefinition(unittest.TestCase):
def test_set_nodes(self):
def test_set_nodes_basic(self):
test_data = """<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="http://www.cgd.ucar.edu/~cam/namelist/namelist_definition.xsl"?>

Expand All @@ -17,9 +17,78 @@ def test_set_nodes(self):
<category>test</category>
</entry>
<entry id="test2">
<type>real</type>
<category>test</category>
</entry>
</entry_id>"""

with tempfile.NamedTemporaryFile() as temp:
temp.write(test_data.encode())
temp.flush()

nmldef = NamelistDefinition(temp.name)

nmldef.set_nodes()

assert nmldef._var_names == ["test1", "test2"]
assert nmldef._nodes.keys() == {"test1", "test2"}
assert nmldef._entry_types == {"test1": "char", "test2": "real"}
assert nmldef._valid_values == {"test1": None, "test2": None}
assert nmldef._group_names == {"test1": None, "test2": None}

def test_set_nodes_multi_variable_mappings_two(self):
"""
Test set_nodes with an entry in multi_variable_mappings that maps to two final variables
"""
test_data = """<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="http://www.cgd.ucar.edu/~cam/namelist/namelist_definition.xsl"?>

<entry_id version="2.0">
<entry id="test1">
<type>char</type>
<category>test</category>
</entry>
<entry id="test2" multi_variable_entry="true">
<type>real</type>
<category>test</category>
</entry>
</entry_id>"""

with tempfile.NamedTemporaryFile() as temp:
temp.write(test_data.encode())
temp.flush()

nmldef = NamelistDefinition(temp.name)

multi_variable_mappings = {"test2": ["test2_a", "test2_b"]}
nmldef.set_nodes(multi_variable_mappings=multi_variable_mappings)

assert nmldef._var_names == ["test1", "test2_a", "test2_b"]
assert nmldef._nodes.keys() == {"test1", "test2_a", "test2_b"}
assert nmldef._entry_types == {
"test1": "char",
"test2_a": "real",
"test2_b": "real",
}
assert nmldef._valid_values == {"test1": None, "test2_a": None, "test2_b": None}
assert nmldef._group_names == {"test1": None, "test2_a": None, "test2_b": None}

def test_set_nodes_multi_variable_mappings_zero(self):
"""
Test set_nodes with an entry in multi_variable_mappings that maps to zero final variables
"""
test_data = """<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="http://www.cgd.ucar.edu/~cam/namelist/namelist_definition.xsl"?>

<entry_id version="2.0">
<entry id="test1">
<type>char</type>
<category>test</category>
</entry>
<entry id="test2" multi_variable_entry="true">
<type>real</type>
<category>test</category>
</entry>
</entry_id>"""

with tempfile.NamedTemporaryFile() as temp:
Expand All @@ -28,14 +97,14 @@ def test_set_nodes(self):

nmldef = NamelistDefinition(temp.name)

nmldef.set_nodes()
multi_variable_mappings = {"test2": []}
nmldef.set_nodes(multi_variable_mappings=multi_variable_mappings)

assert len(nmldef._entry_nodes) == 2
assert nmldef._entry_ids == ["test1", "test2"]
assert len(nmldef._nodes) == 2
assert nmldef._entry_types == {"test1": "char", "test2": "char"}
assert nmldef._valid_values == {"test1": None, "test2": None}
assert nmldef._group_names == {"test1": None, "test2": None}
assert nmldef._var_names == ["test1"]
assert nmldef._nodes.keys() == {"test1"}
assert nmldef._entry_types == {"test1": "char"}
assert nmldef._valid_values == {"test1": None}
assert nmldef._group_names == {"test1": None}


if __name__ == "__main__":
Expand Down
Loading