DISPATCH
namelist.py
1 """Fortran namelist interface.
2 
3 The ``Namelist`` is a representation of a Fortran namelist and its contents in
4 a Python environment.
5 
6 :copyright: Copyright 2014 Marshall Ward, see AUTHORS for details.
7 :license: Apache License, Version 2.0, see LICENSE for details.
8 """
9 from __future__ import print_function
10 
11 import numbers
12 import os
13 import platform
14 try:
15  from StringIO import StringIO # Python 2.x
16 except ImportError:
17  from io import StringIO # Python 3.x
18 try:
19  from collections import OrderedDict
20 except ImportError:
21  from ordereddict import OrderedDict
22 try:
23  basestring # Python 2.x
24 except NameError:
25  basestring = str # Python 3.x
26 
27 
28 class Namelist(OrderedDict):
29  """Representation of Fortran namelist in a Python environment.
30 
31  Namelists can be initialised as empty or with a pre-defined `dict` of
32  `items`. If an explicit default start index is required for `items`, then
33  it can be initialised with the `default_start_index` input argument.
34 
35  In addition to the standard methods supported by `dict`, several additional
36  methods and properties are provided for working with Fortran namelists.
37  """
38 
39  def __init__(self, *args, **kwds):
40  """Create the Namelist object."""
41  s_args = list(args)
42 
43  # If using (unordered) dict, then resort the keys for reproducibility
44  if (args and not isinstance(args[0], OrderedDict) and
45  isinstance(args[0], dict)):
46  s_args[0] = sorted(args[0].items())
47 
48  # Assign the default start index
49  try:
50  self._default_start_index = kwds.pop('default_start_index')
51  except KeyError:
52  self._default_start_index = None
53 
54  super(Namelist, self).__init__(*s_args, **kwds)
55 
56  self.start_index = self.pop('_start_index', {})
57 
58  # Update the complex tuples as intrinsics
59  # TODO: We are effectively setting these twice. Instead, fetch these
60  # from s_args rather than relying on Namelist to handle the content.
61  if '_complex' in self:
62  for key in self['_complex']:
63  if all(isinstance(v, list) for v in self[key]):
64  self[key] = [complex(*v) for v in self[key]]
65  else:
66  self[key] = complex(*self[key])
67  self.pop('_complex')
68 
69  # Formatting properties
70  self._column_width = 72
71  self._indent = 4 * ' '
72  self._end_comma = False
73  self._uppercase = False
74  self._float_format = ''
75  self._logical_repr = {False: '.false.', True: '.true.'}
76 
77  # Namelist group spacing flag
78  self._newline = False
79 
80  # PyPy 2 is dumb and does not use __setitem__() inside __init__()
81  # This loop will explicitly convert any internal dicts to Namelists.
82  if (platform.python_implementation() == 'PyPy' and
83  platform.python_version_tuple()[0] == '2'):
84  for key, value in self.items():
85  self[key] = value
86 
87  def __contains__(self, key):
88  """Case-insensitive interface to OrderedDict."""
89  return super(Namelist, self).__contains__(key.lower())
90 
91  def __delitem__(self, key):
92  """Case-insensitive interface to OrderedDict."""
93  return super(Namelist, self).__delitem__(key.lower())
94 
95  def __getitem__(self, key):
96  """Case-insensitive interface to OrderedDict."""
97  return super(Namelist, self).__getitem__(key.lower())
98 
99  def __setitem__(self, key, value):
100  """Case-insensitive interface to OrderedDict.
101 
102  Python dict inputs to the Namelist, such as derived types, are also
103  converted into Namelists.
104  """
105  if isinstance(value, dict) and not isinstance(value, Namelist):
106  value = Namelist(value,
107  default_start_index=self.default_start_index)
108 
109  elif is_nullable_list(value, dict):
110  for i, v in enumerate(value):
111  if v is not None:
112  value[i] = Namelist(
113  v,
114  default_start_index=self.default_start_index
115  )
116  else:
117  value[i] = None
118 
119  super(Namelist, self).__setitem__(key.lower(), value)
120 
121  def __str__(self):
122  """Print the Fortran representation of the namelist.
123 
124  Currently this can only be applied to the full contents of the namelist
125  file. Indiviual namelist groups or values may not render correctly.
126  """
127  output = StringIO()
128  if all(isinstance(v, Namelist) for v in self.values()):
129  self._writestream(output)
130  else:
131  print(repr(self), file=output)
132 
133  nml_string = output.getvalue().rstrip()
134  output.close()
135  return nml_string
136 
137  # Format configuration
138 
139  @property
140  def column_width(self):
141  """Set the maximum number of characters per line of the namelist file.
142 
143  Tokens longer than ``column_width`` are allowed to extend past this
144  limit. (Default: 72)
145  """
146  return self._column_width
147 
148  @column_width.setter
149  def column_width(self, width):
150  """Validate and set the column width."""
151  if isinstance(width, int):
152  if width >= 0:
153  self._column_width = width
154  else:
155  raise ValueError('Column width must be nonnegative.')
156  else:
157  raise TypeError('Column width must be a nonnegative integer.')
158 
159  @property
160  def indent(self):
161  r"""Set the whitespace indentation of namelist entries.
162 
163  This can be set to an integer, denoting the number of spaces, or to an
164  explicit whitespace character, such as a tab (``\t``).
165  (Default: 4)
166  """
167  return self._indent
168 
169  @indent.setter
170  def indent(self, value):
171  """Validate and set the indent width."""
172  # Explicit indent setting
173  if isinstance(value, str):
174  if value.isspace():
175  self._indent = value
176  else:
177  raise ValueError('String indentation can only contain '
178  'whitespace.')
179 
180  # Set indent width
181  elif isinstance(value, int):
182  if value >= 0:
183  self._indent = value * ' '
184  else:
185  raise ValueError('Indentation spacing must be nonnegative.')
186 
187  else:
188  raise TypeError('Indentation must be specified by string or space '
189  'width.')
190 
191  @property
192  def end_comma(self):
193  """Append commas to the end of namelist variable entries.
194 
195  Fortran will generally disregard any commas separating variable
196  assignments, and the default behaviour is to omit these commas from the
197  output. Enabling this flag will append commas at the end of the line
198  for each variable assignment.
199  """
200  return self._end_comma
201 
202  @end_comma.setter
203  def end_comma(self, value):
204  """Validate and set the comma termination flag."""
205  if not isinstance(value, bool):
206  raise TypeError('end_comma attribute must be a logical type.')
207  self._end_comma = value
208 
209  @property
210  def uppercase(self):
211  """Print group and variable names in uppercase."""
212  return self._uppercase
213 
214  @uppercase.setter
215  def uppercase(self, value):
216  """Validate and set the uppercase flag."""
217  if not isinstance(value, bool):
218  raise TypeError('uppercase attribute must be a logical type.')
219  self._uppercase = value
220 
221  @property
222  def float_format(self):
223  """Set the namelist floating point format.
224 
225  The property sets the format string for floating point numbers,
226  following the format expected by the Python ``format()`` function.
227  """
228  return self._float_format
229 
230  @float_format.setter
231  def float_format(self, value):
232  """Validate and set the upper case flag."""
233  if isinstance(value, str):
234  # Duck-test the format string; raise ValueError on fail
235  '{0:{1}}'.format(1.23, value)
236 
237  self._float_format = value
238  else:
239  raise TypeError('Floating point format code must be a string.')
240 
241  # NOTE: This presumes that bools and ints are identical as dict keys
242  @property
243  def logical_repr(self):
244  """Set the string representation of logical values.
245 
246  There are multiple valid representations of True and False values in
247  Fortran. This property sets the preferred representation in the
248  namelist output.
249 
250  The properties ``true_repr`` and ``false_repr`` are also provided as
251  interfaces to the ``logical_repr`` tuple.
252  (Default: ``.false., .true.``)
253  """
254  return self._logical_repr
255 
256  @logical_repr.setter
257  def logical_repr(self, value):
258  """Set the string representation of logical values."""
259  if not any(isinstance(value, t) for t in (list, tuple)):
260  raise TypeError("Logical representation must be a tuple with "
261  "a valid true and false value.")
262  if not len(value) == 2:
263  raise ValueError("List must contain two values.")
264 
265  self.false_repr = value[0]
266  self.true_repr = value[1]
267 
268  @property
269  def true_repr(self):
270  """Set the string representation of logical true values.
271 
272  This is equivalent to the second element of ``logical_repr``.
273  """
274  return self._logical_repr[1]
275 
276  @true_repr.setter
277  def true_repr(self, value):
278  """Validate and set the logical true representation."""
279  if isinstance(value, str):
280  if not (value.lower().startswith('t') or
281  value.lower().startswith('.t')):
282  raise ValueError("Logical true representation must start with "
283  "'T' or '.T'.")
284  else:
285  self._logical_repr[1] = value
286  else:
287  raise TypeError('Logical true representation must be a string.')
288 
289  @property
290  def false_repr(self):
291  """Set the string representation of logical false values.
292 
293  This is equivalent to the first element of ``logical_repr``.
294  """
295  return self._logical_repr[0]
296 
297  @false_repr.setter
298  def false_repr(self, value):
299  """Validate and set the logical false representation."""
300  if isinstance(value, str):
301  if not (value.lower().startswith('f') or
302  value.lower().startswith('.f')):
303  raise ValueError("Logical false representation must start "
304  "with 'F' or '.F'.")
305  else:
306  self._logical_repr[0] = value
307  else:
308  raise TypeError('Logical false representation must be a string.')
309 
310  @property
311  def start_index(self):
312  """Set the starting index for each vector in the namelist.
313 
314  ``start_index`` is stored as a dict which contains the starting index
315  for each vector saved in the namelist. For the namelist ``vec.nml``
316  shown below,
317 
318  .. code-block:: fortran
319 
320  &vec_nml
321  a = 1, 2, 3
322  b(0:2) = 0, 1, 2
323  c(3:5) = 3, 4, 5
324  d(:,:) = 1, 2, 3, 4
325  /
326 
327  the ``start_index`` contents are
328 
329  .. code:: python
330 
331  >>> import f90nml
332  >>> nml = f90nml.read('vec.nml')
333  >>> nml['vec_nml'].start_index
334  {'b': [0], 'c': [3], 'd': [None, None]}
335 
336  The starting index of ``a`` is absent from ``start_index``, since its
337  starting index is unknown and its values cannot be assigned without
338  referring to the corresponding Fortran source.
339  """
340  return self._start_index
341 
342  @start_index.setter
343  def start_index(self, value):
344  """Validate and set the vector start index."""
345  # TODO: Validate contents? (May want to set before adding the data.)
346  if not isinstance(value, dict):
347  raise TypeError('start_index attribute must be a dict.')
348  self._start_index = value
349 
350  @property
352  """Set the default start index for vectors with no explicit index.
353 
354  When the `default_start_index` is set, all vectors without an explicit
355  start index are assumed to begin with `default_start_index`. This
356  index is shown when printing the namelist output.
357  (Default: None)
358 
359  If set to `None`, then no start index is assumed and is left as
360  implicit for any vectors undefined in `start_index`.
361  """
362  return self._default_start_index
363 
364  @default_start_index.setter
365  def default_start_index(self, value):
366  if not isinstance(value, int):
367  raise TypeError('default_start_index must be an integer.')
368  self._default_start_index = value
369 
370  def write(self, nml_path, force=False, sort=False):
371  """Write Namelist to a Fortran 90 namelist file.
372 
373  >>> nml = f90nml.read('input.nml')
374  >>> nml.write('out.nml')
375  """
376  nml_is_file = hasattr(nml_path, 'read')
377  if not force and not nml_is_file and os.path.isfile(nml_path):
378  raise IOError('File {0} already exists.'.format(nml_path))
379 
380  nml_file = nml_path if nml_is_file else open(nml_path, 'w')
381  try:
382  self._writestream(nml_file, sort)
383  finally:
384  if not nml_is_file:
385  nml_file.close()
386 
387  def patch(self, nml_patch):
388  """Update the namelist from another partial or full namelist.
389 
390  This is different from the intrinsic `update()` method, which replaces
391  a namelist section. Rather, it updates the values within a section.
392  """
393  for sec in nml_patch:
394  if sec not in self:
395  self[sec] = Namelist()
396  self[sec].update(nml_patch[sec])
397 
398  def _writestream(self, nml_file, sort=False):
399  """Output Namelist to a streamable file object."""
400  # Reset newline flag
401  self._newline = False
402 
403  if sort:
404  sel = Namelist(sorted(self.items(), key=lambda t: t[0]))
405  else:
406  sel = self
407 
408  for grp_name, grp_vars in sel.items():
409  # Check for repeated namelist records (saved as lists)
410  if isinstance(grp_vars, list):
411  for g_vars in grp_vars:
412  self._write_nmlgrp(grp_name, g_vars, nml_file, sort)
413  else:
414  self._write_nmlgrp(grp_name, grp_vars, nml_file, sort)
415 
416  def _write_nmlgrp(self, grp_name, grp_vars, nml_file, sort=False):
417  """Write namelist group to target file."""
418  if self._newline:
419  print(file=nml_file)
420  self._newline = True
421 
422  if self.uppercase:
423  grp_name = grp_name.upper()
424 
425  if sort:
426  grp_vars = Namelist(sorted(grp_vars.items(), key=lambda t: t[0]))
427 
428  print('&{0}'.format(grp_name), file=nml_file)
429 
430  for v_name, v_val in grp_vars.items():
431 
432  v_start = grp_vars.start_index.get(v_name, None)
433 
434  for v_str in self._var_strings(v_name, v_val, v_start=v_start):
435  nml_line = self.indent + '{0}'.format(v_str)
436  print(nml_line, file=nml_file)
437 
438  print('/', file=nml_file)
439 
440  def _var_strings(self, v_name, v_values, v_idx=None, v_start=None):
441  """Convert namelist variable to list of fixed-width strings."""
442  if self.uppercase:
443  v_name = v_name.upper()
444 
445  var_strs = []
446 
447  # Parse a multidimensional array
448  if is_nullable_list(v_values, list):
449  if not v_idx:
450  v_idx = []
451 
452  i_s = v_start[::-1][len(v_idx)] if v_start else None
453 
454  # FIXME: We incorrectly assume 1-based indexing if it is
455  # unspecified. This is necessary because our output method always
456  # separates the outer axes to one per line. But we cannot do this
457  # if we don't know the first index (which we are no longer assuming
458  # to be 1-based elsewhere). Unfortunately, the solution needs a
459  # rethink of multidimensional output.
460 
461  # NOTE: Fixing this would also clean up the output of todict(),
462  # which is now incorrectly documenting unspecified indices as 1.
463 
464  # For now, we will assume 1-based indexing here, just to keep
465  # things working smoothly.
466  if i_s is None:
467  i_s = 1
468 
469  for idx, val in enumerate(v_values, start=i_s):
470  v_idx_new = v_idx + [idx]
471  v_strs = self._var_strings(v_name, val, v_idx=v_idx_new,
472  v_start=v_start)
473  var_strs.extend(v_strs)
474 
475  # Parse derived type contents
476  elif isinstance(v_values, Namelist):
477  for f_name, f_vals in v_values.items():
478  v_title = '%'.join([v_name, f_name])
479 
480  v_start_new = v_values.start_index.get(f_name, None)
481 
482  v_strs = self._var_strings(v_title, f_vals,
483  v_start=v_start_new)
484  var_strs.extend(v_strs)
485 
486  # Parse an array of derived types
487  elif is_nullable_list(v_values, Namelist):
488  if not v_idx:
489  v_idx = []
490 
491  i_s = v_start[::-1][len(v_idx)] if v_start else 1
492 
493  for idx, val in enumerate(v_values, start=i_s):
494 
495  # Skip any empty elements in a list of derived types
496  if val is None:
497  continue
498 
499  v_title = v_name + '({0})'.format(idx)
500 
501  v_strs = self._var_strings(v_title, val)
502  var_strs.extend(v_strs)
503 
504  else:
505  use_default_start_index = False
506  if not isinstance(v_values, list):
507  v_values = [v_values]
508  use_default_start_index = False
509  else:
510  use_default_start_index = self.default_start_index is not None
511 
512  # Print the index range
513 
514  # TODO: Include a check for len(v_values) to determine if vector
515  if v_idx or v_start or use_default_start_index:
516  v_idx_repr = '('
517 
518  if v_start or use_default_start_index:
519  if v_start:
520  i_s = v_start[0]
521  else:
522  i_s = self.default_start_index
523 
524  if i_s is None:
525  v_idx_repr += ':'
526 
527  else:
528  i_e = i_s + len(v_values) - 1
529 
530  if i_s == i_e:
531  v_idx_repr += '{0}'.format(i_s)
532  else:
533  v_idx_repr += '{0}:{1}'.format(i_s, i_e)
534  else:
535  v_idx_repr += ':'
536 
537  if v_idx:
538  v_idx_repr += ', '
539  v_idx_repr += ', '.join(str(i) for i in v_idx[::-1])
540 
541  v_idx_repr += ')'
542 
543  else:
544  v_idx_repr = ''
545 
546  # Split output across multiple lines (if necessary)
547 
548  val_strs = []
549 
550  val_line = ''
551  for v_val in v_values:
552 
553  v_header = v_name + v_idx_repr + ' = '
554  # Increase column width if the header exceeds this value
555  if len(self.indent + v_header) >= self.column_width:
556  column_width = len(self.indent + v_header) + 1
557  else:
558  column_width = self.column_width
559 
560  v_width = column_width - len(self.indent + v_header)
561 
562  if len(val_line) < v_width:
563  val_line += self._f90repr(v_val) + ', '
564 
565  if len(val_line) >= v_width:
566  val_strs.append(val_line.rstrip())
567  val_line = ''
568 
569  # Append any remaining values
570  if val_line:
571  val_strs.append(val_line.rstrip())
572 
573  if val_strs:
574  if self.end_comma or v_values[-1] is None:
575  pass
576  else:
577  val_strs[-1] = val_strs[-1][:-1]
578 
579  # Complete the set of values
580  if val_strs:
581  var_strs.append('{0}{1} = {2}'
582  ''.format(v_name, v_idx_repr,
583  val_strs[0]).strip())
584 
585  for v_str in val_strs[1:]:
586  var_strs.append(' ' * len(v_header) + v_str)
587 
588  return var_strs
589 
590  def todict(self, complex_tuple=False):
591  """Return a dict equivalent to the namelist.
592 
593  Since Fortran variables and names cannot start with the ``_``
594  character, any keys starting with this token denote metadata, such as
595  starting index.
596 
597  The ``complex_tuple`` flag is used to convert complex data into an
598  equivalent 2-tuple, with metadata stored to flag the variable as
599  complex. This is primarily used to facilitate the storage of the
600  namelist into an equivalent format which does not support complex
601  numbers, such as JSON or YAML.
602  """
603  # TODO: Preserve ordering
604  nmldict = OrderedDict(self)
605 
606  # Search for namelists within the namelist
607  # TODO: Move repeated stuff to new functions
608  for key, value in self.items():
609  if isinstance(value, Namelist):
610  nmldict[key] = value.todict(complex_tuple)
611 
612  elif isinstance(value, complex) and complex_tuple:
613  nmldict[key] = [value.real, value.imag]
614  try:
615  nmldict['_complex'].append(key)
616  except KeyError:
617  nmldict['_complex'] = [key]
618 
619  elif isinstance(value, list):
620  complex_list = False
621  for idx, entry in enumerate(value):
622  if isinstance(entry, Namelist):
623  nmldict[key][idx] = entry.todict(complex_tuple)
624 
625  elif isinstance(entry, complex) and complex_tuple:
626  nmldict[key][idx] = [entry.real, entry.imag]
627  complex_list = True
628 
629  if complex_list:
630  try:
631  nmldict['_complex'].append(key)
632  except KeyError:
633  nmldict['_complex'] = [key]
634 
635  # Append the start index if present
636  if self.start_index:
637  nmldict['_start_index'] = self.start_index
638 
639  return nmldict
640 
641  def _f90repr(self, value):
642  """Convert primitive Python types to equivalent Fortran strings."""
643  if isinstance(value, bool):
644  return self._f90bool(value)
645  elif isinstance(value, numbers.Integral):
646  return self._f90int(value)
647  elif isinstance(value, numbers.Real):
648  return self._f90float(value)
649  elif isinstance(value, numbers.Complex):
650  return self._f90complex(value)
651  elif isinstance(value, basestring):
652  return self._f90str(value)
653  elif value is None:
654  return ''
655  else:
656  raise ValueError('Type {0} of {1} cannot be converted to a Fortran'
657  ' type.'.format(type(value), value))
658 
659  def _f90bool(self, value):
660  """Return a Fortran 90 representation of a logical value."""
661  return self.logical_repr[value]
662 
663  def _f90int(self, value):
664  """Return a Fortran 90 representation of an integer."""
665  return str(value)
666 
667  def _f90float(self, value):
668  """Return a Fortran 90 representation of a floating point number."""
669  return '{0:{fmt}}'.format(value, fmt=self.float_format)
670 
671  def _f90complex(self, value):
672  """Return a Fortran 90 representation of a complex number."""
673  return '({0:{fmt}}, {1:{fmt}})'.format(value.real, value.imag,
674  fmt=self.float_format)
675 
676  def _f90str(self, value):
677  """Return a Fortran 90 representation of a string."""
678  # Replace Python quote escape sequence with Fortran
679  result = repr(str(value)).replace("\\'", "''").replace('\\"', '""')
680 
681  # Un-escape the Python backslash escape sequence
682  result = result.replace('\\\\', '\\')
683 
684  return result
685 
686 
687 def is_nullable_list(val, vtype):
688  """Return True if list contains either values of type `vtype` or None."""
689  return (isinstance(val, list) and
690  any(isinstance(v, vtype) for v in val) and
691  all((isinstance(v, vtype) or v is None) for v in val))
def todict(self, complex_tuple=False)
Definition: namelist.py:590
def __delitem__(self, key)
Definition: namelist.py:91
def _writestream(self, nml_file, sort=False)
Definition: namelist.py:398
def _write_nmlgrp(self, grp_name, grp_vars, nml_file, sort=False)
Definition: namelist.py:416
def default_start_index(self)
Definition: namelist.py:351
def __setitem__(self, key, value)
Definition: namelist.py:99
def write(self, nml_path, force=False, sort=False)
Definition: namelist.py:370
def __contains__(self, key)
Definition: namelist.py:87
def _f90repr(self, value)
Definition: namelist.py:641
def __init__(self, args, kwds)
Definition: namelist.py:39
def _var_strings(self, v_name, v_values, v_idx=None, v_start=None)
Definition: namelist.py:440
def _f90str(self, value)
Definition: namelist.py:676
def _f90complex(self, value)
Definition: namelist.py:671
def _f90int(self, value)
Definition: namelist.py:663
def _f90bool(self, value)
Definition: namelist.py:659
def _f90float(self, value)
Definition: namelist.py:667
def __getitem__(self, key)
Definition: namelist.py:95
def patch(self, nml_patch)
Definition: namelist.py:387
def is_nullable_list(val, vtype)
Definition: namelist.py:687