nml.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. """Collection of Fortran 90 namelist helper functions.
  2. A common way to interface with a Fortran executable is via
  3. an input file called a namelist. This module defines
  4. functions which simplify the process of updating and
  5. extending namelist data.
  6. .. note:: This module is especially lightweight and follows the
  7. batteries included philosophy. As such, only standard
  8. library modules are required to use this code.
  9. Walkthrough
  10. ===========
  11. New namelist
  12. ------------
  13. A typical usage is to create and update a Fortran namelist on the fly.
  14. >>> import nml
  15. >>> namid = "namfoo"
  16. >>> text = nml.new(namid)
  17. >>> data = {"x": nml.tostring([1, 2, 3])}
  18. >>> text = nml.update(namid, text, data)
  19. >>> print text
  20. &namfoo
  21. x = 1 2 3
  22. /
  23. <BLANKLINE>
  24. In the above snippet :func:`tostring` has been used to sanitize the input
  25. Python list. This function cleverly maps string data and numeric data to
  26. the correct Fortran syntax.
  27. However, the :func:`new` function takes care of many of the above steps automatically.
  28. Where appropriate :func:`sanitize` has been embedded to reduce the need
  29. to worry about data format problems. Take for example,
  30. >>> print nml.new("namfoo", data={"x": range(3)})
  31. &namfoo
  32. x = 0 1 2
  33. /
  34. <BLANKLINE>
  35. Parse existing namelist
  36. -----------------------
  37. In order to update a namelist it is necessary to convert the namelist text into
  38. a dictionary of *key, value* pairs which can be manipulated in the usual Pythonic
  39. fashion before being piped back out to disk.
  40. In everyday usage text will be read from files, here however for illustration
  41. purposes I have hand written a namelist.
  42. >>> text = '''
  43. ... &namfoo
  44. ... x = y ! A description of the variables
  45. ... /
  46. ... &nambar
  47. ... ln_on = .TRUE. ! A description of the variables
  48. ... /
  49. ... '''
  50. This can be parsed by invoking the :func:`variables` command.
  51. >>> nml.variables(text)
  52. {'x': 'y', 'ln_on': '.TRUE.'}
  53. Or by using the :func:`namelists` function to split the file into sub-lists.
  54. >>> nml.namelists(text)
  55. {'namfoo': '&namfoo\\n x = y ! A description of the variables\\n/', 'nambar': '&nambar\\n ln_on = .TRUE. ! A description of the variables\\n/'}
  56. >>> sublists = nml.namelists(text)
  57. >>> print sublists["nambar"]
  58. &nambar
  59. ln_on = .TRUE. ! A description of the variables
  60. /
  61. Which can be parsed into a dictionary as before.
  62. >>> print nml.variables(sublists["nambar"])
  63. {'ln_on': '.TRUE.'}
  64. Update/replace data
  65. -------------------
  66. There are two ways of modifying values inside a Fortran namelist.
  67. Replace
  68. The first is to simply replace a set of variables with new values. This behaviour is accomplished
  69. via the :func:`replace` function. This approach simply overwrites existing variables. No knowledge
  70. of sub-namelist structure is required to modify a string of text.
  71. .. note:: Additional variables will not be added to a namelist via this approach
  72. Update
  73. The second is to extend the set of variables contained within a namelist. This functionality is
  74. controlled by the :func:`update` function. Here, variables which are not already specified are
  75. added using a templated namelist line.
  76. .. note:: It is essential to specify which sub-namelist is to be updated before modification takes place
  77. Pipe to/from file
  78. -----------------
  79. As typical NEMO namelists are no larger than a few tens of kilobytes
  80. it makes sense to process namelists as single strings instead of
  81. line by line.
  82. >>> path = "foo.nml"
  83. >>> text = nml.new("namfoo")
  84. To write to a file simply invoke the writer.
  85. >>> # Write to file
  86. >>> nml.writer(path, text)
  87. To read from a file specify the path to be read.
  88. >>> # Read from file
  89. >>> text = nml.reader(path)
  90. Join multiple namelists
  91. -----------------------
  92. Since the namelists are regular Python strings there is no need for a
  93. specific *join* function. Namelists can be combined in whatever manner
  94. is most pleasing to the eye.
  95. >>> namoff = nml.new("namoff")
  96. >>> namcl4 = nml.new("namcl4")
  97. >>> # new line join
  98. >>> print "\\n".join([namoff, namcl4])
  99. &namoff
  100. /
  101. <BLANKLINE>
  102. &namcl4
  103. /
  104. <BLANKLINE>
  105. >>> # Or addition
  106. >>> print namoff + namcl4
  107. &namoff
  108. /
  109. &namcl4
  110. /
  111. <BLANKLINE>
  112. Module functions
  113. ================
  114. """
  115. __version__ = "0.1.0"
  116. import re
  117. from numbers import Number
  118. def reader(path):
  119. """Reads a file into a string
  120. Reads whole file into single string. Typically,
  121. namelists are small enough to be stored in memory
  122. while updates and edits are being performed.
  123. :param path: Path to input file
  124. :returns: entire file as a single string
  125. """
  126. with open(path, "r") as handle:
  127. text = handle.read()
  128. return text
  129. def writer(path, text):
  130. """Writes to a file from a string
  131. Handy way of piping a processed namelist into
  132. a file.
  133. :param path: Path to output file
  134. :param text: Input text to process
  135. """
  136. with open(path, "w") as handle:
  137. handle.write(text)
  138. def update(namid, text, data, convert=True):
  139. """Extends namelist definition.
  140. Similar to replace this function alters the values
  141. of variables defined within a namelist. In addition to
  142. replacing values it also creates definitions if the
  143. variable is not found in the namelist. As such, the
  144. namelist id must be specified.
  145. :param namid: Namelist id
  146. :param text: Input text to process
  147. :param data: Dictionary of variables
  148. :keyword convert: Sanitizes input data before replacement takes place
  149. :returns: Text
  150. .. seealso:: :func:`replace` :func:`sanitize`
  151. """
  152. sublists = namelists(text)
  153. assert namid in sublists, "Warning: invalid namid specified!"
  154. # Sanitize inputs
  155. if convert:
  156. data = sanitize(data)
  157. # Parse subsection
  158. namtext = sublists[namid]
  159. subdata = variables(namtext)
  160. subvars = subdata.keys()
  161. # Replace existing variables in namtext
  162. tmptext = replace(namtext, data)
  163. text = text.replace(namtext, tmptext)
  164. namtext = tmptext
  165. # Identify new variables
  166. vars = data.keys()
  167. newvars = list(set(vars) - set(subvars))
  168. newvars.sort()
  169. # Append new vars to namid
  170. lines = namtext.split("\n")
  171. for v in newvars:
  172. newline = " %s = %s" % (v, data[v])
  173. lines.insert(-1, newline)
  174. newtext = "\n".join(lines)
  175. # Replace old namtext with new namtext
  176. text = text.replace(namtext, newtext)
  177. return text
  178. def replace(text, data, convert=True):
  179. """Edits existing variables.
  180. Pattern matches and substitutes variables inside
  181. a string of text. This is independent of namid and
  182. as such is useful for modifying existing variables.
  183. To append new variables the :func:`update` function
  184. is required.
  185. >>> text = '''
  186. ... &namobs
  187. ... ln_sst = .TRUE. ! Logical switch for SST observations
  188. ... /
  189. ... '''
  190. >>> data = {"ln_sst": ".FALSE."}
  191. >>> print replace(text, data)
  192. <BLANKLINE>
  193. &namobs
  194. ln_sst = .FALSE. ! Logical switch for SST observations
  195. /
  196. <BLANKLINE>
  197. .. note :: This does not append new variables to a namelist
  198. :param text: string to process
  199. :param data: dictionary with which to modify **text**
  200. :keyword convert: Sanitizes input data before replacement takes place
  201. :returns: string with new data values
  202. .. seealso:: :func:`update`, :func:`sanitize`
  203. """
  204. if convert:
  205. data = sanitize(data)
  206. for k, v in data.iteritems():
  207. pat = r"(%s\s*=\s*).+?(\s*[!\n])" % (k,)
  208. repl = r"\g<1>%s\g<2>" % (v,)
  209. text = re.sub(pat, repl, text)
  210. return text
  211. def variables(text):
  212. """Retrieves dictionary of variables in text.
  213. >>> text = '''
  214. ... &namobs
  215. ... ln_sst = .TRUE. ! Logical switch for SST observations
  216. ... /
  217. ... '''
  218. >>> variables(text)
  219. {'ln_sst': '.TRUE.'}
  220. :param text: Input text to process
  221. :returns: A dictionary of variable, value pairs.
  222. """
  223. data = {}
  224. pairs = re.findall(r"\n\s*(\w+)\s*=\s*(.+?)\s*(?=[!\n])", text)
  225. for key, value in pairs:
  226. data[key] = value
  227. return data
  228. def namelists(text):
  229. """Retrieves dictionary of namelists in text.
  230. Useful for isolating sub-namelists.
  231. >>> text = '''
  232. ... &namobs
  233. ... ln_sst = .TRUE. ! Logical switch for SST observations
  234. ... /
  235. ... '''
  236. >>> namelists(text)
  237. {'namobs': '&namobs\\n ln_sst = .TRUE. ! Logical switch for SST observations\\n/'}
  238. :param text: Input text to process
  239. :returns: A dictionary of id, text block key, value pairs
  240. """
  241. # Boundary case
  242. if text.startswith("&"):
  243. text = "\n" + text
  244. # Regular expression
  245. results = re.findall(r"\n(&(\w+).*?\n/)", text, re.DOTALL)
  246. data = {}
  247. for content, namid in results:
  248. data[namid] = content
  249. return data
  250. def tostring(data):
  251. """Maps standard Python data to Fortran namelist format.
  252. >>> tostring([1, 2, 3])
  253. '1 2 3'
  254. >>> tostring(["foo.nc", "bar.nc"])
  255. "'foo.nc', 'bar.nc'"
  256. >>> tostring(True)
  257. '.TRUE.'
  258. :param data: Input Python data
  259. :returns: Namelist formatted string
  260. .. seealso:: :func:`sanitize`
  261. """
  262. if isinstance(data, list):
  263. if all_numeric(data):
  264. delim = " "
  265. else:
  266. delim = ", "
  267. text = delim.join([convert(item) for item in data])
  268. else:
  269. text = convert(data)
  270. return text
  271. def all_numeric(inputs):
  272. # Checks all list entries are numbers
  273. flag = True
  274. for input in inputs:
  275. if not isinstance(input, Number):
  276. flag = False
  277. break
  278. return flag
  279. def numeric(word):
  280. # Tests input string is numeric data
  281. parts = word.split(" ")
  282. try:
  283. map(float, parts)
  284. flag = True
  285. except ValueError:
  286. flag = False
  287. return flag
  288. def logical(word):
  289. # Tests input string is numeric data
  290. if word.upper() in [".FALSE.", ".TRUE."]:
  291. flag = True
  292. else:
  293. flag = False
  294. return flag
  295. def listed(word):
  296. # Tests input string is not a list
  297. if ("," in word) or (" " in word):
  298. flag = True
  299. else:
  300. flag = False
  301. return flag
  302. def quote(word):
  303. word = str(word)
  304. if not quoted(word):
  305. word = "'%s'" % (word,)
  306. return word
  307. def convert(word):
  308. # Conversion function
  309. if isinstance(word, str):
  310. if (quoted(word) or numeric(word)
  311. or logical(word) or listed(word)):
  312. result = "%s" % (word,)
  313. else:
  314. result = quote(word)
  315. elif isinstance(word, bool):
  316. if word:
  317. result = ".TRUE."
  318. else:
  319. result = ".FALSE."
  320. else:
  321. result = str(word)
  322. return result
  323. def quoted(word):
  324. # Checks if string begins/ends with quotation marks
  325. if (word.startswith("'") and word.endswith("'")):
  326. flag = True
  327. elif (word.startswith('"') and word.endswith('"')):
  328. flag = True
  329. else:
  330. flag = False
  331. return flag
  332. def same_type(data):
  333. # True if all entries are the same type
  334. types = map(type, data)
  335. if len(set(types)) == 1:
  336. flag = True
  337. else:
  338. flag = False
  339. return flag
  340. def sanitize(data):
  341. """Converts dictionary values into Fortran namelist format.
  342. This is a more typical way to prepare data for inclusion in
  343. a Fortran namelist. Instead of manually applying :func:`tostring`
  344. to every element of the input data, **sanitize** fixes the entire
  345. data set.
  346. >>> sanitize({"x": True})
  347. {'x': '.TRUE.'}
  348. >>>
  349. :param data: Dictionary to convert
  350. :returns: Dictionary whose values are in Fortran namelist format
  351. .. seealso:: :func:`tostring`
  352. """
  353. replacements = [(k, tostring(v)) for k, v in data.items()]
  354. data.update(replacements)
  355. return data
  356. def new(namid, data=None, convert=True):
  357. """Creates a new Fortran namelist
  358. >>> new("namobs")
  359. '&namobs\\n/\\n'
  360. >>> print new("namobs")
  361. &namobs
  362. /
  363. <BLANKLINE>
  364. :param namid: Name for the new namelist
  365. :keyword data: Specifies an initial dictionary with which to
  366. populate the namelist
  367. :type data: dict
  368. :keyword convert: Sanitizes input data before replacement takes place
  369. :returns: string representation of a Fortran namelist
  370. """
  371. text = "&{namid}\n/\n".format(namid=namid)
  372. if data is not None:
  373. text = update(namid, text, data, convert=convert)
  374. return text
  375. if __name__ == '__main__':
  376. import doctest
  377. doctest.testmod()