mirror of
https://github.com/cp2k/cp2k.git
synced 2026-07-21 14:35:15 -04:00
3020 lines
110 KiB
Python
Executable file
3020 lines
110 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
################################################################################
|
|
#
|
|
# fypp -- Python powered Fortran preprocessor
|
|
#
|
|
# Copyright (c) 2016-2020 Bálint Aradi, Universität Bremen
|
|
#
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
# list of conditions and the following disclaimer.
|
|
#
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
#
|
|
################################################################################
|
|
|
|
'''For using the functionality of the Fypp preprocessor from within
|
|
Python, one usually interacts with the following two classes:
|
|
|
|
* `Fypp`_: The actual Fypp preprocessor. It returns for a given input
|
|
the preprocessed output.
|
|
|
|
* `FyppOptions`_: Contains customizable settings controling the behaviour of
|
|
`Fypp`_. Alternatively, the function `get_option_parser()`_ can be used to
|
|
obtain an option parser, which can create settings based on command line
|
|
arguments.
|
|
|
|
If processing stops prematurely, an instance of one of the following
|
|
subclasses of `FyppError`_ is raised:
|
|
|
|
* FyppFatalError: Unexpected error (e.g. bad input, missing files, etc.)
|
|
|
|
* FyppStopRequest: Stop was triggered by an explicit request in the input
|
|
(by a stop- or an assert-directive).
|
|
'''
|
|
|
|
from __future__ import print_function
|
|
import sys
|
|
import types
|
|
import inspect
|
|
import re
|
|
import os
|
|
import errno
|
|
import time
|
|
import optparse
|
|
import io
|
|
if sys.version_info[0] >= 3:
|
|
import builtins
|
|
else:
|
|
import __builtin__ as builtins
|
|
|
|
# Prevent cluttering user directory with Python bytecode
|
|
sys.dont_write_bytecode = True
|
|
|
|
VERSION = '3.0'
|
|
|
|
STDIN = '<stdin>'
|
|
|
|
FILEOBJ = '<fileobj>'
|
|
|
|
STRING = '<string>'
|
|
|
|
ERROR_EXIT_CODE = 1
|
|
|
|
USER_ERROR_EXIT_CODE = 2
|
|
|
|
_ALL_DIRECTIVES_PATTERN = r'''
|
|
# comment block
|
|
(?:^[ \t]*\#!.*\n)+
|
|
|
|
|
# line directive (with optional continuation lines)
|
|
^[ \t]*(?P<ldirtype>[\#\$@]):[ \t]*
|
|
(?P<ldir>.+?(?:&[ \t]*\n(?:[ \t]*&)?.*?)*)?[ \t]*\n
|
|
|
|
|
# inline eval directive
|
|
(?P<idirtype>[$\#@])\{[ \t]*(?P<idir>.+?)?[ \t]*\}(?P=idirtype)
|
|
'''
|
|
|
|
_ALL_DIRECTIVES_REGEXP = re.compile(
|
|
_ALL_DIRECTIVES_PATTERN, re.VERBOSE | re.MULTILINE)
|
|
|
|
_CONTROL_DIR_REGEXP = re.compile(
|
|
r'(?P<dir>[a-zA-Z_]\w*)[ \t]*(?:[ \t]+(?P<param>[^ \t].*))?$')
|
|
|
|
_DIRECT_CALL_REGEXP = re.compile(
|
|
r'(?P<callname>[a-zA-Z_][\w.]*)[ \t]*\((?P<callparams>.+?)?\)$')
|
|
|
|
_DIRECT_CALL_KWARG_REGEXP = re.compile(
|
|
r'(?:(?P<kwname>[a-zA-Z_]\w*)\s*=(?=[^=]|$))?')
|
|
|
|
_DEF_PARAM_REGEXP = re.compile(
|
|
r'^(?P<name>[a-zA-Z_]\w*)[ \t]*\(\s*(?P<args>.+)?\s*\)$')
|
|
|
|
_SIMPLE_CALLABLE_REGEXP = re.compile(
|
|
r'^(?P<name>[a-zA-Z_][\w.]*)[ \t]*(?:\([ \t]*(?P<args>.*)[ \t]*\))?$')
|
|
|
|
_IDENTIFIER_NAME_REGEXP = re.compile(r'^(?P<name>[a-zA-Z_]\w*)$')
|
|
|
|
_PREFIXED_IDENTIFIER_NAME_REGEXP = re.compile(r'^(?P<name>[a-zA-Z_][\w.]*)$')
|
|
|
|
_SET_PARAM_REGEXP = re.compile(
|
|
r'^(?P<name>(?:[(]\s*)?[a-zA-Z_]\w*(?:\s*,\s*[a-zA-Z_]\w*)*(?:\s*[)])?)\s*'\
|
|
r'(?:=\s*(?P<expr>.*))?$')
|
|
|
|
_DEL_PARAM_REGEXP = re.compile(
|
|
r'^(?:[(]\s*)?[a-zA-Z_]\w*(?:\s*,\s*[a-zA-Z_]\w*)*(?:\s*[)])?$')
|
|
|
|
_FOR_PARAM_REGEXP = re.compile(
|
|
r'^(?P<loopexpr>[a-zA-Z_]\w*(\s*,\s*[a-zA-Z_]\w*)*)\s+in\s+(?P<iter>.+)$')
|
|
|
|
_INCLUDE_PARAM_REGEXP = re.compile(r'^(\'|")(?P<fname>.*?)\1$')
|
|
|
|
_COMMENTLINE_REGEXP = re.compile(r'^[ \t]*!.*$')
|
|
|
|
_CONTLINE_REGEXP = re.compile(r'&[ \t]*\n(?:[ \t]*&)?')
|
|
|
|
_UNESCAPE_TEXT_REGEXP1 = re.compile(r'([$#@])\\(\\*)([{:])')
|
|
|
|
_UNESCAPE_TEXT_REGEXP2 = re.compile(r'#\\(\\*)([!])')
|
|
|
|
_UNESCAPE_TEXT_REGEXP3 = re.compile(r'(\})\\(\\*)([$#@])')
|
|
|
|
_INLINE_EVAL_REGION_REGEXP = re.compile(r'\${.*?}\$')
|
|
|
|
_RESERVED_PREFIX = '__'
|
|
|
|
_RESERVED_NAMES = set(['defined', 'setvar', 'getvar', 'delvar', 'globalvar',
|
|
'_LINE_', '_FILE_', '_THIS_FILE_', '_THIS_LINE_',
|
|
'_TIME_', '_DATE_'])
|
|
|
|
_LINENUM_NEW_FILE = 1
|
|
|
|
_LINENUM_RETURN_TO_FILE = 2
|
|
|
|
_QUOTES_FORTRAN = '\'"'
|
|
|
|
_OPENING_BRACKETS_FORTRAN = '{(['
|
|
|
|
_CLOSING_BRACKETS_FORTRAN = '})]'
|
|
|
|
_ARGUMENT_SPLIT_CHAR_FORTRAN = ','
|
|
|
|
|
|
class FyppError(Exception):
|
|
'''Signalizes error occuring during preprocessing.
|
|
|
|
Args:
|
|
msg (str): Error message.
|
|
fname (str): File name. None (default) if file name is not available.
|
|
span (tuple of int): Beginning and end line of the region where error
|
|
occured or None if not available. If fname was not None, span must
|
|
not be None.
|
|
cause (Exception): Contains the exception, which triggered this
|
|
exception or None, if this exception is not masking any underlying
|
|
one. (Emulates Python 3 exception chaining in a Python 2 compatible
|
|
way.)
|
|
|
|
Attributes:
|
|
msg (str): Error message.
|
|
fname (str or None): File name or None if not available.
|
|
span (tuple of int or None): Beginning and end line of the region
|
|
where error occured or None if not available. Line numbers start
|
|
from zero. For directives, which do not consume end of the line,
|
|
start and end lines are identical.
|
|
cause (Exception): In case this exception is raised in an except block,
|
|
the original exception should be passed here. (Emulates Python 3
|
|
exception chaining in a Python 2 compatible way.)
|
|
'''
|
|
|
|
def __init__(self, msg, fname=None, span=None, cause=None):
|
|
super(FyppError, self).__init__()
|
|
self.msg = msg
|
|
self.fname = fname
|
|
self.span = span
|
|
self.cause = cause
|
|
|
|
|
|
def __str__(self):
|
|
msg = [self.__class__.__name__, ': ']
|
|
if self.fname is not None:
|
|
msg.append("file '" + self.fname + "'")
|
|
if self.span[1] > self.span[0] + 1:
|
|
msg.append(', lines {0}-{1}'.format(
|
|
self.span[0] + 1, self.span[1]))
|
|
else:
|
|
msg.append(', line {0}'.format(self.span[0] + 1))
|
|
msg.append('\n')
|
|
if self.msg:
|
|
msg.append(self.msg)
|
|
if self.cause is not None:
|
|
msg.append('\n' + str(self.cause))
|
|
return ''.join(msg)
|
|
|
|
|
|
class FyppFatalError(FyppError):
|
|
'''Signalizes an unexpected error during processing.'''
|
|
|
|
|
|
class FyppStopRequest(FyppError):
|
|
'''Signalizes an explicitely triggered stop (e.g. via stop directive)'''
|
|
|
|
|
|
class Parser:
|
|
'''Parses a text and generates events when encountering Fypp constructs.
|
|
|
|
Args:
|
|
includedirs (list): List of directories, in which include files should
|
|
be searched for, when they are not found at the default location.
|
|
|
|
encoding (str): Encoding to use when reading the file (default: utf-8)
|
|
'''
|
|
|
|
def __init__(self, includedirs=None, encoding='utf-8'):
|
|
|
|
# Directories to search for include files
|
|
if includedirs is None:
|
|
self._includedirs = []
|
|
else:
|
|
self._includedirs = includedirs
|
|
|
|
# Encoding
|
|
self._encoding = encoding
|
|
|
|
# Name of current file
|
|
self._curfile = None
|
|
|
|
# Directory of current file
|
|
self._curdir = None
|
|
|
|
|
|
def parsefile(self, fobj):
|
|
'''Parses file or a file like object.
|
|
|
|
Args:
|
|
fobj (str or file): Name of a file or a file like object.
|
|
'''
|
|
if isinstance(fobj, str):
|
|
if fobj == STDIN:
|
|
self._includefile(None, sys.stdin, STDIN, os.getcwd())
|
|
else:
|
|
inpfp = _open_input_file(fobj, self._encoding)
|
|
self._includefile(None, inpfp, fobj, os.path.dirname(fobj))
|
|
inpfp.close()
|
|
else:
|
|
self._includefile(None, fobj, FILEOBJ, os.getcwd())
|
|
|
|
|
|
def _includefile(self, span, fobj, fname, curdir):
|
|
oldfile = self._curfile
|
|
olddir = self._curdir
|
|
self._curfile = fname
|
|
self._curdir = curdir
|
|
self._parse_txt(span, fname, fobj.read())
|
|
self._curfile = oldfile
|
|
self._curdir = olddir
|
|
|
|
|
|
def parse(self, txt):
|
|
'''Parses string.
|
|
|
|
Args:
|
|
txt (str): Text to parse.
|
|
'''
|
|
self._curfile = STRING
|
|
self._curdir = ''
|
|
self._parse_txt(None, self._curfile, txt)
|
|
|
|
|
|
def handle_include(self, span, fname):
|
|
'''Called when parser starts to process a new file.
|
|
|
|
It is a dummy methond and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the include directive
|
|
or None if called the first time for the main input.
|
|
fname (str): Name of the file.
|
|
'''
|
|
self._log_event('include', span, filename=fname)
|
|
|
|
|
|
def handle_endinclude(self, span, fname):
|
|
'''Called when parser finished processing a file.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the include directive
|
|
or None if called the first time for the main input.
|
|
fname (str): Name of the file.
|
|
'''
|
|
self._log_event('endinclude', span, filename=fname)
|
|
|
|
|
|
def handle_set(self, span, name, expr):
|
|
'''Called when parser encounters a set directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the variable.
|
|
expr (str): String representation of the expression to be assigned
|
|
to the variable.
|
|
'''
|
|
self._log_event('set', span, name=name, expression=expr)
|
|
|
|
|
|
def handle_def(self, span, name, args):
|
|
'''Called when parser encounters a def directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the macro to be defined.
|
|
argexpr (str): String with argument definition (or None)
|
|
'''
|
|
self._log_event('def', span, name=name, arguments=args)
|
|
|
|
|
|
def handle_enddef(self, span, name):
|
|
'''Called when parser encounters an enddef directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name found after the enddef directive.
|
|
'''
|
|
self._log_event('enddef', span, name=name)
|
|
|
|
|
|
def handle_del(self, span, name):
|
|
'''Called when parser encounters a del directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the variable to delete.
|
|
'''
|
|
self._log_event('del', span, name=name)
|
|
|
|
|
|
def handle_if(self, span, cond):
|
|
'''Called when parser encounters an if directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
cond (str): String representation of the branching condition.
|
|
'''
|
|
self._log_event('if', span, condition=cond)
|
|
|
|
|
|
def handle_elif(self, span, cond):
|
|
'''Called when parser encounters an elif directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
cond (str): String representation of the branching condition.
|
|
'''
|
|
self._log_event('elif', span, condition=cond)
|
|
|
|
|
|
def handle_else(self, span):
|
|
'''Called when parser encounters an else directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('else', span)
|
|
|
|
|
|
def handle_endif(self, span):
|
|
'''Called when parser encounters an endif directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('endif', span)
|
|
|
|
|
|
def handle_for(self, span, varexpr, iterator):
|
|
'''Called when parser encounters a for directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
varexpr (str): String representation of the loop variable
|
|
expression.
|
|
iterator (str): String representation of the iterable.
|
|
'''
|
|
self._log_event('for', span, variable=varexpr, iterable=iterator)
|
|
|
|
|
|
def handle_endfor(self, span):
|
|
'''Called when parser encounters an endfor directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('endfor', span)
|
|
|
|
|
|
def handle_call(self, span, name, argexpr, blockcall):
|
|
'''Called when parser encounters a call directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the callable to call
|
|
argexpr (str or None): Argument expression containing additional
|
|
arguments for the call.
|
|
blockcall (bool): Whether the alternative "block / contains /
|
|
endblock" calling directive has been used.
|
|
'''
|
|
self._log_event('call', span, name=name, argexpr=argexpr,
|
|
blockcall=blockcall)
|
|
|
|
|
|
def handle_nextarg(self, span, name, blockcall):
|
|
'''Called when parser encounters a nextarg directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str or None): Name of the argument following next or
|
|
None if it should be the next positional argument.
|
|
blockcall (bool): Whether the alternative "block / contains /
|
|
endblock" calling directive has been used.
|
|
'''
|
|
self._log_event('nextarg', span, name=name, blockcall=blockcall)
|
|
|
|
|
|
def handle_endcall(self, span, name, blockcall):
|
|
'''Called when parser encounters an endcall directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name found after the endcall directive.
|
|
blockcall (bool): Whether the alternative "block / contains /
|
|
endblock" calling directive has been used.
|
|
'''
|
|
self._log_event('endcall', span, name=name, blockcall=blockcall)
|
|
|
|
|
|
def handle_eval(self, span, expr):
|
|
'''Called when parser encounters an eval directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
expr (str): String representation of the Python expression to
|
|
be evaluated.
|
|
'''
|
|
self._log_event('eval', span, expression=expr)
|
|
|
|
|
|
def handle_global(self, span, name):
|
|
'''Called when parser encounters a global directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the variable which should be made global.
|
|
'''
|
|
self._log_event('global', span, name=name)
|
|
|
|
|
|
def handle_text(self, span, txt):
|
|
'''Called when parser finds text which must left unaltered.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
txt (str): Text.
|
|
'''
|
|
self._log_event('text', span, content=txt)
|
|
|
|
|
|
def handle_comment(self, span):
|
|
'''Called when parser finds a preprocessor comment.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('comment', span)
|
|
|
|
|
|
def handle_mute(self, span):
|
|
'''Called when parser finds a mute directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('mute', span)
|
|
|
|
|
|
def handle_endmute(self, span):
|
|
'''Called when parser finds an endmute directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('endmute', span)
|
|
|
|
|
|
def handle_stop(self, span, msg):
|
|
'''Called when parser finds an stop directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
msg (str): Stop message.
|
|
'''
|
|
self._log_event('stop', span, msg=msg)
|
|
|
|
|
|
def handle_assert(self, span):
|
|
'''Called when parser finds an assert directive.
|
|
|
|
It is a dummy method and should be overriden for actual use.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._log_event('assert', span)
|
|
|
|
|
|
@staticmethod
|
|
def _log_event(event, span=(-1, -1), **params):
|
|
print('{0}: {1} --> {2}'.format(event, span[0], span[1]))
|
|
for parname, parvalue in params.items():
|
|
print(' {0}: ->|{1}|<-'.format(parname, parvalue))
|
|
print()
|
|
|
|
|
|
def _parse_txt(self, includespan, fname, txt):
|
|
self.handle_include(includespan, fname)
|
|
self._parse(txt)
|
|
self.handle_endinclude(includespan, fname)
|
|
|
|
|
|
def _parse(self, txt, linenr=0, directcall=False):
|
|
pos = 0
|
|
for match in _ALL_DIRECTIVES_REGEXP.finditer(txt):
|
|
start, end = match.span()
|
|
if start > pos:
|
|
endlinenr = linenr + txt.count('\n', pos, start)
|
|
self._process_text(txt[pos:start], (linenr, endlinenr))
|
|
linenr = endlinenr
|
|
endlinenr = linenr + txt.count('\n', start, end)
|
|
span = (linenr, endlinenr)
|
|
ldirtype, ldir, idirtype, idir = match.groups()
|
|
if directcall and (idirtype is None or idirtype != '$'):
|
|
msg = 'only inline eval directives allowed in direct calls'
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
elif idirtype is not None:
|
|
if idir is None:
|
|
msg = 'missing inline directive content'
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
dirtype = idirtype
|
|
content = idir
|
|
elif ldirtype is not None:
|
|
if ldir is None:
|
|
msg = 'missing line directive content'
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
dirtype = ldirtype
|
|
content = _CONTLINE_REGEXP.sub('', ldir)
|
|
else:
|
|
# Comment directive
|
|
dirtype = None
|
|
if dirtype == '$':
|
|
self.handle_eval(span, content)
|
|
elif dirtype == '#':
|
|
self._process_control_dir(content, span)
|
|
elif dirtype == '@':
|
|
self._process_direct_call(content, span)
|
|
else:
|
|
self.handle_comment(span)
|
|
pos = end
|
|
linenr = endlinenr
|
|
if pos < len(txt):
|
|
endlinenr = linenr + txt.count('\n', pos)
|
|
self._process_text(txt[pos:], (linenr, endlinenr))
|
|
|
|
|
|
def _process_text(self, txt, span):
|
|
escaped_txt = self._unescape(txt)
|
|
self.handle_text(span, escaped_txt)
|
|
|
|
|
|
def _process_control_dir(self, content, span):
|
|
match = _CONTROL_DIR_REGEXP.match(content)
|
|
if not match:
|
|
msg = "invalid control directive content '{0}'".format(content)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
directive, param = match.groups()
|
|
if directive == 'if':
|
|
self._check_param_presence(True, 'if', param, span)
|
|
self.handle_if(span, param)
|
|
elif directive == 'else':
|
|
self._check_param_presence(False, 'else', param, span)
|
|
self.handle_else(span)
|
|
elif directive == 'elif':
|
|
self._check_param_presence(True, 'elif', param, span)
|
|
self.handle_elif(span, param)
|
|
elif directive == 'endif':
|
|
self._check_param_presence(False, 'endif', param, span)
|
|
self.handle_endif(span)
|
|
elif directive == 'def':
|
|
self._check_param_presence(True, 'def', param, span)
|
|
self._check_not_inline_directive('def', span)
|
|
self._process_def(param, span)
|
|
elif directive == 'enddef':
|
|
self._process_enddef(param, span)
|
|
elif directive == 'set':
|
|
self._check_param_presence(True, 'set', param, span)
|
|
self._process_set(param, span)
|
|
elif directive == 'del':
|
|
self._check_param_presence(True, 'del', param, span)
|
|
self._process_del(param, span)
|
|
elif directive == 'for':
|
|
self._check_param_presence(True, 'for', param, span)
|
|
self._process_for(param, span)
|
|
elif directive == 'endfor':
|
|
self._check_param_presence(False, 'endfor', param, span)
|
|
self.handle_endfor(span)
|
|
elif directive == 'call' or directive == 'block':
|
|
self._check_param_presence(True, directive, param, span)
|
|
self._process_call(param, span, directive == 'block')
|
|
elif directive == 'nextarg' or directive == 'contains':
|
|
self._process_nextarg(param, span, directive == 'contains')
|
|
elif directive == 'endcall' or directive == 'endblock':
|
|
self._process_endcall(param, span, directive == 'endblock')
|
|
elif directive == 'include':
|
|
self._check_param_presence(True, 'include', param, span)
|
|
self._check_not_inline_directive('include', span)
|
|
self._process_include(param, span)
|
|
elif directive == 'mute':
|
|
self._check_param_presence(False, 'mute', param, span)
|
|
self._check_not_inline_directive('mute', span)
|
|
self.handle_mute(span)
|
|
elif directive == 'endmute':
|
|
self._check_param_presence(False, 'endmute', param, span)
|
|
self._check_not_inline_directive('endmute', span)
|
|
self.handle_endmute(span)
|
|
elif directive == 'stop':
|
|
self._check_param_presence(True, 'stop', param, span)
|
|
self._check_not_inline_directive('stop', span)
|
|
self.handle_stop(span, param)
|
|
elif directive == 'assert':
|
|
self._check_param_presence(True, 'assert', param, span)
|
|
self._check_not_inline_directive('assert', span)
|
|
self.handle_assert(span, param)
|
|
elif directive == 'global':
|
|
self._check_param_presence(True, 'global', param, span)
|
|
self._process_global(param, span)
|
|
else:
|
|
msg = "unknown directive '{0}'".format(directive)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
|
|
|
|
def _process_direct_call(self, callexpr, span):
|
|
match = _DIRECT_CALL_REGEXP.match(callexpr)
|
|
if not match:
|
|
msg = "invalid direct call expression"
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
callname = match.group('callname')
|
|
self.handle_call(span, callname, None, False)
|
|
callparams = match.group('callparams')
|
|
if callparams is None or not callparams.strip():
|
|
args = []
|
|
else:
|
|
try:
|
|
args = [arg.strip() for arg in _argsplit_fortran(callparams)]
|
|
except Exception as exc:
|
|
msg = 'unable to parse direct call argument'
|
|
raise FyppFatalError(msg, self._curfile, span, exc)
|
|
for arg in args:
|
|
match = _DIRECT_CALL_KWARG_REGEXP.match(arg)
|
|
argval = arg[match.end():].strip()
|
|
# Remove enclosing braces if present
|
|
if argval.startswith('{'):
|
|
argval = argval[1:-1]
|
|
keyword = match.group('kwname')
|
|
self.handle_nextarg(span, keyword, False)
|
|
self._parse(argval, linenr=span[0], directcall=True)
|
|
self.handle_endcall(span, callname, False)
|
|
|
|
|
|
def _process_def(self, param, span):
|
|
match = _DEF_PARAM_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid macro definition '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
name = match.group('name')
|
|
argexpr = match.group('args')
|
|
self.handle_def(span, name, argexpr)
|
|
|
|
|
|
def _process_enddef(self, param, span):
|
|
if param is not None:
|
|
match = _IDENTIFIER_NAME_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid enddef parameter '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
param = match.group('name')
|
|
self.handle_enddef(span, param)
|
|
|
|
|
|
def _process_set(self, param, span):
|
|
match = _SET_PARAM_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid variable assignment '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
self.handle_set(span, match.group('name'), match.group('expr'))
|
|
|
|
|
|
def _process_global(self, param, span):
|
|
match = _DEL_PARAM_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid variable specification '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
self.handle_global(span, param)
|
|
|
|
|
|
def _process_del(self, param, span):
|
|
match = _DEL_PARAM_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid variable specification '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
self.handle_del(span, param)
|
|
|
|
|
|
def _process_for(self, param, span):
|
|
match = _FOR_PARAM_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid for loop declaration '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
loopexpr = match.group('loopexpr')
|
|
loopvars = [s.strip() for s in loopexpr.split(',')]
|
|
self.handle_for(span, loopvars, match.group('iter'))
|
|
|
|
|
|
def _process_call(self, param, span, blockcall):
|
|
match = _SIMPLE_CALLABLE_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid callable expression '{}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
name, args = match.groups()
|
|
self.handle_call(span, name, args, blockcall)
|
|
|
|
|
|
def _process_nextarg(self, param, span, blockcall):
|
|
if param is not None:
|
|
match = _IDENTIFIER_NAME_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid nextarg parameter '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
param = match.group('name')
|
|
self.handle_nextarg(span, param, blockcall)
|
|
|
|
|
|
def _process_endcall(self, param, span, blockcall):
|
|
if param is not None:
|
|
match = _PREFIXED_IDENTIFIER_NAME_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid endcall parameter '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
param = match.group('name')
|
|
self.handle_endcall(span, param, blockcall)
|
|
|
|
|
|
def _process_include(self, param, span):
|
|
match = _INCLUDE_PARAM_REGEXP.match(param)
|
|
if not match:
|
|
msg = "invalid include file declaration '{0}'".format(param)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
fname = match.group('fname')
|
|
for incdir in [self._curdir] + self._includedirs:
|
|
fpath = os.path.join(incdir, fname)
|
|
if os.path.exists(fpath):
|
|
break
|
|
else:
|
|
msg = "include file '{0}' not found".format(fname)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
inpfp = _open_input_file(fpath, self._encoding)
|
|
self._includefile(span, inpfp, fpath, os.path.dirname(fpath))
|
|
inpfp.close()
|
|
|
|
|
|
def _process_mute(self, span):
|
|
if span[0] == span[1]:
|
|
msg = 'Inline form of mute directive not allowed'
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
self.handle_mute(span)
|
|
|
|
|
|
def _process_endmute(self, span):
|
|
if span[0] == span[1]:
|
|
msg = 'Inline form of endmute directive not allowed'
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
self.handle_endmute(span)
|
|
|
|
|
|
def _check_param_presence(self, presence, directive, param, span):
|
|
if (param is not None) != presence:
|
|
if presence:
|
|
msg = 'missing data in {0} directive'.format(directive)
|
|
else:
|
|
msg = 'forbidden data in {0} directive'.format(directive)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
|
|
|
|
def _check_not_inline_directive(self, directive, span):
|
|
if span[0] == span[1]:
|
|
msg = 'Inline form of {0} directive not allowed'.format(directive)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
|
|
|
|
@staticmethod
|
|
def _unescape(txt):
|
|
txt = _UNESCAPE_TEXT_REGEXP1.sub(r'\1\2\3', txt)
|
|
txt = _UNESCAPE_TEXT_REGEXP2.sub(r'#\1\2', txt)
|
|
txt = _UNESCAPE_TEXT_REGEXP3.sub(r'\1\2\3', txt)
|
|
return txt
|
|
|
|
|
|
class Builder:
|
|
'''Builds a tree representing a text with preprocessor directives.
|
|
'''
|
|
|
|
def __init__(self):
|
|
# The tree, which should be built.
|
|
self._tree = []
|
|
|
|
# List of all open constructs
|
|
self._open_blocks = []
|
|
|
|
# Nodes to which the open blocks have to be appended when closed
|
|
self._path = []
|
|
|
|
# Nr. of open blocks when file was opened. Used for checking whether all
|
|
# blocks have been closed, when file processing finishes.
|
|
self._nr_prev_blocks = []
|
|
|
|
# Current node, to which content should be added
|
|
self._curnode = self._tree
|
|
|
|
# Current file
|
|
self._curfile = None
|
|
|
|
|
|
def reset(self):
|
|
'''Resets the builder so that it starts to build a new tree.'''
|
|
self._tree = []
|
|
self._open_blocks = []
|
|
self._path = []
|
|
self._nr_prev_blocks = []
|
|
self._curnode = self._tree
|
|
self._curfile = None
|
|
|
|
|
|
def handle_include(self, span, fname):
|
|
'''Should be called to signalize change to new file.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the include directive
|
|
or None if called the first time for the main input.
|
|
fname (str): Name of the file to be included.
|
|
'''
|
|
self._path.append(self._curnode)
|
|
self._curnode = []
|
|
self._open_blocks.append(
|
|
('include', self._curfile, [span], fname, None))
|
|
self._curfile = fname
|
|
self._nr_prev_blocks.append(len(self._open_blocks))
|
|
|
|
|
|
def handle_endinclude(self, span, fname):
|
|
'''Should be called when processing of a file finished.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the include directive
|
|
or None if called the first time for the main input.
|
|
fname (str): Name of the file which has been included.
|
|
'''
|
|
nprev_blocks = self._nr_prev_blocks.pop(-1)
|
|
if len(self._open_blocks) > nprev_blocks:
|
|
directive, fname, spans = self._open_blocks[-1][0:3]
|
|
msg = '{0} directive still unclosed when reaching end of file'\
|
|
.format(directive)
|
|
raise FyppFatalError(msg, self._curfile, spans[0])
|
|
block = self._open_blocks.pop(-1)
|
|
directive, blockfname, spans = block[0:3]
|
|
if directive != 'include':
|
|
msg = 'internal error: last open block is not \'include\' when '\
|
|
'closing file \'{0}\''.format(fname)
|
|
raise FyppFatalError(msg)
|
|
if span != spans[0]:
|
|
msg = 'internal error: span for include and endinclude differ ('\
|
|
'{0} vs {1}'.format(span, spans[0])
|
|
raise FyppFatalError(msg)
|
|
oldfname, _ = block[3:5]
|
|
if fname != oldfname:
|
|
msg = 'internal error: mismatching file name in close_file event'\
|
|
" (expected: '{0}', got: '{1}')".format(oldfname, fname)
|
|
raise FyppFatalError(msg, fname)
|
|
block = directive, blockfname, spans, fname, self._curnode
|
|
self._curnode = self._path.pop(-1)
|
|
self._curnode.append(block)
|
|
self._curfile = blockfname
|
|
|
|
|
|
def handle_if(self, span, cond):
|
|
'''Should be called to signalize an if directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
param (str): String representation of the branching condition.
|
|
'''
|
|
self._path.append(self._curnode)
|
|
self._curnode = []
|
|
self._open_blocks.append(('if', self._curfile, [span], [cond], []))
|
|
|
|
|
|
def handle_elif(self, span, cond):
|
|
'''Should be called to signalize an elif directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
cond (str): String representation of the branching condition.
|
|
'''
|
|
self._check_for_open_block(span, 'elif')
|
|
block = self._open_blocks[-1]
|
|
directive, _, spans = block[0:3]
|
|
self._check_if_matches_last(directive, 'if', spans[-1], span, 'elif')
|
|
conds, contents = block[3:5]
|
|
conds.append(cond)
|
|
contents.append(self._curnode)
|
|
spans.append(span)
|
|
self._curnode = []
|
|
|
|
|
|
def handle_else(self, span):
|
|
'''Should be called to signalize an else directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._check_for_open_block(span, 'else')
|
|
block = self._open_blocks[-1]
|
|
directive, _, spans = block[0:3]
|
|
self._check_if_matches_last(directive, 'if', spans[-1], span, 'else')
|
|
conds, contents = block[3:5]
|
|
conds.append('True')
|
|
contents.append(self._curnode)
|
|
spans.append(span)
|
|
self._curnode = []
|
|
|
|
|
|
def handle_endif(self, span):
|
|
'''Should be called to signalize an endif directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._check_for_open_block(span, 'endif')
|
|
block = self._open_blocks.pop(-1)
|
|
directive, _, spans = block[0:3]
|
|
self._check_if_matches_last(directive, 'if', spans[-1], span, 'endif')
|
|
_, contents = block[3:5]
|
|
contents.append(self._curnode)
|
|
spans.append(span)
|
|
self._curnode = self._path.pop(-1)
|
|
self._curnode.append(block)
|
|
|
|
|
|
def handle_for(self, span, loopvar, iterator):
|
|
'''Should be called to signalize a for directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
varexpr (str): String representation of the loop variable
|
|
expression.
|
|
iterator (str): String representation of the iterable.
|
|
'''
|
|
self._path.append(self._curnode)
|
|
self._curnode = []
|
|
self._open_blocks.append(('for', self._curfile, [span], loopvar,
|
|
iterator, None))
|
|
|
|
|
|
def handle_endfor(self, span):
|
|
'''Should be called to signalize an endfor directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._check_for_open_block(span, 'endfor')
|
|
block = self._open_blocks.pop(-1)
|
|
directive, fname, spans = block[0:3]
|
|
self._check_if_matches_last(directive, 'for', spans[-1], span, 'endfor')
|
|
loopvar, iterator, dummy = block[3:6]
|
|
spans.append(span)
|
|
block = (directive, fname, spans, loopvar, iterator, self._curnode)
|
|
self._curnode = self._path.pop(-1)
|
|
self._curnode.append(block)
|
|
|
|
|
|
def handle_def(self, span, name, argexpr):
|
|
'''Should be called to signalize a def directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the macro to be defined.
|
|
argexpr (str): Macro argument definition or None
|
|
'''
|
|
self._path.append(self._curnode)
|
|
self._curnode = []
|
|
defblock = ('def', self._curfile, [span], name, argexpr, None)
|
|
self._open_blocks.append(defblock)
|
|
|
|
|
|
def handle_enddef(self, span, name):
|
|
'''Should be called to signalize an enddef directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the enddef statement. Could be None, if enddef
|
|
was specified without name.
|
|
'''
|
|
self._check_for_open_block(span, 'enddef')
|
|
block = self._open_blocks.pop(-1)
|
|
directive, fname, spans = block[0:3]
|
|
self._check_if_matches_last(directive, 'def', spans[-1], span, 'enddef')
|
|
defname, argexpr, dummy = block[3:6]
|
|
if name is not None and name != defname:
|
|
msg = "wrong name in enddef directive "\
|
|
"(expected '{0}', got '{1}')".format(defname, name)
|
|
raise FyppFatalError(msg, fname, span)
|
|
spans.append(span)
|
|
block = (directive, fname, spans, defname, argexpr, self._curnode)
|
|
self._curnode = self._path.pop(-1)
|
|
self._curnode.append(block)
|
|
|
|
|
|
def handle_call(self, span, name, argexpr, blockcall):
|
|
'''Should be called to signalize a call directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the callable to call
|
|
argexpr (str or None): Argument expression containing additional
|
|
arguments for the call.
|
|
blockcall (bool): Whether the alternative "block / contains /
|
|
endblock" calling directive has been used.
|
|
'''
|
|
self._path.append(self._curnode)
|
|
self._curnode = []
|
|
directive = 'block' if blockcall else 'call'
|
|
self._open_blocks.append(
|
|
(directive, self._curfile, [span, span], name, argexpr, [], []))
|
|
|
|
|
|
def handle_nextarg(self, span, name, blockcall):
|
|
'''Should be called to signalize a nextarg directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str or None): Name of the argument following next or
|
|
None if it should be the next positional argument.
|
|
blockcall (bool): Whether the alternative "block / contains /
|
|
endblock" calling directive has been used.
|
|
'''
|
|
self._check_for_open_block(span, 'nextarg')
|
|
block = self._open_blocks[-1]
|
|
directive, fname, spans = block[0:3]
|
|
if blockcall:
|
|
opened, current = 'block', 'contains'
|
|
else:
|
|
opened, current = 'call', 'nextarg'
|
|
self._check_if_matches_last(directive, opened, spans[-1], span, current)
|
|
args, argnames = block[5:7]
|
|
args.append(self._curnode)
|
|
spans.append(span)
|
|
if name is not None:
|
|
argnames.append(name)
|
|
elif argnames:
|
|
msg = 'non-keyword argument following keyword argument'
|
|
raise FyppFatalError(msg, fname, span)
|
|
self._curnode = []
|
|
|
|
|
|
def handle_endcall(self, span, name, blockcall):
|
|
'''Should be called to signalize an endcall directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the endcall statement. Could be None, if endcall
|
|
was specified without name.
|
|
blockcall (bool): Whether the alternative "block / contains /
|
|
endblock" calling directive has been used.
|
|
'''
|
|
self._check_for_open_block(span, 'endcall')
|
|
block = self._open_blocks.pop(-1)
|
|
directive, fname, spans = block[0:3]
|
|
callname, callargexpr, args, argnames = block[3:7]
|
|
if blockcall:
|
|
opened, current = 'block', 'endblock'
|
|
else:
|
|
opened, current = 'call', 'endcall'
|
|
self._check_if_matches_last(directive, opened, spans[0], span, current)
|
|
|
|
if name is not None and name != callname:
|
|
msg = "wrong name in {0} directive "\
|
|
"(expected '{1}', got '{2}')".format(current, callname, name)
|
|
raise FyppFatalError(msg, fname, span)
|
|
args.append(self._curnode)
|
|
# If nextarg or endcall immediately followed call, then first argument
|
|
# is empty and should be removed (to allow for calls without arguments
|
|
# and named first argument in calls)
|
|
if args and not args[0]:
|
|
if len(argnames) == len(args):
|
|
del argnames[0]
|
|
del args[0]
|
|
del spans[1]
|
|
spans.append(span)
|
|
block = (directive, fname, spans, callname, callargexpr, args, argnames)
|
|
self._curnode = self._path.pop(-1)
|
|
self._curnode.append(block)
|
|
|
|
|
|
def handle_set(self, span, name, expr):
|
|
'''Should be called to signalize a set directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the variable.
|
|
expr (str): String representation of the expression to be assigned
|
|
to the variable.
|
|
'''
|
|
self._curnode.append(('set', self._curfile, span, name, expr))
|
|
|
|
|
|
def handle_global(self, span, name):
|
|
'''Should be called to signalize a global directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the variable(s) to make global.
|
|
'''
|
|
self._curnode.append(('global', self._curfile, span, name))
|
|
|
|
|
|
def handle_del(self, span, name):
|
|
'''Should be called to signalize a del directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
name (str): Name of the variable(s) to delete.
|
|
'''
|
|
self._curnode.append(('del', self._curfile, span, name))
|
|
|
|
|
|
def handle_eval(self, span, expr):
|
|
'''Should be called to signalize an eval directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
expr (str): String representation of the Python expression to
|
|
be evaluated.
|
|
'''
|
|
self._curnode.append(('eval', self._curfile, span, expr))
|
|
|
|
|
|
def handle_comment(self, span):
|
|
'''Should be called to signalize a comment directive.
|
|
|
|
The content of the comment is not needed by the builder, but it needs
|
|
the span of the comment to generate proper line numbers if needed.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._curnode.append(('comment', self._curfile, span))
|
|
|
|
|
|
def handle_text(self, span, txt):
|
|
'''Should be called to pass text which goes to output unaltered.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the text.
|
|
txt (str): Text.
|
|
'''
|
|
self._curnode.append(('txt', self._curfile, span, txt))
|
|
|
|
|
|
def handle_mute(self, span):
|
|
'''Should be called to signalize a mute directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._path.append(self._curnode)
|
|
self._curnode = []
|
|
self._open_blocks.append(('mute', self._curfile, [span], None))
|
|
|
|
|
|
def handle_endmute(self, span):
|
|
'''Should be called to signalize an endmute directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._check_for_open_block(span, 'endmute')
|
|
block = self._open_blocks.pop(-1)
|
|
directive, fname, spans = block[0:3]
|
|
self._check_if_matches_last(directive, 'mute', spans[-1], span,
|
|
'endmute')
|
|
spans.append(span)
|
|
block = (directive, fname, spans, self._curnode)
|
|
self._curnode = self._path.pop(-1)
|
|
self._curnode.append(block)
|
|
|
|
|
|
def handle_stop(self, span, msg):
|
|
'''Should be called to signalize a stop directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._curnode.append(('stop', self._curfile, span, msg))
|
|
|
|
|
|
def handle_assert(self, span, cond):
|
|
'''Should be called to signalize an assert directive.
|
|
|
|
Args:
|
|
span (tuple of int): Start and end line of the directive.
|
|
'''
|
|
self._curnode.append(('assert', self._curfile, span, cond))
|
|
|
|
|
|
@property
|
|
def tree(self):
|
|
'''Returns the tree built by the Builder.'''
|
|
return self._tree
|
|
|
|
|
|
def _check_for_open_block(self, span, directive):
|
|
if len(self._open_blocks) <= self._nr_prev_blocks[-1]:
|
|
msg = 'unexpected {0} directive'.format(directive)
|
|
raise FyppFatalError(msg, self._curfile, span)
|
|
|
|
|
|
def _check_if_matches_last(self, lastdir, curdir, lastspan, curspan,
|
|
directive):
|
|
if curdir != lastdir:
|
|
msg = "mismatching '{0}' directive (last block opened was '{1}')"\
|
|
.format(directive, lastdir)
|
|
raise FyppFatalError(msg, self._curfile, curspan)
|
|
inline_last = lastspan[0] == lastspan[1]
|
|
inline_cur = curspan[0] == curspan[1]
|
|
if inline_last != inline_cur:
|
|
if inline_cur:
|
|
msg = 'expecting line form of directive {0}'.format(directive)
|
|
else:
|
|
msg = 'expecting inline form of directive {0}'.format(directive)
|
|
raise FyppFatalError(msg, self._curfile, curspan)
|
|
elif inline_cur and curspan[0] != lastspan[0]:
|
|
msg = 'inline directives of the same construct must be in the '\
|
|
'same row'
|
|
raise FyppFatalError(msg, self._curfile, curspan)
|
|
|
|
|
|
class Renderer:
|
|
|
|
''''Renders a tree.
|
|
|
|
Args:
|
|
evaluator (Evaluator, optional): Evaluator to use when rendering eval
|
|
directives. If None (default), Evaluator() is used.
|
|
linenums (bool, optional): Whether linenums should be generated,
|
|
defaults to False.
|
|
contlinenums (bool, optional): Whether linenums for continuation
|
|
should be generated, defaults to False.
|
|
linenumformat (str, optional): If set to "gfortran5", a workaround
|
|
for broken gfortran versions (version 5.1 and above) is applied when
|
|
emitting line numbering directives.
|
|
linefolder (callable): Callable to use when folding a line.
|
|
'''
|
|
|
|
def __init__(self, evaluator=None, linenums=False, contlinenums=False,
|
|
linenumformat=None, linefolder=None):
|
|
# Evaluator to use for Python expressions
|
|
self._evaluator = Evaluator() if evaluator is None else evaluator
|
|
|
|
# Whether rendered output is diverted and will be processed
|
|
# further before output (if True: no line numbering and post processing)
|
|
self._diverted = False
|
|
|
|
# Whether file name and line numbers should be kept fixed and
|
|
# not updated (typically when rendering macro content)
|
|
self._fixedposition = False
|
|
|
|
# Whether line numbering directives should be emitted
|
|
self._linenums = linenums
|
|
|
|
# Whether line numbering directives in continuation lines are needed.
|
|
self._contlinenums = contlinenums
|
|
|
|
# Whether to use the fix for GFortran in the line numbering directives
|
|
self._linenum_gfortran5 = (linenumformat == 'gfortran5')
|
|
|
|
# Callable to be used for folding lines
|
|
if linefolder is None:
|
|
self._linefolder = lambda line: [line]
|
|
else:
|
|
self._linefolder = linefolder
|
|
|
|
|
|
def render(self, tree, divert=False, fixposition=False):
|
|
'''Renders a tree.
|
|
|
|
Args:
|
|
tree (fypp-tree): Tree to render.
|
|
divert (bool): Whether output will be diverted and sent for further
|
|
processing, so that no line numbering directives and
|
|
postprocessing are needed at this stage. (Default: False)
|
|
fixposition (bool): Whether file name and line position (variables
|
|
_FILE_ and _LINE_) should be kept at their current values or
|
|
should be updated continuously. (Default: False).
|
|
|
|
Returns: str: Rendered string.
|
|
'''
|
|
diverted = self._diverted
|
|
self._diverted = divert
|
|
fixedposition_old = self._fixedposition
|
|
self._fixedposition = self._fixedposition or fixposition
|
|
output, eval_inds, eval_pos = self._render(tree)
|
|
if not self._diverted and eval_inds:
|
|
self._postprocess_eval_lines(output, eval_inds, eval_pos)
|
|
self._diverted = diverted
|
|
self._fixedposition = fixedposition_old
|
|
txt = ''.join(output)
|
|
|
|
return txt
|
|
|
|
|
|
def _render(self, tree):
|
|
output = []
|
|
eval_inds = []
|
|
eval_pos = []
|
|
for node in tree:
|
|
cmd = node[0]
|
|
if cmd == 'txt':
|
|
output.append(node[3])
|
|
elif cmd == 'if':
|
|
out, ieval, peval = self._get_conditional_content(*node[1:5])
|
|
eval_inds += _shiftinds(ieval, len(output))
|
|
eval_pos += peval
|
|
output += out
|
|
elif cmd == 'eval':
|
|
out, ieval, peval = self._get_eval(*node[1:4])
|
|
eval_inds += _shiftinds(ieval, len(output))
|
|
eval_pos += peval
|
|
output += out
|
|
elif cmd == 'def':
|
|
result = self._define_macro(*node[1:6])
|
|
output.append(result)
|
|
elif cmd == 'set':
|
|
result = self._define_variable(*node[1:5])
|
|
output.append(result)
|
|
elif cmd == 'del':
|
|
self._delete_variable(*node[1:4])
|
|
elif cmd == 'for':
|
|
out, ieval, peval = self._get_iterated_content(*node[1:6])
|
|
eval_inds += _shiftinds(ieval, len(output))
|
|
eval_pos += peval
|
|
output += out
|
|
elif cmd == 'call' or cmd == 'block':
|
|
out, ieval, peval = self._get_called_content(*node[1:7])
|
|
eval_inds += _shiftinds(ieval, len(output))
|
|
eval_pos += peval
|
|
output += out
|
|
elif cmd == 'include':
|
|
out, ieval, peval = self._get_included_content(*node[1:5])
|
|
eval_inds += _shiftinds(ieval, len(output))
|
|
eval_pos += peval
|
|
output += out
|
|
elif cmd == 'comment':
|
|
output.append(self._get_comment(*node[1:3]))
|
|
elif cmd == 'mute':
|
|
output.append(self._get_muted_content(*node[1:4]))
|
|
elif cmd == 'stop':
|
|
self._handle_stop(*node[1:4])
|
|
elif cmd == 'assert':
|
|
result = self._handle_assert(*node[1:4])
|
|
output.append(result)
|
|
elif cmd == 'global':
|
|
self._add_global(*node[1:4])
|
|
else:
|
|
msg = "internal error: unknown command '{0}'".format(cmd)
|
|
raise FyppFatalError(msg)
|
|
return output, eval_inds, eval_pos
|
|
|
|
|
|
def _get_eval(self, fname, span, expr):
|
|
try:
|
|
result = self._evaluate(expr, fname, span[0])
|
|
except Exception as exc:
|
|
msg = "exception occured when evaluating '{0}'".format(expr)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
out = []
|
|
ieval = []
|
|
peval = []
|
|
if result is not None:
|
|
out.append(str(result))
|
|
if not self._diverted:
|
|
ieval.append(0)
|
|
peval.append((span, fname))
|
|
if span[0] != span[1]:
|
|
out.append('\n')
|
|
return out, ieval, peval
|
|
|
|
|
|
def _get_conditional_content(self, fname, spans, conditions, contents):
|
|
out = []
|
|
ieval = []
|
|
peval = []
|
|
multiline = (spans[0][0] != spans[-1][1])
|
|
for condition, content, span in zip(conditions, contents, spans):
|
|
try:
|
|
cond = bool(self._evaluate(condition, fname, span[0]))
|
|
except Exception as exc:
|
|
msg = "exception occured when evaluating '{0}'"\
|
|
.format(condition)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
if cond:
|
|
if self._linenums and not self._diverted and multiline:
|
|
out.append(linenumdir(span[1], fname))
|
|
outcont, ievalcont, pevalcont = self._render(content)
|
|
ieval += _shiftinds(ievalcont, len(out))
|
|
peval += pevalcont
|
|
out += outcont
|
|
break
|
|
if self._linenums and not self._diverted and multiline:
|
|
out.append(linenumdir(spans[-1][1], fname))
|
|
return out, ieval, peval
|
|
|
|
|
|
def _get_iterated_content(self, fname, spans, loopvars, loopiter, content):
|
|
out = []
|
|
ieval = []
|
|
peval = []
|
|
try:
|
|
iterobj = iter(self._evaluate(loopiter, fname, spans[0][0]))
|
|
except Exception as exc:
|
|
msg = "exception occured when evaluating '{0}'"\
|
|
.format(loopiter)
|
|
raise FyppFatalError(msg, fname, spans[0], exc)
|
|
multiline = (spans[0][0] != spans[-1][1])
|
|
for var in iterobj:
|
|
if len(loopvars) == 1:
|
|
self._define(loopvars[0], var)
|
|
else:
|
|
for varname, value in zip(loopvars, var):
|
|
self._define(varname, value)
|
|
if self._linenums and not self._diverted and multiline:
|
|
out.append(linenumdir(spans[0][1], fname))
|
|
outcont, ievalcont, pevalcont = self._render(content)
|
|
ieval += _shiftinds(ievalcont, len(out))
|
|
peval += pevalcont
|
|
out += outcont
|
|
if self._linenums and not self._diverted and multiline:
|
|
out.append(linenumdir(spans[1][1], fname))
|
|
return out, ieval, peval
|
|
|
|
|
|
def _get_called_content(self, fname, spans, name, argexpr, contents,
|
|
argnames):
|
|
posargs, kwargs = self._get_call_arguments(fname, spans, argexpr,
|
|
contents, argnames)
|
|
try:
|
|
callobj = self._evaluate(name, fname, spans[0][0])
|
|
result = callobj(*posargs, **kwargs)
|
|
except Exception as exc:
|
|
msg = "exception occured when calling '{0}'".format(name)
|
|
raise FyppFatalError(msg, fname, spans[0], exc)
|
|
self._update_predef_globals(fname, spans[0][0])
|
|
span = (spans[0][0], spans[-1][1])
|
|
out = []
|
|
ieval = []
|
|
peval = []
|
|
if result is not None:
|
|
out = [str(result)]
|
|
if not self._diverted:
|
|
ieval = [0]
|
|
peval = [(span, fname)]
|
|
if span[0] != span[1]:
|
|
out.append('\n')
|
|
return out, ieval, peval
|
|
|
|
|
|
def _get_call_arguments(self, fname, spans, argexpr, contents, argnames):
|
|
if argexpr is None:
|
|
posargs = []
|
|
kwargs = {}
|
|
else:
|
|
# Parse and evaluate arguments passed in call header
|
|
self._evaluator.openscope()
|
|
try:
|
|
posargs, kwargs = self._evaluate(
|
|
'__getargvalues(' + argexpr + ')', fname, spans[0][0])
|
|
except Exception as exc:
|
|
msg = "unable to parse argument expression '{0}'"\
|
|
.format(argexpr)
|
|
raise FyppFatalError(msg, fname, spans[0], exc)
|
|
self._evaluator.closescope()
|
|
|
|
# Render arguments passed in call body
|
|
args = []
|
|
for content in contents:
|
|
self._evaluator.openscope()
|
|
rendered = self.render(content, divert=True)
|
|
self._evaluator.closescope()
|
|
if rendered.endswith('\n'):
|
|
rendered = rendered[:-1]
|
|
args.append(rendered)
|
|
|
|
# Separate arguments in call body into positional and keyword ones:
|
|
if argnames:
|
|
posargs += args[:len(args) - len(argnames)]
|
|
offset = len(args) - len(argnames)
|
|
for iargname, argname in enumerate(argnames):
|
|
ind = offset + iargname
|
|
if argname in kwargs:
|
|
msg = "keyword argument '{0}' already defined"\
|
|
.format(argname)
|
|
raise FyppFatalError(msg, fname, spans[ind + 1])
|
|
kwargs[argname] = args[ind]
|
|
else:
|
|
posargs += args
|
|
|
|
return posargs, kwargs
|
|
|
|
|
|
def _get_included_content(self, fname, spans, includefname, content):
|
|
includefile = spans[0] is not None
|
|
out = []
|
|
if self._linenums and not self._diverted:
|
|
if includefile or self._linenum_gfortran5:
|
|
out += linenumdir(0, includefname, _LINENUM_NEW_FILE)
|
|
else:
|
|
out += linenumdir(0, includefname)
|
|
outcont, ieval, peval = self._render(content)
|
|
ieval = _shiftinds(ieval, len(out))
|
|
out += outcont
|
|
if self._linenums and not self._diverted and includefile:
|
|
out += linenumdir(spans[0][1], fname, _LINENUM_RETURN_TO_FILE)
|
|
return out, ieval, peval
|
|
|
|
|
|
def _define_macro(self, fname, spans, name, argexpr, content):
|
|
if argexpr is None:
|
|
args = []
|
|
defaults = {}
|
|
varpos = None
|
|
varkw = None
|
|
else:
|
|
# Try to create a lambda function with the argument expression
|
|
self._evaluator.openscope()
|
|
lambdaexpr = 'lambda ' + argexpr + ': None'
|
|
try:
|
|
func = self._evaluate(lambdaexpr, fname, spans[0][0])
|
|
except Exception as exc:
|
|
msg = "exception occured when evaluating argument expression "\
|
|
"'{0}'".format(argexpr)
|
|
raise FyppFatalError(msg, fname, spans[0], exc)
|
|
self._evaluator.closescope()
|
|
try:
|
|
args, defaults, varpos, varkw = _GET_CALLABLE_ARGSPEC(func)
|
|
except Exception as exc:
|
|
msg = "invalid argument expression '{0}'".format(argexpr)
|
|
raise FyppFatalError(msg, fname, spans[0], exc)
|
|
named_args = args if varpos is None else args + [varpos]
|
|
named_args = named_args if varkw is None else named_args + [varkw]
|
|
for arg in named_args:
|
|
if arg in _RESERVED_NAMES or arg.startswith(_RESERVED_PREFIX):
|
|
msg = "invalid argument name '{0}'".format(arg)
|
|
raise FyppFatalError(msg, fname, spans[0])
|
|
result = ''
|
|
try:
|
|
macro = _Macro(
|
|
name, fname, spans, args, defaults, varpos, varkw, content,
|
|
self, self._evaluator, self._evaluator.localscope)
|
|
self._define(name, macro)
|
|
except Exception as exc:
|
|
msg = "exception occured when defining macro '{0}'"\
|
|
.format(name)
|
|
raise FyppFatalError(msg, fname, spans[0], exc)
|
|
if self._linenums and not self._diverted:
|
|
result = linenumdir(spans[1][1], fname)
|
|
return result
|
|
|
|
|
|
def _define_variable(self, fname, span, name, valstr):
|
|
result = ''
|
|
try:
|
|
if valstr is None:
|
|
expr = None
|
|
else:
|
|
expr = self._evaluate(valstr, fname, span[0])
|
|
self._define(name, expr)
|
|
except Exception as exc:
|
|
msg = "exception occured when setting variable(s) '{0}' to '{1}'"\
|
|
.format(name, valstr)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
multiline = (span[0] != span[1])
|
|
if self._linenums and not self._diverted and multiline:
|
|
result = linenumdir(span[1], fname)
|
|
return result
|
|
|
|
|
|
def _delete_variable(self, fname, span, name):
|
|
result = ''
|
|
try:
|
|
self._evaluator.undefine(name)
|
|
except Exception as exc:
|
|
msg = "exception occured when deleting variable(s) '{0}'"\
|
|
.format(name)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
multiline = (span[0] != span[1])
|
|
if self._linenums and not self._diverted and multiline:
|
|
result = linenumdir(span[1], fname)
|
|
return result
|
|
|
|
|
|
def _add_global(self, fname, span, name):
|
|
result = ''
|
|
try:
|
|
self._evaluator.addglobal(name)
|
|
except Exception as exc:
|
|
msg = "exception occured when making variable(s) '{0}' global"\
|
|
.format(name)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
multiline = (span[0] != span[1])
|
|
if self._linenums and not self._diverted and multiline:
|
|
result = linenumdir(span[1], fname)
|
|
return result
|
|
|
|
|
|
def _get_comment(self, fname, span):
|
|
if self._linenums and not self._diverted:
|
|
return linenumdir(span[1], fname)
|
|
return ''
|
|
|
|
|
|
def _get_muted_content(self, fname, spans, content):
|
|
self._render(content)
|
|
if self._linenums and not self._diverted:
|
|
return linenumdir(spans[-1][1], fname)
|
|
return ''
|
|
|
|
|
|
def _handle_stop(self, fname, span, msgstr):
|
|
try:
|
|
msg = str(self._evaluate(msgstr, fname, span[0]))
|
|
except Exception as exc:
|
|
msg = "exception occured when evaluating stop message '{0}'"\
|
|
.format(msgstr)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
raise FyppStopRequest(msg, fname, span)
|
|
|
|
|
|
def _handle_assert(self, fname, span, expr):
|
|
result = ''
|
|
try:
|
|
cond = bool(self._evaluate(expr, fname, span[0]))
|
|
except Exception as exc:
|
|
msg = "exception occured when evaluating assert condition '{0}'"\
|
|
.format(expr)
|
|
raise FyppFatalError(msg, fname, span, exc)
|
|
if not cond:
|
|
msg = "Assertion failed ('{0}')".format(expr)
|
|
raise FyppStopRequest(msg, fname, span)
|
|
if self._linenums and not self._diverted:
|
|
result = linenumdir(span[1], fname)
|
|
return result
|
|
|
|
|
|
def _evaluate(self, expr, fname, linenr):
|
|
self._update_predef_globals(fname, linenr)
|
|
result = self._evaluator.evaluate(expr)
|
|
self._update_predef_globals(fname, linenr)
|
|
return result
|
|
|
|
|
|
def _update_predef_globals(self, fname, linenr):
|
|
self._evaluator.updatelocals(
|
|
_DATE_=time.strftime('%Y-%m-%d'), _TIME_=time.strftime('%H:%M:%S'),
|
|
_THIS_FILE_=fname, _THIS_LINE_=linenr + 1)
|
|
if not self._fixedposition:
|
|
self._evaluator.updateglobals(_FILE_=fname, _LINE_=linenr + 1)
|
|
|
|
|
|
def _define(self, var, value):
|
|
self._evaluator.define(var, value)
|
|
|
|
|
|
def _postprocess_eval_lines(self, output, eval_inds, eval_pos):
|
|
ilastproc = -1
|
|
for ieval, ind in enumerate(eval_inds):
|
|
span, fname = eval_pos[ieval]
|
|
if ind <= ilastproc:
|
|
continue
|
|
iprev, eolprev = self._find_last_eol(output, ind)
|
|
inext, eolnext = self._find_next_eol(output, ind)
|
|
curline = self._glue_line(output, ind, iprev, eolprev, inext,
|
|
eolnext)
|
|
output[iprev + 1:inext] = [''] * (inext - iprev - 1)
|
|
output[ind] = self._postprocess_eval_line(curline, fname, span)
|
|
ilastproc = inext
|
|
|
|
|
|
@staticmethod
|
|
def _find_last_eol(output, ind):
|
|
'Find last newline before current position.'
|
|
iprev = ind - 1
|
|
while iprev >= 0:
|
|
eolprev = output[iprev].rfind('\n')
|
|
if eolprev != -1:
|
|
break
|
|
iprev -= 1
|
|
else:
|
|
iprev = 0
|
|
eolprev = -1
|
|
return iprev, eolprev
|
|
|
|
|
|
@staticmethod
|
|
def _find_next_eol(output, ind):
|
|
'Find last newline before current position.'
|
|
# find first eol after expr. evaluation
|
|
inext = ind + 1
|
|
while inext < len(output):
|
|
eolnext = output[inext].find('\n')
|
|
if eolnext != -1:
|
|
break
|
|
inext += 1
|
|
else:
|
|
inext = len(output) - 1
|
|
eolnext = len(output[-1]) - 1
|
|
return inext, eolnext
|
|
|
|
|
|
@staticmethod
|
|
def _glue_line(output, ind, iprev, eolprev, inext, eolnext):
|
|
'Create line from parts between specified boundaries.'
|
|
curline_parts = []
|
|
if iprev != ind:
|
|
curline_parts = [output[iprev][eolprev + 1:]]
|
|
output[iprev] = output[iprev][:eolprev + 1]
|
|
curline_parts.extend(output[iprev + 1:ind])
|
|
curline_parts.extend(output[ind])
|
|
curline_parts.extend(output[ind + 1:inext])
|
|
if inext != ind:
|
|
curline_parts.append(output[inext][:eolnext + 1])
|
|
output[inext] = output[inext][eolnext + 1:]
|
|
return ''.join(curline_parts)
|
|
|
|
|
|
def _postprocess_eval_line(self, evalline, fname, span):
|
|
lines = evalline.split('\n')
|
|
# If line ended on '\n', last element is ''. We remove it and
|
|
# add the trailing newline later manually.
|
|
trailing_newline = (lines[-1] == '')
|
|
if trailing_newline:
|
|
del lines[-1]
|
|
lnum = linenumdir(span[0], fname) if self._linenums else ''
|
|
clnum = lnum if self._contlinenums else ''
|
|
linenumsep = '\n' + lnum
|
|
clinenumsep = '\n' + clnum
|
|
foldedlines = [self._foldline(line) for line in lines]
|
|
outlines = [clinenumsep.join(lines) for lines in foldedlines]
|
|
result = linenumsep.join(outlines)
|
|
# Add missing trailing newline
|
|
if trailing_newline:
|
|
trailing = '\n'
|
|
if self._linenums:
|
|
# Last line was folded, but no linenums were generated for
|
|
# the continuation lines -> current line position is not
|
|
# in sync with the one calculated from the last line number
|
|
unsync = (
|
|
len(foldedlines) and len(foldedlines[-1]) > 1
|
|
and not self._contlinenums)
|
|
# Eval directive in source consists of more than one line
|
|
multiline = span[1] - span[0] > 1
|
|
if unsync or multiline:
|
|
# For inline eval directives span[0] == span[1]
|
|
# -> next line is span[0] + 1 and not span[1] as for
|
|
# line eval directives
|
|
nextline = max(span[1], span[0] + 1)
|
|
trailing += linenumdir(nextline, fname)
|
|
else:
|
|
trailing = ''
|
|
return result + trailing
|
|
|
|
|
|
def _foldline(self, line):
|
|
if _COMMENTLINE_REGEXP.match(line) is None:
|
|
return self._linefolder(line)
|
|
return [line]
|
|
|
|
|
|
class Evaluator:
|
|
|
|
'''Provides an isolated environment for evaluating Python expressions.
|
|
|
|
It restricts the builtins which can be used within this environment to a
|
|
(hopefully safe) subset. Additionally it defines the functions which are
|
|
provided by the preprocessor for the eval directives.
|
|
|
|
Args:
|
|
env (dict, optional): Initial definitions for the environment, defaults
|
|
to None.
|
|
'''
|
|
|
|
# Restricted builtins working in all supported Python verions. Version
|
|
# specific ones are added dynamically in _get_restricted_builtins().
|
|
_RESTRICTED_BUILTINS = {
|
|
'abs': builtins.abs,
|
|
'all': builtins.all,
|
|
'any': builtins.any,
|
|
'bin': builtins.bin,
|
|
'bool': builtins.bool,
|
|
'bytearray': builtins.bytearray,
|
|
'bytes': builtins.bytes,
|
|
'chr': builtins.chr,
|
|
'classmethod': builtins.classmethod,
|
|
'complex': builtins.complex,
|
|
'delattr': builtins.delattr,
|
|
'dict': builtins.dict,
|
|
'dir': builtins.dir,
|
|
'divmod': builtins.divmod,
|
|
'enumerate': builtins.enumerate,
|
|
'filter': builtins.filter,
|
|
'float': builtins.float,
|
|
'format': builtins.format,
|
|
'frozenset': builtins.frozenset,
|
|
'getattr': builtins.getattr,
|
|
'globals': builtins.globals,
|
|
'hasattr': builtins.hasattr,
|
|
'hash': builtins.hash,
|
|
'hex': builtins.hex,
|
|
'id': builtins.id,
|
|
'int': builtins.int,
|
|
'isinstance': builtins.isinstance,
|
|
'issubclass': builtins.issubclass,
|
|
'iter': builtins.iter,
|
|
'len': builtins.len,
|
|
'list': builtins.list,
|
|
'locals': builtins.locals,
|
|
'map': builtins.map,
|
|
'max': builtins.max,
|
|
'min': builtins.min,
|
|
'next': builtins.next,
|
|
'object': builtins.object,
|
|
'oct': builtins.oct,
|
|
'ord': builtins.ord,
|
|
'pow': builtins.pow,
|
|
'property': builtins.property,
|
|
'range': builtins.range,
|
|
'repr': builtins.repr,
|
|
'reversed': builtins.reversed,
|
|
'round': builtins.round,
|
|
'set': builtins.set,
|
|
'setattr': builtins.setattr,
|
|
'slice': builtins.slice,
|
|
'sorted': builtins.sorted,
|
|
'staticmethod': builtins.staticmethod,
|
|
'str': builtins.str,
|
|
'sum': builtins.sum,
|
|
'super': builtins.super,
|
|
'tuple': builtins.tuple,
|
|
'type': builtins.type,
|
|
'vars': builtins.vars,
|
|
'zip': builtins.zip,
|
|
}
|
|
|
|
|
|
def __init__(self, env=None):
|
|
|
|
# Global scope
|
|
self._globals = env if env is not None else {}
|
|
|
|
# Local scope(s)
|
|
self._locals = None
|
|
self._locals_stack = []
|
|
|
|
# Variables which are references to entries in global scope
|
|
self._globalrefs = None
|
|
self._globalrefs_stack = []
|
|
|
|
# Current scope (globals + locals in all embedding and in current scope)
|
|
self._scope = self._globals
|
|
|
|
# Turn on restricted mode
|
|
self._restrict_builtins()
|
|
|
|
|
|
def evaluate(self, expr):
|
|
'''Evaluate a Python expression using the `eval()` builtin.
|
|
|
|
Args:
|
|
expr (str): String represantion of the expression.
|
|
|
|
Return:
|
|
Python object: Result of the expression evaluation.
|
|
'''
|
|
result = eval(expr, self._scope)
|
|
return result
|
|
|
|
|
|
def import_module(self, module):
|
|
'''Import a module into the evaluator.
|
|
|
|
Note: Import only trustworthy modules! Module imports are global,
|
|
therefore, importing a malicious module which manipulates other global
|
|
modules could affect code behaviour outside of the Evaluator as well.
|
|
|
|
Args:
|
|
module (str): Python module to import.
|
|
|
|
Raises:
|
|
FyppFatalError: If module could not be imported.
|
|
|
|
'''
|
|
rootmod = module.split('.', 1)[0]
|
|
try:
|
|
imported = __import__(module, self._scope)
|
|
self.define(rootmod, imported)
|
|
except Exception as exc:
|
|
msg = "failed to import module '{0}'".format(module)
|
|
raise FyppFatalError(msg, cause=exc)
|
|
|
|
|
|
def define(self, name, value):
|
|
'''Define a Python entity.
|
|
|
|
Args:
|
|
name (str): Name of the entity.
|
|
value (Python object): Value of the entity.
|
|
|
|
Raises:
|
|
FyppFatalError: If name starts with the reserved prefix or if it is
|
|
a reserved name.
|
|
'''
|
|
varnames = self._get_variable_names(name)
|
|
if len(varnames) == 1:
|
|
value = (value,)
|
|
elif len(varnames) != len(value):
|
|
msg = 'value for tuple assignment has incompatible length'
|
|
raise FyppFatalError(msg)
|
|
for varname, varvalue in zip(varnames, value):
|
|
self._check_variable_name(varname)
|
|
if self._locals is None:
|
|
self._globals[varname] = varvalue
|
|
else:
|
|
if varname in self._globalrefs:
|
|
self._globals[varname] = varvalue
|
|
else:
|
|
self._locals[varname] = varvalue
|
|
self._scope[varname] = varvalue
|
|
|
|
|
|
def undefine(self, name):
|
|
'''Undefine a Python entity.
|
|
|
|
Args:
|
|
name (str): Name of the entity to undefine.
|
|
|
|
Raises:
|
|
FyppFatalError: If name starts with the reserved prefix or if it is
|
|
a reserved name.
|
|
'''
|
|
varnames = self._get_variable_names(name)
|
|
for varname in varnames:
|
|
self._check_variable_name(varname)
|
|
deleted = False
|
|
if self._locals is None:
|
|
if varname in self._globals:
|
|
del self._globals[varname]
|
|
deleted = True
|
|
else:
|
|
if varname in self._locals:
|
|
del self._locals[varname]
|
|
del self._scope[varname]
|
|
deleted = True
|
|
elif varname in self._globalrefs and varname in self._globals:
|
|
del self._globals[varname]
|
|
del self._scope[varname]
|
|
deleted = True
|
|
if not deleted:
|
|
msg = "lookup for an erasable instance of '{0}' failed"\
|
|
.format(varname)
|
|
raise FyppFatalError(msg)
|
|
|
|
|
|
def addglobal(self, name):
|
|
'''Define a given entity as global.
|
|
|
|
Args:
|
|
name (str): Name of the entity to make global.
|
|
|
|
Raises:
|
|
FyppFatalError: If entity name is invalid or if the current scope is
|
|
a local scope and entity is already defined in it.
|
|
'''
|
|
varnames = self._get_variable_names(name)
|
|
for varname in varnames:
|
|
self._check_variable_name(varname)
|
|
if self._locals is not None:
|
|
if varname in self._locals:
|
|
msg = "variable '{0}' already defined in local scope"\
|
|
.format(varname)
|
|
raise FyppFatalError(msg)
|
|
self._globalrefs.add(varname)
|
|
|
|
|
|
def updateglobals(self, **vardict):
|
|
'''Update variables in the global scope.
|
|
|
|
This is a shortcut function to inject protected variables in the global
|
|
scope without extensive checks (as in define()). Vardict must not
|
|
contain any global entries which can be shadowed in local scopes
|
|
(e.g. should only contain variables with forbidden prefix).
|
|
|
|
Args:
|
|
**vardict: variable defintions.
|
|
|
|
'''
|
|
self._scope.update(vardict)
|
|
if self._locals is not None:
|
|
self._globals.update(vardict)
|
|
|
|
|
|
def updatelocals(self, **vardict):
|
|
'''Update variables in the local scope.
|
|
|
|
This is a shortcut function to inject variables in the local scope
|
|
without extensive checks (as in define()). Vardict must not contain any
|
|
entries which have been made global via addglobal() before. In order to
|
|
ensure this, updatelocals() should be called immediately after
|
|
openscope(), or with variable names, which are warrantedly not globals
|
|
(e.g variables starting with forbidden prefix)
|
|
|
|
Args:
|
|
**vardict: variable defintions.
|
|
'''
|
|
self._scope.update(vardict)
|
|
if self._locals is not None:
|
|
self._locals.update(vardict)
|
|
|
|
|
|
def openscope(self, customlocals=None):
|
|
'''Opens a new (embedded) scope.
|
|
|
|
Args:
|
|
customlocals (dict): By default, the locals of the embedding scope
|
|
are visible in the new one. When this is not the desired
|
|
behaviour a dictionary of customized locals can be passed,
|
|
and those locals will become the only visible ones.
|
|
'''
|
|
self._locals_stack.append(self._locals)
|
|
self._globalrefs_stack.append(self._globalrefs)
|
|
if customlocals is not None:
|
|
self._locals = customlocals.copy()
|
|
elif self._locals is not None:
|
|
self._locals = self._locals.copy()
|
|
else:
|
|
self._locals = {}
|
|
self._globalrefs = set()
|
|
self._scope = self._globals.copy()
|
|
self._scope.update(self._locals)
|
|
|
|
|
|
def closescope(self):
|
|
'''Close scope and restore embedding scope.'''
|
|
self._locals = self._locals_stack.pop(-1)
|
|
self._globalrefs = self._globalrefs_stack.pop(-1)
|
|
if self._locals is not None:
|
|
self._scope = self._globals.copy()
|
|
self._scope.update(self._locals)
|
|
else:
|
|
self._scope = self._globals
|
|
|
|
|
|
@property
|
|
def globalscope(self):
|
|
'Dictionary of the global scope.'
|
|
return self._globals
|
|
|
|
|
|
@property
|
|
def localscope(self):
|
|
'Dictionary of the current local scope.'
|
|
return self._locals
|
|
|
|
|
|
def _restrict_builtins(self):
|
|
builtindict = self._get_restricted_builtins()
|
|
builtindict['__import__'] = self._func_import
|
|
builtindict['defined'] = self._func_defined
|
|
builtindict['setvar'] = self._func_setvar
|
|
builtindict['getvar'] = self._func_getvar
|
|
builtindict['delvar'] = self._func_delvar
|
|
builtindict['globalvar'] = self._func_globalvar
|
|
builtindict['__getargvalues'] = self._func_getargvalues
|
|
self._globals['__builtins__'] = builtindict
|
|
|
|
|
|
@classmethod
|
|
def _get_restricted_builtins(cls):
|
|
bidict = dict(cls._RESTRICTED_BUILTINS)
|
|
major = sys.version_info[0]
|
|
if major == 2:
|
|
bidict['True'] = True
|
|
bidict['False'] = False
|
|
return bidict
|
|
|
|
|
|
@staticmethod
|
|
def _get_variable_names(varexpr):
|
|
lpar = varexpr.startswith('(')
|
|
rpar = varexpr.endswith(')')
|
|
if lpar != rpar:
|
|
msg = "unbalanced paranthesis around variable varexpr(s) in '{0}'"\
|
|
.format(varexpr)
|
|
raise FyppFatalError(msg, None, None)
|
|
if lpar:
|
|
varexpr = varexpr[1:-1]
|
|
varnames = [s.strip() for s in varexpr.split(',')]
|
|
return varnames
|
|
|
|
|
|
@staticmethod
|
|
def _check_variable_name(varname):
|
|
if varname.startswith(_RESERVED_PREFIX):
|
|
msg = "Name '{0}' starts with reserved prefix '{1}'"\
|
|
.format(varname, _RESERVED_PREFIX)
|
|
raise FyppFatalError(msg, None, None)
|
|
if varname in _RESERVED_NAMES:
|
|
msg = "Name '{0}' is reserved and can not be redefined"\
|
|
.format(varname)
|
|
raise FyppFatalError(msg, None, None)
|
|
|
|
|
|
def _func_defined(self, var):
|
|
defined = var in self._scope
|
|
return defined
|
|
|
|
|
|
def _func_import(self, name, *_, **__):
|
|
module = self._scope.get(name, None)
|
|
if module is not None and isinstance(module, types.ModuleType):
|
|
return module
|
|
msg = "Import of module '{0}' via '__import__' not allowed".format(name)
|
|
raise ImportError(msg)
|
|
|
|
|
|
def _func_setvar(self, *namesvalues):
|
|
if len(namesvalues) % 2:
|
|
msg = 'setvar function needs an even number of arguments'
|
|
raise FyppFatalError(msg)
|
|
for ind in range(0, len(namesvalues), 2):
|
|
self.define(namesvalues[ind], namesvalues[ind + 1])
|
|
|
|
|
|
def _func_getvar(self, name, defvalue=None):
|
|
if name in self._scope:
|
|
return self._scope[name]
|
|
return defvalue
|
|
|
|
|
|
def _func_delvar(self, *names):
|
|
for name in names:
|
|
self.undefine(name)
|
|
|
|
|
|
def _func_globalvar(self, *names):
|
|
for name in names:
|
|
self.addglobal(name)
|
|
|
|
|
|
@staticmethod
|
|
def _func_getargvalues(*args, **kwargs):
|
|
return list(args), kwargs
|
|
|
|
|
|
|
|
class _Macro:
|
|
|
|
'''Represents a user defined macro.
|
|
|
|
This object should only be initiatied by a Renderer instance, as it
|
|
needs access to Renderers internal variables and methods.
|
|
|
|
Args:
|
|
name (str): Name of the macro.
|
|
fname (str): The file where the macro was defined.
|
|
spans (str): Line spans of macro defintion.
|
|
argnames (list of str): Macro dummy arguments.
|
|
varpos (str): Name of variable positional argument or None.
|
|
varkw (str): Name of variable keyword argument or None.
|
|
content (list): Content of the macro as tree.
|
|
renderer (Renderer): Renderer to use for evaluating macro content.
|
|
localscope (dict): Dictionary with local variables, which should be used
|
|
the local scope, when the macro is called. Default: None (empty
|
|
local scope).
|
|
'''
|
|
|
|
def __init__(self, name, fname, spans, argnames, defaults, varpos, varkw,
|
|
content, renderer, evaluator, localscope=None):
|
|
self._name = name
|
|
self._fname = fname
|
|
self._spans = spans
|
|
self._argnames = argnames
|
|
self._defaults = defaults
|
|
self._varpos = varpos
|
|
self._varkw = varkw
|
|
self._content = content
|
|
self._renderer = renderer
|
|
self._evaluator = evaluator
|
|
self._localscope = localscope if localscope is not None else {}
|
|
|
|
|
|
def __call__(self, *args, **keywords):
|
|
argdict = self._process_arguments(args, keywords)
|
|
self._evaluator.openscope(customlocals=self._localscope)
|
|
self._evaluator.updatelocals(**argdict)
|
|
output = self._renderer.render(self._content, divert=True,
|
|
fixposition=True)
|
|
self._evaluator.closescope()
|
|
if output.endswith('\n'):
|
|
return output[:-1]
|
|
return output
|
|
|
|
|
|
def _process_arguments(self, args, keywords):
|
|
kwdict = dict(keywords)
|
|
argdict = {}
|
|
nargs = min(len(args), len(self._argnames))
|
|
for iarg in range(nargs):
|
|
argdict[self._argnames[iarg]] = args[iarg]
|
|
if nargs < len(args):
|
|
if self._varpos is None:
|
|
msg = "macro '{0}' called with too many positional arguments "\
|
|
"(expected: {1}, received: {2})"\
|
|
.format(self._name, len(self._argnames), len(args))
|
|
raise FyppFatalError(msg, self._fname, self._spans[0])
|
|
else:
|
|
argdict[self._varpos] = list(args[nargs:])
|
|
elif self._varpos is not None:
|
|
argdict[self._varpos] = []
|
|
for argname in self._argnames[:nargs]:
|
|
if argname in kwdict:
|
|
msg = "got multiple values for argument '{0}'".format(argname)
|
|
raise FyppFatalError(msg, self._fname, self._spans[0])
|
|
if nargs < len(self._argnames):
|
|
for argname in self._argnames[nargs:]:
|
|
if argname in kwdict:
|
|
argdict[argname] = kwdict.pop(argname)
|
|
elif argname in self._defaults:
|
|
argdict[argname] = self._defaults[argname]
|
|
else:
|
|
msg = "macro '{0}' called without mandatory positional "\
|
|
"argument '{1}'".format(self._name, argname)
|
|
raise FyppFatalError(msg, self._fname, self._spans[0])
|
|
if kwdict and self._varkw is None:
|
|
kwstr = "', '".join(kwdict.keys())
|
|
msg = "macro '{0}' called with unknown keyword argument(s) '{1}'"\
|
|
.format(self._name, kwstr)
|
|
raise FyppFatalError(msg, self._fname, self._spans[0])
|
|
if self._varkw is not None:
|
|
argdict[self._varkw] = kwdict
|
|
return argdict
|
|
|
|
|
|
|
|
class Processor:
|
|
|
|
'''Connects various objects with each other to create a processor.
|
|
|
|
Args:
|
|
parser (Parser, optional): Parser to use for parsing text. If None
|
|
(default), `Parser()` is used.
|
|
builder (Builder, optional): Builder to use for building the tree
|
|
representation of the text. If None (default), `Builder()` is used.
|
|
renderer (Renderer, optional): Renderer to use for rendering the
|
|
output. If None (default), `Renderer()` is used with a default
|
|
Evaluator().
|
|
evaluator (Evaluator, optional): Evaluator to use for evaluating Python
|
|
expressions. If None (default), `Evaluator()` is used.
|
|
'''
|
|
|
|
def __init__(self, parser=None, builder=None, renderer=None,
|
|
evaluator=None):
|
|
self._parser = Parser() if parser is None else parser
|
|
self._builder = Builder() if builder is None else builder
|
|
if renderer is None:
|
|
evaluator = Evaluator() if evaluator is None else evaluator
|
|
self._renderer = Renderer(evaluator)
|
|
else:
|
|
self._renderer = renderer
|
|
|
|
self._parser.handle_include = self._builder.handle_include
|
|
self._parser.handle_endinclude = self._builder.handle_endinclude
|
|
self._parser.handle_if = self._builder.handle_if
|
|
self._parser.handle_else = self._builder.handle_else
|
|
self._parser.handle_elif = self._builder.handle_elif
|
|
self._parser.handle_endif = self._builder.handle_endif
|
|
self._parser.handle_eval = self._builder.handle_eval
|
|
self._parser.handle_text = self._builder.handle_text
|
|
self._parser.handle_def = self._builder.handle_def
|
|
self._parser.handle_enddef = self._builder.handle_enddef
|
|
self._parser.handle_set = self._builder.handle_set
|
|
self._parser.handle_del = self._builder.handle_del
|
|
self._parser.handle_global = self._builder.handle_global
|
|
self._parser.handle_for = self._builder.handle_for
|
|
self._parser.handle_endfor = self._builder.handle_endfor
|
|
self._parser.handle_call = self._builder.handle_call
|
|
self._parser.handle_nextarg = self._builder.handle_nextarg
|
|
self._parser.handle_endcall = self._builder.handle_endcall
|
|
self._parser.handle_comment = self._builder.handle_comment
|
|
self._parser.handle_mute = self._builder.handle_mute
|
|
self._parser.handle_endmute = self._builder.handle_endmute
|
|
self._parser.handle_stop = self._builder.handle_stop
|
|
self._parser.handle_assert = self._builder.handle_assert
|
|
|
|
|
|
def process_file(self, fname):
|
|
'''Processeses a file.
|
|
|
|
Args:
|
|
fname (str): Name of the file to process.
|
|
|
|
Returns:
|
|
str: Processed content.
|
|
'''
|
|
self._parser.parsefile(fname)
|
|
return self._render()
|
|
|
|
|
|
def process_text(self, txt):
|
|
'''Processes a string.
|
|
|
|
Args:
|
|
txt (str): Text to process.
|
|
|
|
Returns:
|
|
str: Processed content.
|
|
'''
|
|
self._parser.parse(txt)
|
|
return self._render()
|
|
|
|
|
|
def _render(self):
|
|
output = self._renderer.render(self._builder.tree)
|
|
self._builder.reset()
|
|
return ''.join(output)
|
|
|
|
|
|
class Fypp:
|
|
|
|
'''Fypp preprocessor.
|
|
|
|
You can invoke it like ::
|
|
|
|
tool = fypp.Fypp()
|
|
tool.process_file('file.in', 'file.out')
|
|
|
|
to initialize Fypp with default options, process `file.in` and write the
|
|
result to `file.out`. If the input should be read from a string, the
|
|
``process_text()`` method can be used::
|
|
|
|
tool = fypp.Fypp()
|
|
output = tool.process_text('#:if DEBUG > 0\\nprint *, "DEBUG"\\n#:endif\\n')
|
|
|
|
If you want to fine tune Fypps behaviour, pass a customized `FyppOptions`_
|
|
instance at initialization::
|
|
|
|
options = fypp.FyppOptions()
|
|
options.fixed_format = True
|
|
tool = fypp.Fypp(options)
|
|
|
|
Alternatively, you can use the command line parser ``optparse.OptionParser``
|
|
to set options for Fypp. The function ``get_option_parser()`` returns you a
|
|
default option parser. You can then use its ``parse_args()`` method to
|
|
obtain settings by reading the command line arguments::
|
|
|
|
optparser = fypp.get_option_parser()
|
|
options, leftover = optparser.parse_args()
|
|
tool = fypp.Fypp(options)
|
|
|
|
The command line options can also be passed directly as a list when
|
|
calling ``parse_args()``::
|
|
|
|
args = ['-DDEBUG=0', 'input.fpp', 'output.f90']
|
|
optparser = fypp.get_option_parser()
|
|
options, leftover = optparser.parse_args(args=args)
|
|
tool = fypp.Fypp(options)
|
|
|
|
|
|
Args:
|
|
options (object): Object containing the settings for Fypp. You typically
|
|
would pass a customized `FyppOptions`_ instance or an
|
|
``optparse.Values`` object as returned by the option parser. If not
|
|
present, the default settings in `FyppOptions`_ are used.
|
|
'''
|
|
|
|
def __init__(self, options=None):
|
|
syspath = self._get_syspath_without_scriptdir()
|
|
self._adjust_syspath(syspath)
|
|
if options is None:
|
|
options = FyppOptions()
|
|
evaluator = Evaluator()
|
|
self._encoding = options.encoding
|
|
if options.modules:
|
|
self._import_modules(options.modules, evaluator, syspath,
|
|
options.moduledirs)
|
|
if options.defines:
|
|
self._apply_definitions(options.defines, evaluator)
|
|
parser = Parser(includedirs=options.includes, encoding=self._encoding)
|
|
builder = Builder()
|
|
|
|
fixed_format = options.fixed_format
|
|
linefolding = not options.no_folding
|
|
if linefolding:
|
|
folding = 'brute' if fixed_format else options.folding_mode
|
|
linelength = 72 if fixed_format else options.line_length
|
|
indentation = 5 if fixed_format else options.indentation
|
|
prefix = '&'
|
|
suffix = '' if fixed_format else '&'
|
|
linefolder = FortranLineFolder(linelength, indentation, folding,
|
|
prefix, suffix)
|
|
else:
|
|
linefolder = DummyLineFolder()
|
|
linenums = options.line_numbering
|
|
contlinenums = (options.line_numbering_mode != 'nocontlines')
|
|
self._create_parent_folder = options.create_parent_folder
|
|
renderer = Renderer(
|
|
evaluator, linenums=linenums, contlinenums=contlinenums,
|
|
linenumformat=options.line_marker_format, linefolder=linefolder)
|
|
self._preprocessor = Processor(parser, builder, renderer)
|
|
|
|
|
|
def process_file(self, infile, outfile=None):
|
|
'''Processes input file and writes result to output file.
|
|
|
|
Args:
|
|
infile (str): Name of the file to read and process. If its value is
|
|
'-', input is read from stdin.
|
|
outfile (str, optional): Name of the file to write the result to.
|
|
If its value is '-', result is written to stdout. If not
|
|
present, result will be returned as string.
|
|
env (dict, optional): Additional definitions for the evaluator.
|
|
|
|
Returns:
|
|
str: Result of processed input, if no outfile was specified.
|
|
'''
|
|
infile = STDIN if infile == '-' else infile
|
|
output = self._preprocessor.process_file(infile)
|
|
if outfile is None:
|
|
return output
|
|
if outfile == '-':
|
|
outfile = sys.stdout
|
|
else:
|
|
outfile = _open_output_file(outfile, self._encoding,
|
|
self._create_parent_folder)
|
|
outfile.write(output)
|
|
if outfile != sys.stdout:
|
|
outfile.close()
|
|
return None
|
|
|
|
|
|
def process_text(self, txt):
|
|
'''Processes a string.
|
|
|
|
Args:
|
|
txt (str): String to process.
|
|
env (dict, optional): Additional definitions for the evaluator.
|
|
|
|
Returns:
|
|
str: Processed content.
|
|
'''
|
|
return self._preprocessor.process_text(txt)
|
|
|
|
|
|
@staticmethod
|
|
def _apply_definitions(defines, evaluator):
|
|
for define in defines:
|
|
words = define.split('=', 2)
|
|
name = words[0]
|
|
value = None
|
|
if len(words) > 1:
|
|
try:
|
|
value = evaluator.evaluate(words[1])
|
|
except Exception as exc:
|
|
msg = "exception at evaluating '{0}' in definition for " \
|
|
"'{1}'".format(words[1], name)
|
|
raise FyppFatalError(msg, cause=exc)
|
|
evaluator.define(name, value)
|
|
|
|
|
|
def _import_modules(self, modules, evaluator, syspath, moduledirs):
|
|
lookuppath = []
|
|
if moduledirs is not None:
|
|
lookuppath += [os.path.abspath(moddir) for moddir in moduledirs]
|
|
lookuppath.append(os.path.abspath('.'))
|
|
lookuppath += syspath
|
|
self._adjust_syspath(lookuppath)
|
|
for module in modules:
|
|
evaluator.import_module(module)
|
|
self._adjust_syspath(syspath)
|
|
|
|
|
|
@staticmethod
|
|
def _get_syspath_without_scriptdir():
|
|
'''Remove the folder of the fypp binary from the search path'''
|
|
syspath = list(sys.path)
|
|
scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))
|
|
if os.path.abspath(syspath[0]) == scriptdir:
|
|
del syspath[0]
|
|
return syspath
|
|
|
|
|
|
@staticmethod
|
|
def _adjust_syspath(syspath):
|
|
sys.path = syspath
|
|
|
|
|
|
class FyppOptions(optparse.Values):
|
|
|
|
'''Container for Fypp options with default values.
|
|
|
|
Attributes:
|
|
defines (list of str): List of variable definitions in the form of
|
|
'VARNAME=VALUE'. Default: []
|
|
includes (list of str): List of paths to search when looking for include
|
|
files. Default: []
|
|
line_numbering (bool): Whether line numbering directives should appear
|
|
in the output. Default: False
|
|
line_numbering_mode (str): Line numbering mode 'full' or 'nocontlines'.
|
|
Default: 'full'.
|
|
line_marker_format (str): Line marker format. Currently 'cpp' and
|
|
'gfortran5' are supported. Later fixes the line marker handling bug
|
|
introduced in GFortran 5. Default: 'cpp'.
|
|
line_length (int): Length of output lines. Default: 132.
|
|
folding_mode (str): Folding mode 'smart', 'simple' or 'brute'. Default:
|
|
'smart'.
|
|
no_folding (bool): Whether folding should be suppresed. Default: False.
|
|
indentation (int): Indentation in continuation lines. Default: 4.
|
|
modules (list of str): Modules to import at initialization. Default: [].
|
|
moduledirs (list of str): Module lookup directories for importing user
|
|
specified modules. The specified paths are looked up *before* the
|
|
standard module locations in sys.path.
|
|
fixed_format (bool): Whether input file is in fixed format.
|
|
Default: False.
|
|
encoding (str): Character encoding for reading/writing files. Allowed
|
|
values are Pythons codec identifiers, e.g. 'ascii', 'utf-8', etc.
|
|
Default: 'utf-8'. Reading from stdin and writing to stdout is always
|
|
encoded according to the current locale and is not affected by this
|
|
setting.
|
|
create_parent_folder (bool): Whether the parent folder for the output
|
|
file should be created if it does not exist. Default: False.
|
|
'''
|
|
|
|
def __init__(self):
|
|
optparse.Values.__init__(self)
|
|
self.defines = []
|
|
self.includes = []
|
|
self.line_numbering = False
|
|
self.line_numbering_mode = 'full'
|
|
self.line_marker_format = 'cpp'
|
|
self.line_length = 132
|
|
self.folding_mode = 'smart'
|
|
self.no_folding = False
|
|
self.indentation = 4
|
|
self.modules = []
|
|
self.moduledirs = []
|
|
self.fixed_format = False
|
|
self.encoding = 'utf-8'
|
|
self.create_parent_folder = False
|
|
|
|
|
|
class FortranLineFolder:
|
|
|
|
'''Implements line folding with Fortran continuation lines.
|
|
|
|
Args:
|
|
maxlen (int, optional): Maximal line length (default: 132).
|
|
indent (int, optional): Indentation for continuation lines (default: 4).
|
|
method (str, optional): Folding method with following options:
|
|
|
|
* ``brute``: folding with maximal length of continuation lines,
|
|
* ``simple``: indents with respect of indentation of first line,
|
|
* ``smart``: like ``simple``, but tries to fold at whitespaces.
|
|
|
|
prefix (str, optional): String to use at the beginning of a continuation
|
|
line (default: '&').
|
|
suffix (str, optional): String to use at the end of the line preceeding
|
|
a continuation line (default: '&')
|
|
'''
|
|
|
|
def __init__(self, maxlen=132, indent=4, method='smart', prefix='&',
|
|
suffix='&'):
|
|
# Line length should be long enough that contintuation lines can host at
|
|
# east one character apart of indentation and two continuation signs
|
|
minmaxlen = indent + len(prefix) + len(suffix) + 1
|
|
if maxlen < minmaxlen:
|
|
msg = 'Maximal line length less than {0} when using an indentation'\
|
|
' of {1}'.format(minmaxlen, indent)
|
|
raise FyppFatalError(msg)
|
|
self._maxlen = maxlen
|
|
self._indent = indent
|
|
self._prefix = ' ' * self._indent + prefix
|
|
self._suffix = suffix
|
|
if method not in ['brute', 'smart', 'simple']:
|
|
raise FyppFatalError('invalid folding type')
|
|
if method == 'brute':
|
|
self._inherit_indent = False
|
|
self._fold_position_finder = self._get_maximal_fold_pos
|
|
elif method == 'simple':
|
|
self._inherit_indent = True
|
|
self._fold_position_finder = self._get_maximal_fold_pos
|
|
elif method == 'smart':
|
|
self._inherit_indent = True
|
|
self._fold_position_finder = self._get_smart_fold_pos
|
|
|
|
|
|
def __call__(self, line):
|
|
'''Folds a line.
|
|
|
|
Can be directly called to return the list of folded lines::
|
|
|
|
linefolder = FortranLineFolder(maxlen=10)
|
|
linefolder(' print *, "some Fortran line"')
|
|
|
|
Args:
|
|
line (str): Line to fold.
|
|
|
|
Returns:
|
|
list of str: Components of folded line. They should be
|
|
assembled via ``\\n.join()`` to obtain the string
|
|
representation.
|
|
'''
|
|
if self._maxlen < 0 or len(line) <= self._maxlen:
|
|
return [line]
|
|
if self._inherit_indent:
|
|
indent = len(line) - len(line.lstrip())
|
|
prefix = ' ' * indent + self._prefix
|
|
else:
|
|
indent = 0
|
|
prefix = self._prefix
|
|
suffix = self._suffix
|
|
return self._split_line(line, self._maxlen, prefix, suffix,
|
|
self._fold_position_finder)
|
|
|
|
|
|
@staticmethod
|
|
def _split_line(line, maxlen, prefix, suffix, fold_position_finder):
|
|
# length of continuation lines with 1 or two continuation chars.
|
|
maxlen1 = maxlen - len(prefix)
|
|
maxlen2 = maxlen1 - len(suffix)
|
|
start = 0
|
|
end = fold_position_finder(line, start, maxlen - len(suffix))
|
|
result = [line[start:end] + suffix]
|
|
while end < len(line) - maxlen1:
|
|
start = end
|
|
end = fold_position_finder(line, start, start + maxlen2)
|
|
result.append(prefix + line[start:end] + suffix)
|
|
result.append(prefix + line[end:])
|
|
return result
|
|
|
|
|
|
@staticmethod
|
|
def _get_maximal_fold_pos(_, __, end):
|
|
return end
|
|
|
|
|
|
@staticmethod
|
|
def _get_smart_fold_pos(line, start, end):
|
|
linelen = end - start
|
|
ispace = line.rfind(' ', start, end)
|
|
# The space we waste for smart folding should be max. 1/3rd of the line
|
|
if ispace != -1 and ispace >= start + (2 * linelen) // 3:
|
|
return ispace
|
|
return end
|
|
|
|
|
|
class DummyLineFolder:
|
|
|
|
'''Implements a dummy line folder returning the line unaltered.'''
|
|
|
|
def __call__(self, line):
|
|
'''Returns the entire line without any folding.
|
|
|
|
Returns:
|
|
list of str: Components of folded line. They should be
|
|
assembled via ``\\n.join()`` to obtain the string
|
|
representation.
|
|
'''
|
|
return [line]
|
|
|
|
|
|
def get_option_parser():
|
|
'''Returns an option parser for the Fypp command line tool.
|
|
|
|
Returns:
|
|
OptionParser: Parser which can create an optparse.Values object with
|
|
Fypp settings based on command line arguments.
|
|
'''
|
|
defs = FyppOptions()
|
|
fypp_name = 'fypp'
|
|
fypp_desc = 'Preprocesses source code with Fypp directives. The input is '\
|
|
'read from INFILE (default: \'-\', stdin) and written to '\
|
|
'OUTFILE (default: \'-\', stdout).'
|
|
fypp_version = fypp_name + ' ' + VERSION
|
|
usage = '%prog [options] [INFILE] [OUTFILE]'
|
|
parser = optparse.OptionParser(prog=fypp_name, description=fypp_desc,
|
|
version=fypp_version, usage=usage)
|
|
msg = 'define variable, value is interpreted as ' \
|
|
'Python expression (e.g \'-DDEBUG=1\' sets DEBUG to the ' \
|
|
'integer 1) or set to None if ommitted'
|
|
parser.add_option('-D', '--define', action='append', dest='defines',
|
|
metavar='VAR[=VALUE]', default=defs.defines, help=msg)
|
|
msg = 'add directory to the search paths for include files'
|
|
parser.add_option('-I', '--include', action='append', dest='includes',
|
|
metavar='INCDIR', default=defs.includes, help=msg)
|
|
msg = 'import a python module at startup (import only trustworthy modules '\
|
|
'as they have access to an **unrestricted** Python environment!)'
|
|
parser.add_option('-m', '--module', action='append', dest='modules',
|
|
metavar='MOD', default=defs.modules, help=msg)
|
|
msg = 'directory to be searched for user imported modules before '\
|
|
'looking up standard locations in sys.path'
|
|
parser.add_option('-M', '--module-dir', action='append',
|
|
dest='moduledirs', metavar='MODDIR',
|
|
default=defs.moduledirs, help=msg)
|
|
msg = 'emit line numbering markers'
|
|
parser.add_option('-n', '--line-numbering', action='store_true',
|
|
dest='line_numbering', default=defs.line_numbering,
|
|
help=msg)
|
|
msg = 'line numbering mode, \'full\' (default): line numbering '\
|
|
'markers generated whenever source and output lines are out '\
|
|
'of sync, \'nocontlines\': line numbering markers omitted '\
|
|
'for continuation lines'
|
|
parser.add_option('-N', '--line-numbering-mode', metavar='MODE',
|
|
choices=['full', 'nocontlines'],
|
|
default=defs.line_numbering_mode,
|
|
dest='line_numbering_mode', help=msg)
|
|
msg = 'line numbering marker format, \'cpp\' (default): GNU cpp format, '\
|
|
'\'gfortran5\': modified markers to work around bug in GFortran 5 '\
|
|
'and above'
|
|
parser.add_option('--line-marker-format', metavar='FMT',
|
|
choices=['cpp', 'gfortran5'], dest='line_marker_format',
|
|
default=defs.line_marker_format, help=msg)
|
|
msg = 'maximal line length (default: 132), lines modified by the '\
|
|
'preprocessor are folded if becoming longer'
|
|
parser.add_option('-l', '--line-length', type=int, metavar='LEN',
|
|
dest='line_length', default=defs.line_length, help=msg)
|
|
msg = 'line folding mode, \'smart\' (default): indentation context '\
|
|
'and whitespace aware, \'simple\': indentation context aware, '\
|
|
'\'brute\': mechnical folding'
|
|
parser.add_option('-f', '--folding-mode', metavar='MODE',
|
|
choices=['smart', 'simple', 'brute'], dest='folding_mode',
|
|
default=defs.folding_mode, help=msg)
|
|
msg = 'suppress line folding'
|
|
parser.add_option('-F', '--no-folding', action='store_true',
|
|
dest='no_folding', default=defs.no_folding, help=msg)
|
|
msg = 'indentation to use for continuation lines (default 4)'
|
|
parser.add_option('--indentation', type=int, metavar='IND',
|
|
dest='indentation', default=defs.indentation, help=msg)
|
|
msg = 'produce fixed format output (any settings for options '\
|
|
'--line-length, --folding-method and --indentation are ignored)'
|
|
parser.add_option('--fixed-format', action='store_true',
|
|
dest='fixed_format', default=defs.fixed_format, help=msg)
|
|
msg = 'character encoding for reading/writing files. Default: \'utf-8\'. '\
|
|
'Note: reading from stdin and writing to stdout is encoded '\
|
|
'according to the current locale and is not affected by this setting.'
|
|
parser.add_option('--encoding', metavar='ENC', default=defs.encoding,
|
|
help=msg)
|
|
msg = 'create parent folders of the output file if they do not exist'
|
|
parser.add_option('-p', '--create-parents', action='store_true',
|
|
dest='create_parent_folder',
|
|
default=defs.create_parent_folder, help=msg)
|
|
return parser
|
|
|
|
|
|
def run_fypp():
|
|
'''Run the Fypp command line tool.'''
|
|
options = FyppOptions()
|
|
optparser = get_option_parser()
|
|
opts, leftover = optparser.parse_args(values=options)
|
|
infile = leftover[0] if len(leftover) > 0 else '-'
|
|
outfile = leftover[1] if len(leftover) > 1 else '-'
|
|
try:
|
|
tool = Fypp(opts)
|
|
tool.process_file(infile, outfile)
|
|
except FyppStopRequest as exc:
|
|
sys.stderr.write(_formatted_exception(exc))
|
|
sys.exit(USER_ERROR_EXIT_CODE)
|
|
except FyppFatalError as exc:
|
|
sys.stderr.write(_formatted_exception(exc))
|
|
sys.exit(ERROR_EXIT_CODE)
|
|
|
|
|
|
def linenumdir(linenr, fname, flag=None):
|
|
'''Returns a line numbering directive.
|
|
|
|
Args:
|
|
linenr (int): Line nr (starting with 0).
|
|
fname (str): File name.
|
|
'''
|
|
if flag is None:
|
|
return '# {0} "{1}"\n'.format(linenr + 1, fname)
|
|
return '# {0} "{1}" {2}\n'.format(linenr + 1, fname, flag)
|
|
|
|
|
|
def _shiftinds(inds, shift):
|
|
return [ind + shift for ind in inds]
|
|
|
|
|
|
def _open_input_file(inpfile, encoding=None):
|
|
try:
|
|
inpfp = io.open(inpfile, 'r', encoding=encoding)
|
|
except IOError as exc:
|
|
msg = "Failed to open file '{0}' for read".format(inpfile)
|
|
raise FyppFatalError(msg, cause=exc)
|
|
return inpfp
|
|
|
|
|
|
def _open_output_file(outfile, encoding=None, create_parents=False):
|
|
if create_parents:
|
|
parentdir = os.path.abspath(os.path.dirname(outfile))
|
|
if not os.path.exists(parentdir):
|
|
try:
|
|
os.makedirs(parentdir)
|
|
except OSError as exc:
|
|
if exc.errno != errno.EEXIST:
|
|
msg = "Folder '{0}' can not be created"\
|
|
.format(parentdir)
|
|
raise FyppFatalError(msg, cause=exc)
|
|
try:
|
|
outfp = io.open(outfile, 'w', encoding=encoding)
|
|
except IOError as exc:
|
|
msg = "Failed to open file '{0}' for write".format(outfile)
|
|
raise FyppFatalError(msg, cause=exc)
|
|
return outfp
|
|
|
|
|
|
def _get_callable_argspec_py2(func):
|
|
argspec = inspect.getargspec(func)
|
|
varpos = argspec.varargs
|
|
varkw = argspec.keywords
|
|
args = argspec.args
|
|
tuplearg = False
|
|
for elem in args:
|
|
tuplearg = tuplearg or isinstance(elem, list)
|
|
if tuplearg:
|
|
msg = 'tuple argument(s) found'
|
|
raise FyppFatalError(msg)
|
|
defaults = {}
|
|
if argspec.defaults is not None:
|
|
for ind, default in enumerate(argspec.defaults):
|
|
iarg = len(args) - len(argspec.defaults) + ind
|
|
defaults[args[iarg]] = default
|
|
return args, defaults, varpos, varkw
|
|
|
|
|
|
def _get_callable_argspec_py3(func):
|
|
sig = inspect.signature(func)
|
|
args = []
|
|
defaults = {}
|
|
varpos = None
|
|
varkw = None
|
|
for param in sig.parameters.values():
|
|
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
|
args.append(param.name)
|
|
if param.default != param.empty:
|
|
defaults[param.name] = param.default
|
|
elif param.kind == param.VAR_POSITIONAL:
|
|
varpos = param.name
|
|
elif param.kind == param.VAR_KEYWORD:
|
|
varkw = param.name
|
|
else:
|
|
msg = "argument '{0}' has invalid argument type".format(param.name)
|
|
raise FyppFatalError(msg)
|
|
return args, defaults, varpos, varkw
|
|
|
|
|
|
# Signature objects are available from Python 3.3 (and deprecated from 3.5)
|
|
|
|
if sys.version_info[0] >= 3 and sys.version_info[1] >= 3:
|
|
_GET_CALLABLE_ARGSPEC = _get_callable_argspec_py3
|
|
else:
|
|
_GET_CALLABLE_ARGSPEC = _get_callable_argspec_py2
|
|
|
|
|
|
def _blank_match(match):
|
|
size = match.end() - match.start()
|
|
return " " * size
|
|
|
|
|
|
def _argsplit_fortran(argtxt):
|
|
txt = _INLINE_EVAL_REGION_REGEXP.sub(_blank_match, argtxt)
|
|
splitpos = [-1]
|
|
quote = None
|
|
closing_brace_stack = []
|
|
closing_brace = None
|
|
for ind, char in enumerate(txt):
|
|
if quote:
|
|
if char == quote:
|
|
quote = None
|
|
continue
|
|
if char in _QUOTES_FORTRAN:
|
|
quote = char
|
|
continue
|
|
if char in _OPENING_BRACKETS_FORTRAN:
|
|
closing_brace_stack.append(closing_brace)
|
|
ind = _OPENING_BRACKETS_FORTRAN.index(char)
|
|
closing_brace = _CLOSING_BRACKETS_FORTRAN[ind]
|
|
continue
|
|
if char in _CLOSING_BRACKETS_FORTRAN:
|
|
if char == closing_brace:
|
|
closing_brace = closing_brace_stack.pop(-1)
|
|
continue
|
|
else:
|
|
msg = "unexpected closing delimiter '{0}' in expression '{1}' "\
|
|
"at position {2}".format(char, argtxt, ind + 1)
|
|
raise FyppFatalError(msg)
|
|
if not closing_brace and char == _ARGUMENT_SPLIT_CHAR_FORTRAN:
|
|
splitpos.append(ind)
|
|
if quote or closing_brace:
|
|
msg = "open quotes or brackets in expression '{0}'".format(argtxt)
|
|
raise FyppFatalError(msg)
|
|
splitpos.append(len(txt))
|
|
fragments = [argtxt[start + 1 : end]
|
|
for start, end in zip(splitpos, splitpos[1:])]
|
|
return fragments
|
|
|
|
|
|
def _formatted_exception(exc):
|
|
error_header_formstr = '{file}:{line}: '
|
|
error_body_formstr = 'error: {errormsg} [{errorclass}]'
|
|
if not isinstance(exc, FyppError):
|
|
return error_body_formstr.format(
|
|
errormsg=str(exc), errorclass=exc.__class__.__name__)
|
|
out = []
|
|
if exc.fname is not None:
|
|
if exc.span[1] > exc.span[0] + 1:
|
|
line = '{0}-{1}'.format(exc.span[0] + 1, exc.span[1])
|
|
else:
|
|
line = '{0}'.format(exc.span[0] + 1)
|
|
out.append(error_header_formstr.format(file=exc.fname, line=line))
|
|
out.append(error_body_formstr.format(errormsg=exc.msg,
|
|
errorclass=exc.__class__.__name__))
|
|
if exc.cause is not None:
|
|
out.append('\n' + _formatted_exception(exc.cause))
|
|
out.append('\n')
|
|
return ''.join(out)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run_fypp()
|