-
Notifications
You must be signed in to change notification settings - Fork 216
/
Copy pathnamelist_definition.py
448 lines (381 loc) · 18.7 KB
/
namelist_definition.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
"""Interface to `namelist_definition.xml`.
This module contains only one class, `NamelistDefinition`, inheriting from
`EntryID`.
"""
# Warnings we typically ignore.
# pylint:disable=invalid-name
# Disable warnings due to using `standard_module_setup`
# pylint:disable=wildcard-import,unused-wildcard-import
import re
import collections
from CIME.namelist import fortran_namelist_base_value, \
is_valid_fortran_namelist_literal, character_literal_to_string, \
expand_literal_list, Namelist, get_fortran_name_only
from CIME.XML.standard_module_setup import *
from CIME.XML.entry_id import EntryID
from CIME.XML.files import Files
logger = logging.getLogger(__name__)
_array_size_re = re.compile(r'^(?P<type>[^(]+)\((?P<size>[^)]+)\)$')
class CaseInsensitiveDict(dict):
"""Basic case insensitive dict with strings only keys.
From https://stackoverflow.com/a/27890005 """
proxy = {}
def __init__(self, data):
dict.__init__(self)
self.proxy = dict((k.lower(), k) for k in data)
for k in data:
self[k] = data[k]
def __contains__(self, k):
return k.lower() in self.proxy
def __delitem__(self, k):
key = self.proxy[k.lower()]
super(CaseInsensitiveDict, self).__delitem__(key)
del self.proxy[k.lower()]
def __getitem__(self, k):
key = self.proxy[k.lower()]
return super(CaseInsensitiveDict, self).__getitem__(key)
def get(self, k, default=None):
return self[k] if k in self else default
def __setitem__(self, k, v):
super(CaseInsensitiveDict, self).__setitem__(k, v)
self.proxy[k.lower()] = k
class NamelistDefinition(EntryID):
"""Class representing variable definitions for a namelist.
This class inherits from `EntryID`, and supports most inherited methods;
however, `set_value` is unsupported.
Additional public methods:
- dict_to_namelist.
- is_valid_value
- validate
"""
def __init__(self, infile, files=None):
"""Construct a `NamelistDefinition` from an XML file."""
# if the file is invalid we may not be able to check the version
# but we need to do it this way until we remove the version 1 files
schema = None
if files is None:
files = Files()
schema = files.get_schema("NAMELIST_DEFINITION_FILE")
expect(os.path.isfile(infile), "File {} does not exist".format(infile))
super(NamelistDefinition, self).__init__(infile, schema=schema)
self._attributes = {}
self._entry_nodes = []
self._entry_ids = []
self._valid_values = {}
self._entry_types = {}
self._group_names = CaseInsensitiveDict({})
self._nodes = {}
def set_nodes(self, skip_groups=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`
"""
default_nodes = []
for node in self.get_children("entry"):
name = self.get(node, "id")
skip_default_entry = self.get(node, "skip_default_entry") == "true"
per_stream_entry = self.get(node, "per_stream_entry") == "true"
set_node_values = False
if skip_groups:
group_name = self.get_group_name(node)
if not group_name in skip_groups:
self._entry_nodes.append(node)
set_node_values = True
if not skip_default_entry and not per_stream_entry:
default_nodes.append(node)
else:
self._entry_nodes.append(node)
set_node_values = True
if not skip_default_entry and not per_stream_entry:
default_nodes.append(node)
if set_node_values:
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)
return default_nodes
def get_group_name(self, node=None):
if self.get_version() == 1.0:
group = self.get(node, 'group')
elif self.get_version() >= 2.0:
group = self.get_element_text("group", root=node)
return(group)
def _get_type(self, node):
if self.get_version() == 1.0:
type_info = self.get(node, 'type')
elif self.get_version() >= 2.0:
type_info = self._get_type_info(node)
return(type_info)
def _get_valid_values(self, node):
# The "valid_values" attribute is not required, and an empty string has
# the same effect as not specifying it.
# Returns a list from a comma seperated string in xml
valid_values = ''
if self.get_version() == 1.0:
valid_values = self.get(node, 'valid_values')
elif self.get_version() >= 2.0:
valid_values = self._get_node_element_info(node, "valid_values")
if valid_values == '':
valid_values = None
if valid_values is not None:
valid_values = valid_values.split(',')
return valid_values
def get_group(self, name):
return self._group_names[name]
def add_attributes(self, attributes):
self._attributes = attributes
def get_entry_nodes(self):
return self._entry_nodes
def get_per_stream_entries(self):
entries = []
nodes = self.get_children("entry")
for node in nodes:
per_stream_entry = self.get(node, "per_stream_entry") == "true"
if per_stream_entry:
entries.append(self.get(node, "id"))
return entries
# Currently we don't use this object to construct new files, and it's no
# good for that purpose anyway, so stop this function from being called.
def set_value(self, vid, value, subgroup=None, ignore_type=True):
"""This function is not implemented."""
raise TypeError("NamelistDefinition does not support `set_value`.")
def get_value_match(self, vid, attributes=None, exact_match=True, entry_node=None):
"""Return the default value for the variable named `vid`.
The return value is a list of strings corresponding to the
comma-separated list of entries for the value (length 1 for scalars). If
there is no default value in the file, this returns `None`.
"""
# Merge internal attributes with those passed in.
all_attributes = {}
if self._attributes is not None:
all_attributes.update(self._attributes)
if attributes is not None:
all_attributes.update(attributes)
if entry_node is None:
entry_node = self._nodes[vid]
value = super(NamelistDefinition, self).get_value_match(vid.lower(),attributes=all_attributes, exact_match=exact_match,
entry_node=entry_node)
if value is None:
value = ''
else:
value = self._split_defaults_text(value)
return value
@staticmethod
def _split_defaults_text(string):
"""Take a comma-separated list in a string, and split it into a list."""
# Some trickiness here; we want to split items on commas, but not inside
# quote-delimited strings. Stripping whitespace is also useful.
value = []
if len(string):
pos = 0
delim = None
for i, char in enumerate(string):
if delim is None:
# If not inside a string...
if char in ('"', "'"):
# if we have a quote character, start a string.
delim = char
elif char == ',':
# if we have a comma, this is a new value.
value.append(string[pos:i].strip())
pos = i+1
else:
# If inside a string, the only thing that can happen is the end
# of the string.
if char == delim:
delim = None
value.append(string[pos:].strip())
return value
def split_type_string(self, name):
"""Split a 'type' attribute string into its component parts.
The `name` argument is the variable name.
This is used for error reporting purposes.
The return value is a tuple consisting of the type itself, a length
(which is an integer for character variables, otherwise `None`), and the
size of the array (which is 1 for scalar variables).
"""
type_string = self._entry_types[name]
# 'char' is frequently used as an abbreviation of 'character'.
type_string = type_string.replace('char', 'character')
# Separate into a size and the rest of the type.
size_match = _array_size_re.search(type_string)
if size_match:
type_string = size_match.group('type')
size_string = size_match.group('size')
try:
size = int(size_string)
except ValueError:
expect(False,
"In namelist definition, variable {} had the non-integer string {!r} specified as an array size.".format(name, size_string))
else:
size = 1
# Separate into a type and an optional length.
type_, star, length = type_string.partition('*')
if star == '*':
# Length allowed only for character variables.
expect(type_ == 'character',
"In namelist definition, length specified for non-character "
"variable {}.".format(name))
# Check that the length is actually an integer, to make the error
# message a bit cleaner if the xml input is bad.
try:
max_len = int(length)
except ValueError:
expect(False,
"In namelist definition, character variable {} had the non-integer string {!r} specified as a length.".format(name, length))
else:
max_len = None
return type_, max_len, size
@staticmethod
def _canonicalize_value(type_, value):
"""Create 'canonical' version of a value for comparison purposes."""
canonical_value = [fortran_namelist_base_value(scalar)
for scalar in value]
canonical_value = [scalar for scalar in canonical_value if scalar != '']
if type_ == 'character':
canonical_value = [character_literal_to_string(scalar)
for scalar in canonical_value]
elif type_ == 'integer':
canonical_value = [int(scalar) for scalar in canonical_value]
return canonical_value
def is_valid_value(self, name, value):
"""Determine whether a value is valid for the named variable.
The `value` argument must be a list of strings formatted as they would
appear in the namelist (even for scalar variables, in which case the
length of the list is always 1).
"""
# Separate into a type, optional length, and optional size.
type_, max_len, size = self.split_type_string(name)
invalid = []
# Check value against type.
for scalar in value:
if not is_valid_fortran_namelist_literal(type_, scalar):
invalid.append(scalar)
if len(invalid) > 0:
logger.warning("Invalid values {}".format(invalid))
return False
# Now that we know that the strings as input are valid Fortran, do some
# canonicalization for further checks.
canonical_value = self._canonicalize_value(type_, value)
# Check maximum length (if applicable).
if max_len is not None:
for scalar in canonical_value:
if len(scalar) > max_len:
return False
# Check valid value constraints (if applicable).
valid_values = self._valid_values[name]
if valid_values is not None:
expect(type_ in ('integer', 'character'),
"Found valid_values attribute for variable {} with type {}, but valid_values only allowed for character and integer variables.".format(name, type_))
if type_ == 'integer':
compare_list = [int(vv) for vv in valid_values]
else:
compare_list = valid_values
for scalar in canonical_value:
if scalar not in compare_list:
invalid.append(scalar)
if len(invalid) > 0:
logger.warning("Invalid values {}".format(invalid))
return False
# Check size of input array.
if len(expand_literal_list(value)) > size:
expect(False, "Value index exceeds variable size for variable {}, allowed array length is {} value array size is {}".format(name, size, len(expand_literal_list(value))))
return True
def _expect_variable_in_definition(self, name, variable_template):
"""Used to get a better error message for an unexpected variable.
case insensitve match"""
expect(name in self._entry_ids,
(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})
user_modifiable_only_by_xml = self.get(node, 'modify_via_xml')
if user_modifiable_only_by_xml is not None:
expect(False,
"Cannot change {} in user_nl file: set via xml variable {}".format(name, user_modifiable_only_by_xml))
user_cannot_modify = self.get(node, 'cannot_modify_by_user_nl')
if user_cannot_modify is not None:
expect(False,
"Cannot change {} in user_nl file: {}".format(name, user_cannot_modify))
def _generate_variable_template(self, filename):
# Improve error reporting when a file name is provided.
if filename is None:
variable_template = "Variable {!r}"
else:
# for the next step we want the name of the original user_nl file not the internal one
# We do this by extracting the component name from the filepath string
if "Buildconf" in filename and "namelist_infile" in filename:
msgfn = "user_nl_" + (filename.split(os.sep)[-2])[:-4]
else:
msgfn = filename
variable_template = "Variable {!r} from file " + repr(str(msgfn))
return variable_template
def validate(self, namelist,filename=None):
"""Validate a namelist object against this definition.
The optional `filename` argument can be used to assist in error
reporting when the namelist comes from a specific, known file.
"""
variable_template = self._generate_variable_template(filename)
# Iterate through variables.
for group_name in namelist.get_group_names():
for variable_name in namelist.get_variable_names(group_name):
# Check that the variable is defined...
qualified_variable_name = get_fortran_name_only(variable_name)
self._expect_variable_in_definition(qualified_variable_name, variable_template)
# Check if can actually change this variable via filename change
if filename is not None:
self._user_modifiable_in_variable_definition(qualified_variable_name)
# and has the right group name...
var_group = self.get_group(qualified_variable_name)
expect(var_group == group_name,
(variable_template + " is in a group named {!r}, but should be in {!r}.").format(str(variable_name), str(group_name), str(var_group)))
# and has a valid value.
value = namelist.get_variable_value(group_name, variable_name)
expect(self.is_valid_value(qualified_variable_name, value),
(variable_template + " has invalid value {!r}.").format(str(variable_name), [str(scalar) for scalar in value]))
def dict_to_namelist(self, dict_, filename=None):
"""Converts a dictionary of name-value pairs to a `Namelist`.
The input is assumed to be similar to the output of `parse` when
`groupless=True` is set. This function uses the namelist definition file
to look up the namelist group associated with each variable, and uses
this information to create a true `Namelist` object.
The optional `filename` argument can be used to assist in error
reporting when the namelist comes from a specific, known file.
"""
# Improve error reporting when a file name is provided.
variable_template = self._generate_variable_template(filename)
groups = {}
for variable_name in dict_:
variable_lc = variable_name.lower()
qualified_varname = get_fortran_name_only(variable_lc)
self._expect_variable_in_definition(qualified_varname, variable_template)
group_name = self.get_group(qualified_varname)
expect (group_name is not None, "No group found for var {}".format(variable_lc))
if group_name not in groups:
groups[group_name] = collections.OrderedDict()
groups[group_name][variable_lc] = dict_[variable_name]
return Namelist(groups)
def get_input_pathname(self, name):
node = self._nodes[name]
if self.get_version() == 1.0:
input_pathname = self.get(node, 'input_pathname')
elif self.get_version() >= 2.0:
input_pathname = self._get_node_element_info(node, "input_pathname")
return(input_pathname)
# pylint: disable=arguments-differ
def get_default_value(self, item, attribute=None):
"""Return the default value for the variable named `item`.
The return value is a list of strings corresponding to the
comma-separated list of entries for the value (length 1 for scalars). If
there is no default value in the file, this returns `None`.
"""
# Merge internal attributes with those passed in.
all_attributes = {}
if self._attributes is not None:
all_attributes.update(self._attributes)
if attribute is not None:
all_attributes.update(attribute)
value = self.get_value_match(item.lower(), all_attributes, True)
return self._split_defaults_text(value)