fparser Reference Guide  0.0.14
utils.py
1 # Modified work Copyright (c) 2017-2022 Science and Technology
2 # Facilities Council
3 # Original work Copyright (c) 1999-2008 Pearu Peterson
4 
5 # All rights reserved.
6 
7 # Modifications made as part of the fparser project are distributed
8 # under the following license:
9 
10 # Redistribution and use in source and binary forms, with or without
11 # modification, are permitted provided that the following conditions are
12 # met:
13 
14 # 1. Redistributions of source code must retain the above copyright
15 # notice, this list of conditions and the following disclaimer.
16 
17 # 2. Redistributions in binary form must reproduce the above copyright
18 # notice, this list of conditions and the following disclaimer in the
19 # documentation and/or other materials provided with the distribution.
20 
21 # 3. Neither the name of the copyright holder nor the names of its
22 # contributors may be used to endorse or promote products derived from
23 # this software without specific prior written permission.
24 
25 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29 # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
31 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 
37 # --------------------------------------------------------------------
38 
39 # The original software (in the f2py project) was distributed under
40 # the following license:
41 
42 # Redistribution and use in source and binary forms, with or without
43 # modification, are permitted provided that the following conditions are met:
44 
45 # a. Redistributions of source code must retain the above copyright notice,
46 # this list of conditions and the following disclaimer.
47 # b. Redistributions in binary form must reproduce the above copyright
48 # notice, this list of conditions and the following disclaimer in the
49 # documentation and/or other materials provided with the distribution.
50 # c. Neither the name of the F2PY project nor the names of its
51 # contributors may be used to endorse or promote products derived from
52 # this software without specific prior written permission.
53 
54 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
55 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
56 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
57 # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
58 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
59 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
60 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
61 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
62 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
63 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
64 # DAMAGE.
65 
66 """
67 Various utility functions.
68 
69 Permission to use, modify, and distribute this software is given under the
70 terms of the NumPy License. See http://scipy.org.
71 
72 NO WARRANTY IS EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
73 Author: Pearu Peterson <pearu@cens.ioc.ee>
74 Created: May 2006
75 
76 """
77 
78 __all__ = [
79  "split_comma",
80  "specs_split_comma",
81  "ParseError",
82  "AnalyzeError",
83  "get_module_file",
84  "parse_bind",
85  "parse_result",
86  "is_name",
87  "parse_array_spec",
88  "CHAR_BIT",
89  "str2stmt",
90  "classes",
91 ]
92 
93 import logging
94 import glob
95 import io
96 import os
97 import re
98 import traceback
99 
100 
101 class ParseError(Exception):
102  pass
103 
104 
105 class AnalyzeError(Exception):
106  pass
107 
108 
109 is_name = re.compile(r"^[a-z_]\w*$", re.I).match
110 name_re = re.compile(r"[a-z_]\w*", re.I).match
111 is_entity_decl = re.compile(r"^[a-z_]\w*", re.I).match
112 is_int_literal_constant = re.compile(r"^\d+(_\w+|)$").match
113 module_file_extensions = [".f", ".f90", ".f95", ".f03", ".f08"]
114 
115 
116 def split_comma(line, item=None, comma=",", keep_empty=False, brackets=None):
117  """Split (an optionally bracketed) comma-separated list into
118  items and return a list containing them. If supplied then
119  brackets must be a list of containing two strings, the first
120  being the opening bracket and the second the closing bracket."""
121  # we may have blank space so strip the line
122  line = line.strip()
123  if not line:
124  return []
125  if brackets:
126  if not isinstance(brackets, tuple):
127  raise ParseError("split_comma: brackets must be a tuple")
128  if len(brackets) != 2:
129  raise ParseError(
130  "split_comma: brackets tuple must contain "
131  "just two items but got: {0}",
132  brackets,
133  )
134  open = brackets[0]
135  close = brackets[1]
136  if not line.startswith(open) or not line.endswith(close):
137  return []
138  line = line.strip(brackets[0])
139  line = line.strip(brackets[1])
140  items = []
141  if item is None:
142  for s in line.split(comma):
143  s = s.strip()
144  if not s and not keep_empty:
145  continue
146  items.append(s)
147  return items
148  newitem = item.copy(line, True)
149  apply_map = newitem.apply_map
150  for s in newitem.get_line().split(comma):
151  s = apply_map(s).strip()
152  if not s and not keep_empty:
153  continue
154  items.append(s)
155  return items
156 
157 
158 def extract_bracketed_list_items(line, item=None):
159  """Takes any line that contains "xxx (a,b,...) yyy" and returns
160  a list of items corresponding to a, b, ... Anything outside of
161  the parentheses is ignored. Only works for strings containing
162  a single set of parentheses."""
163  if line.count("(") > 1 or line.count(")") > 1:
164  raise ParseError(
165  "parse_bracketed_list: more than one opening/closing parenthesis "
166  "found in string '{0}'; this is not supported".format(line)
167  )
168  idx1 = line.find("(")
169  idx2 = line.rfind(")")
170  if idx1 < 0 or idx2 < 0 or idx2 < idx1:
171  raise ParseError(
172  "parse_bracketed_list: failed to find expression within "
173  "parentheses in '{0}'".format(line)
174  )
175  items = split_comma(line[idx1 : idx2 + 1], item, brackets=("(", ")"))
176  if item:
177  for idx in range(len(items)):
178  itm = item.copy(items[idx])
179  rlst = []
180  for rpart in itm.get_line().split(":"):
181  rlst.append(itm.apply_map(rpart.strip()))
182  items[idx] = rlst
183  return items
184 
185 
186 def parse_array_spec(line, item=None):
187  items = []
188  for spec in split_comma(line, item):
189  items.append(tuple(split_comma(spec, item, comma=":", keep_empty=True)))
190  return items
191 
192 
193 def specs_split_comma(line, item=None, upper=False):
194  specs0 = split_comma(line, item)
195  specs = []
196  for spec in specs0:
197  i = spec.find("=")
198  if i != -1:
199  kw = spec[:i].strip().upper()
200  v = spec[i + 1 :].strip()
201  specs.append("%s = %s" % (kw, v))
202  else:
203  if upper:
204  spec = spec.upper()
205  specs.append(spec)
206  return specs
207 
208 
209 def parse_bind(line, item=None):
210  if not line.lower().startswith("bind"):
211  return None, line
212  if item is not None:
213  newitem = item.copy(line, apply_map=True)
214  newline = newitem.get_line()
215  else:
216  newitem = None
217  newline = newline[4:].lstrip()
218  i = newline.find(")")
219  assert i != -1, repr(newline)
220  args = []
221  for a in specs_split_comma(newline[1:i].strip(), newitem, upper=True):
222  args.append(a)
223  rest = newline[i + 1 :].lstrip()
224  if item is not None:
225  rest = newitem.apply_map(rest)
226  return args, rest
227 
228 
229 def parse_result(line, item=None):
230  if not line.lower().startswith("result"):
231  return None, line
232  line = line[6:].lstrip()
233  i = line.find(")")
234  assert i != -1, repr(line)
235  name = line[1:i].strip()
236  assert is_name(name), repr(name)
237  return name, line[i + 1 :].lstrip()
238 
239 
240 def filter_stmts(content, classes):
241  """Pop and return classes instances from content."""
242  stmts = []
243  indices = []
244  for i in range(len(content)):
245  stmt = content[i]
246  if isinstance(stmt, classes):
247  stmts.append(stmt)
248  indices.append(i)
249  indices.reverse()
250  for i in indices:
251  del content[i]
252  return stmts
253 
254 
255 def get_module_files(directory, _cache={}):
256  if directory in _cache:
257  return _cache[directory]
258  module_line = re.compile(r"(\A|^)module\s+(?P<name>\w+)\s*(!.*|)$", re.I | re.M)
259  d = {}
260  files = []
261  for ext in module_file_extensions:
262  files += glob.glob(os.path.join(directory, "*" + ext))
263  for fn in files:
264  f = open(fn, "r")
265  for name in module_line.findall(f.read()):
266  name = name[1]
267  if name in d:
268  print(d[name], "already defines", name)
269  continue
270  d[name] = fn
271  _cache[directory] = d
272  return d
273 
274 
275 def get_module_file(name, directory, _cache={}):
276  fn = _cache.get(name, None)
277  if fn is not None:
278  return fn
279  if name.endswith("_module"):
280  for ext in module_file_extensions:
281  f1 = os.path.join(directory, name[:-7] + ext)
282  if os.path.isfile(f1):
283  _cache[name] = fn
284  return f1
285  files = []
286  for ext in module_file_extensions:
287  files += glob.glob(os.path.join(directory, "*" + ext))
288  for fn in files:
289  if module_in_file(name, fn):
290  _cache[name] = fn
291  return fn
292  return None
293 
294 
295 def module_in_file(name, filename):
296  name = name.lower()
297  pattern = re.compile(r"\s*module\s+(?P<name>[a-z]\w*)", re.I).match
298  encoding = {"encoding": "UTF-8"}
299  f = io.open(filename, "r", **encoding)
300  for line in f:
301  m = pattern(line)
302  if m and m.group("name").lower() == name:
303  f.close()
304  return filename
305  f.close()
306 
307 
308 def str2stmt(string, isfree=True, isstrict=False):
309  """Convert Fortran code to Statement tree."""
310  from .readfortran import Line, FortranStringReader
311  from .parsefortran import FortranParser
312 
313  reader = FortranStringReader(string, isfree, isstrict)
314  parser = FortranParser(reader)
315  parser.parse()
316  parser.analyze()
317  block = parser.block
318  while len(block.content) == 1:
319  block = block.content[0]
320  return block
321 
322 
323 def show_item_on_failure(func, _exception_depth=[0]):
324  """
325  Decorator for analyze methods.
326  """
327 
328  def new_func(self):
329  try:
330  func(self)
331  except AnalyzeError as msg:
332  clsname = self.__class__.__name__
333  self.error("%s.analyze error: %s" % (clsname, msg))
334  traceback.print_exc()
335  except ParseError as msg:
336  self.error("parse error: %s" % (msg))
337  except Exception as msg:
338  _exception_depth[0] += 1
339  if _exception_depth[0] == 1:
340  self.error("exception triggered here: %s %s" % (Exception, msg))
341  raise
342  _exception_depth[0] = 0
343 
344  return new_func
345 
346 
347 _classes_cache = {}
348 
349 
350 class meta_classes(type):
351  """Meta class for ``classes``."""
352 
353  __abstractmethods__ = False
354 
355  def __getattr__(self, name):
356  # Expose created classes only as attributes to ``classes`` type.
357  cls = _classes_cache.get(name)
358  if cls is None:
359  raise AttributeError("instance does not have attribute %r" % (name))
360  return cls
361 
362 
363 class classes(type, metaclass=meta_classes):
364  """Make classes available as attributes of this class.
365 
366  To add a class to the attributes list, one must use::
367 
368  class Name(metaclass=classes):
369 
370  in the definition of the class.
371 
372  In addition, apply the following tasks:
373 
374  * decorate analyze methods with show_item_on_failure
375  """
376 
377  def __new__(metacls, name, bases, dict):
378  if "analyze" in dict:
379  dict["analyze"] = show_item_on_failure(dict["analyze"])
380  cls = type.__new__(metacls, name, bases, dict)
381  _classes_cache[name] = cls
382  return cls