1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3################################################################################
4#
5# fypp -- Python powered Fortran preprocessor
6#
7# Copyright (c) 2016-2021 Bálint Aradi, Universität Bremen
8#
9# All rights reserved.
10#
11# Redistribution and use in source and binary forms, with or without
12# modification, are permitted provided that the following conditions are met:
13#
14# 1. Redistributions of source code must retain the above copyright notice, this
15# list of conditions and the following disclaimer.
16#
17# 2. Redistributions in binary form must reproduce the above copyright notice,
18# this list of conditions and the following disclaimer in the documentation
19# and/or other materials provided with the distribution.
20#
21# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
22# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#
32################################################################################
33
34'''For using the functionality of the Fypp preprocessor from within
35Python, one usually interacts with the following two classes:
36
37* `Fypp`_: The actual Fypp preprocessor. It returns for a given input
38  the preprocessed output.
39
40* `FyppOptions`_: Contains customizable settings controlling the behaviour of
41  `Fypp`_. Alternatively, the function `get_option_parser()`_ can be used to
42  obtain an option parser, which can create settings based on command line
43  arguments.
44
45If processing stops prematurely, an instance of one of the following
46subclasses of `FyppError`_ is raised:
47
48* FyppFatalError: Unexpected error (e.g. bad input, missing files, etc.)
49
50* FyppStopRequest: Stop was triggered by an explicit request in the input
51  (by a stop- or an assert-directive).
52'''
53
54import sys
55import types
56import inspect
57import re
58import os
59import errno
60import time
61import optparse
62import io
63import platform
64import builtins
65
66# Prevent cluttering user directory with Python bytecode
67sys.dont_write_bytecode = True
68
69VERSION = '3.1'
70
71STDIN = '<stdin>'
72
73FILEOBJ = '<fileobj>'
74
75STRING = '<string>'
76
77ERROR_EXIT_CODE = 1
78
79USER_ERROR_EXIT_CODE = 2
80
81_ALL_DIRECTIVES_PATTERN = r'''
82# comment block
83(?:^[ \t]*\#!.*\n)+
84|
85# line directive (with optional continuation lines)
86^[ \t]*(?P<ldirtype>[\#\$@]):[ \t]*
87(?P<ldir>.+?(?:&[ \t]*\n(?:[ \t]*&)?.*?)*)?[ \t]*\n
88|
89# inline eval directive
90(?P<idirtype>[$\#@])\{[ \t]*(?P<idir>.+?)?[ \t]*\}(?P=idirtype)
91'''
92
93_ALL_DIRECTIVES_REGEXP = re.compile(
94    _ALL_DIRECTIVES_PATTERN, re.VERBOSE | re.MULTILINE)
95
96_CONTROL_DIR_REGEXP = re.compile(
97    r'(?P<dir>[a-zA-Z_]\w*)[ \t]*(?:[ \t]+(?P<param>[^ \t].*))?$')
98
99_DIRECT_CALL_REGEXP = re.compile(
100    r'(?P<callname>[a-zA-Z_][\w.]*)[ \t]*\((?P<callparams>.+?)?\)$')
101
102_DIRECT_CALL_KWARG_REGEXP = re.compile(
103    r'(?:(?P<kwname>[a-zA-Z_]\w*)\s*=(?=[^=]|$))?')
104
105_DEF_PARAM_REGEXP = re.compile(
106    r'^(?P<name>[a-zA-Z_]\w*)[ \t]*\(\s*(?P<args>.+)?\s*\)$')
107
108_SIMPLE_CALLABLE_REGEXP = re.compile(
109    r'^(?P<name>[a-zA-Z_][\w.]*)[ \t]*(?:\([ \t]*(?P<args>.*)[ \t]*\))?$')
110
111_IDENTIFIER_NAME_REGEXP = re.compile(r'^(?P<name>[a-zA-Z_]\w*)$')
112
113_PREFIXED_IDENTIFIER_NAME_REGEXP = re.compile(r'^(?P<name>[a-zA-Z_][\w.]*)$')
114
115_SET_PARAM_REGEXP = re.compile(
116    r'^(?P<name>(?:[(]\s*)?[a-zA-Z_]\w*(?:\s*,\s*[a-zA-Z_]\w*)*(?:\s*[)])?)\s*'\
117    r'(?:=\s*(?P<expr>.*))?$')
118
119_DEL_PARAM_REGEXP = re.compile(
120    r'^(?:[(]\s*)?[a-zA-Z_]\w*(?:\s*,\s*[a-zA-Z_]\w*)*(?:\s*[)])?$')
121
122_FOR_PARAM_REGEXP = re.compile(
123    r'^(?P<loopexpr>[a-zA-Z_]\w*(\s*,\s*[a-zA-Z_]\w*)*)\s+in\s+(?P<iter>.+)$')
124
125_INCLUDE_PARAM_REGEXP = re.compile(r'^(\'|")(?P<fname>.*?)\1$')
126
127_COMMENTLINE_REGEXP = re.compile(r'^[ \t]*!.*$')
128
129_CONTLINE_REGEXP = re.compile(r'&[ \t]*\n(?:[ \t]*&)?')
130
131_UNESCAPE_TEXT_REGEXP1 = re.compile(r'([$#@])\\(\\*)([{:])')
132
133_UNESCAPE_TEXT_REGEXP2 = re.compile(r'#\\(\\*)([!])')
134
135_UNESCAPE_TEXT_REGEXP3 = re.compile(r'(\})\\(\\*)([$#@])')
136
137_INLINE_EVAL_REGION_REGEXP = re.compile(r'\${.*?}\$')
138
139_RESERVED_PREFIX = '__'
140
141_RESERVED_NAMES = set(['defined', 'setvar', 'getvar', 'delvar', 'globalvar',
142                       '_LINE_', '_FILE_', '_THIS_FILE_', '_THIS_LINE_',
143                       '_TIME_', '_DATE_', '_SYSTEM_', '_MACHINE_'])
144
145_LINENUM_NEW_FILE = 1
146
147_LINENUM_RETURN_TO_FILE = 2
148
149_QUOTES_FORTRAN = '\'"'
150
151_OPENING_BRACKETS_FORTRAN = '{(['
152
153_CLOSING_BRACKETS_FORTRAN = '})]'
154
155_ARGUMENT_SPLIT_CHAR_FORTRAN = ','
156
157
158class FyppError(Exception):
159    '''Signalizes error occurring during preprocessing.
160
161    Args:
162        msg (str): Error message.
163        fname (str): File name. None (default) if file name is not available.
164        span (tuple of int): Beginning and end line of the region where error
165            occurred or None if not available. If fname was not None, span must
166            not be None.
167
168    Attributes:
169        msg (str): Error message.
170        fname (str or None): File name or None if not available.
171        span (tuple of int or None): Beginning and end line of the region
172            where error occurred or None if not available. Line numbers start
173            from zero. For directives, which do not consume end of the line,
174            start and end lines are identical.
175    '''
176
177    def __init__(self, msg, fname=None, span=None):
178        super().__init__()
179        self.msg = msg
180        self.fname = fname
181        self.span = span
182
183
184    def __str__(self):
185        msg = [self.__class__.__name__, ': ']
186        if self.fname is not None:
187            msg.append("file '" + self.fname + "'")
188            if self.span[1] > self.span[0] + 1:
189                msg.append(', lines {0}-{1}'.format(
190                    self.span[0] + 1, self.span[1]))
191            else:
192                msg.append(', line {0}'.format(self.span[0] + 1))
193            msg.append('\n')
194        if self.msg:
195            msg.append(self.msg)
196        if self.__cause__ is not None:
197            msg.append('\n' + str(self.__cause__))
198        return ''.join(msg)
199
200
201class FyppFatalError(FyppError):
202    '''Signalizes an unexpected error during processing.'''
203
204
205class FyppStopRequest(FyppError):
206    '''Signalizes an explicitely triggered stop (e.g. via stop directive)'''
207
208
209class Parser:
210    '''Parses a text and generates events when encountering Fypp constructs.
211
212    Args:
213        includedirs (list): List of directories, in which include files should
214            be searched for, when they are not found at the default location.
215
216        encoding (str): Encoding to use when reading the file (default: utf-8)
217    '''
218
219    def __init__(self, includedirs=None, encoding='utf-8'):
220
221        # Directories to search for include files
222        if includedirs is None:
223            self._includedirs = []
224        else:
225            self._includedirs = includedirs
226
227        # Encoding
228        self._encoding = encoding
229
230        # Name of current file
231        self._curfile = None
232
233        # Directory of current file
234        self._curdir = None
235
236
237    def parsefile(self, fobj):
238        '''Parses file or a file like object.
239
240        Args:
241            fobj (str or file): Name of a file or a file like object.
242        '''
243        if isinstance(fobj, str):
244            if fobj == STDIN:
245                self._includefile(None, sys.stdin, STDIN, os.getcwd())
246            else:
247                inpfp = _open_input_file(fobj, self._encoding)
248                self._includefile(None, inpfp, fobj, os.path.dirname(fobj))
249                inpfp.close()
250        else:
251            self._includefile(None, fobj, FILEOBJ, os.getcwd())
252
253
254    def _includefile(self, span, fobj, fname, curdir):
255        oldfile = self._curfile
256        olddir = self._curdir
257        self._curfile = fname
258        self._curdir = curdir
259        self._parse_txt(span, fname, fobj.read())
260        self._curfile = oldfile
261        self._curdir = olddir
262
263
264    def parse(self, txt):
265        '''Parses string.
266
267        Args:
268            txt (str): Text to parse.
269        '''
270        self._curfile = STRING
271        self._curdir = ''
272        self._parse_txt(None, self._curfile, txt)
273
274
275    def handle_include(self, span, fname):
276        '''Called when parser starts to process a new file.
277
278        It is a dummy methond and should be overridden for actual use.
279
280        Args:
281            span (tuple of int): Start and end line of the include directive
282                or None if called the first time for the main input.
283            fname (str): Name of the file.
284        '''
285        self._log_event('include', span, filename=fname)
286
287
288    def handle_endinclude(self, span, fname):
289        '''Called when parser finished processing a file.
290
291        It is a dummy method and should be overridden for actual use.
292
293        Args:
294            span (tuple of int): Start and end line of the include directive
295                or None if called the first time for the main input.
296            fname (str): Name of the file.
297        '''
298        self._log_event('endinclude', span, filename=fname)
299
300
301    def handle_set(self, span, name, expr):
302        '''Called when parser encounters a set directive.
303
304        It is a dummy method and should be overridden for actual use.
305
306        Args:
307            span (tuple of int): Start and end line of the directive.
308            name (str): Name of the variable.
309            expr (str): String representation of the expression to be assigned
310                to the variable.
311        '''
312        self._log_event('set', span, name=name, expression=expr)
313
314
315    def handle_def(self, span, name, args):
316        '''Called when parser encounters a def directive.
317
318        It is a dummy method and should be overridden for actual use.
319
320        Args:
321            span (tuple of int): Start and end line of the directive.
322            name (str): Name of the macro to be defined.
323            argexpr (str): String with argument definition (or None)
324        '''
325        self._log_event('def', span, name=name, arguments=args)
326
327
328    def handle_enddef(self, span, name):
329        '''Called when parser encounters an enddef directive.
330
331        It is a dummy method and should be overridden for actual use.
332
333        Args:
334            span (tuple of int): Start and end line of the directive.
335            name (str): Name found after the enddef directive.
336        '''
337        self._log_event('enddef', span, name=name)
338
339
340    def handle_del(self, span, name):
341        '''Called when parser encounters a del directive.
342
343        It is a dummy method and should be overridden for actual use.
344
345        Args:
346            span (tuple of int): Start and end line of the directive.
347            name (str): Name of the variable to delete.
348        '''
349        self._log_event('del', span, name=name)
350
351
352    def handle_if(self, span, cond):
353        '''Called when parser encounters an if directive.
354
355        It is a dummy method and should be overridden for actual use.
356
357        Args:
358            span (tuple of int): Start and end line of the directive.
359            cond (str): String representation of the branching condition.
360        '''
361        self._log_event('if', span, condition=cond)
362
363
364    def handle_elif(self, span, cond):
365        '''Called when parser encounters an elif directive.
366
367        It is a dummy method and should be overridden for actual use.
368
369        Args:
370            span (tuple of int): Start and end line of the directive.
371            cond (str): String representation of the branching condition.
372        '''
373        self._log_event('elif', span, condition=cond)
374
375
376    def handle_else(self, span):
377        '''Called when parser encounters an else directive.
378
379        It is a dummy method and should be overridden for actual use.
380
381        Args:
382            span (tuple of int): Start and end line of the directive.
383        '''
384        self._log_event('else', span)
385
386
387    def handle_endif(self, span):
388        '''Called when parser encounters an endif directive.
389
390        It is a dummy method and should be overridden for actual use.
391
392        Args:
393            span (tuple of int): Start and end line of the directive.
394        '''
395        self._log_event('endif', span)
396
397
398    def handle_for(self, span, varexpr, iterator):
399        '''Called when parser encounters a for directive.
400
401        It is a dummy method and should be overridden for actual use.
402
403        Args:
404            span (tuple of int): Start and end line of the directive.
405            varexpr (str): String representation of the loop variable
406                expression.
407            iterator (str): String representation of the iterable.
408        '''
409        self._log_event('for', span, variable=varexpr, iterable=iterator)
410
411
412    def handle_endfor(self, span):
413        '''Called when parser encounters an endfor directive.
414
415        It is a dummy method and should be overridden for actual use.
416
417        Args:
418            span (tuple of int): Start and end line of the directive.
419        '''
420        self._log_event('endfor', span)
421
422
423    def handle_call(self, span, name, argexpr, blockcall):
424        '''Called when parser encounters a call directive.
425
426        It is a dummy method and should be overridden for actual use.
427
428        Args:
429            span (tuple of int): Start and end line of the directive.
430            name (str): Name of the callable to call
431            argexpr (str or None): Argument expression containing additional
432                arguments for the call.
433            blockcall (bool): Whether the alternative "block / contains /
434                endblock" calling directive has been used.
435        '''
436        self._log_event('call', span, name=name, argexpr=argexpr,
437                        blockcall=blockcall)
438
439
440    def handle_nextarg(self, span, name, blockcall):
441        '''Called when parser encounters a nextarg directive.
442
443        It is a dummy method and should be overridden for actual use.
444
445        Args:
446            span (tuple of int): Start and end line of the directive.
447            name (str or None): Name of the argument following next or
448                None if it should be the next positional argument.
449            blockcall (bool): Whether the alternative "block / contains /
450                endblock" calling directive has been used.
451        '''
452        self._log_event('nextarg', span, name=name, blockcall=blockcall)
453
454
455    def handle_endcall(self, span, name, blockcall):
456        '''Called when parser encounters an endcall directive.
457
458        It is a dummy method and should be overridden for actual use.
459
460        Args:
461            span (tuple of int): Start and end line of the directive.
462            name (str): Name found after the endcall directive.
463            blockcall (bool): Whether the alternative "block / contains /
464                endblock" calling directive has been used.
465        '''
466        self._log_event('endcall', span, name=name, blockcall=blockcall)
467
468
469    def handle_eval(self, span, expr):
470        '''Called when parser encounters an eval directive.
471
472        It is a dummy method and should be overridden for actual use.
473
474        Args:
475            span (tuple of int): Start and end line of the directive.
476            expr (str): String representation of the Python expression to
477                be evaluated.
478        '''
479        self._log_event('eval', span, expression=expr)
480
481
482    def handle_global(self, span, name):
483        '''Called when parser encounters a global directive.
484
485        It is a dummy method and should be overridden for actual use.
486
487        Args:
488            span (tuple of int): Start and end line of the directive.
489            name (str): Name of the variable which should be made global.
490        '''
491        self._log_event('global', span, name=name)
492
493
494    def handle_text(self, span, txt):
495        '''Called when parser finds text which must left unaltered.
496
497        It is a dummy method and should be overridden for actual use.
498
499        Args:
500            span (tuple of int): Start and end line of the directive.
501            txt (str): Text.
502        '''
503        self._log_event('text', span, content=txt)
504
505
506    def handle_comment(self, span):
507        '''Called when parser finds a preprocessor comment.
508
509        It is a dummy method and should be overridden for actual use.
510
511        Args:
512            span (tuple of int): Start and end line of the directive.
513        '''
514        self._log_event('comment', span)
515
516
517    def handle_mute(self, span):
518        '''Called when parser finds a mute directive.
519
520        It is a dummy method and should be overridden for actual use.
521
522        Args:
523            span (tuple of int): Start and end line of the directive.
524        '''
525        self._log_event('mute', span)
526
527
528    def handle_endmute(self, span):
529        '''Called when parser finds an endmute directive.
530
531        It is a dummy method and should be overridden for actual use.
532
533        Args:
534            span (tuple of int): Start and end line of the directive.
535        '''
536        self._log_event('endmute', span)
537
538
539    def handle_stop(self, span, msg):
540        '''Called when parser finds an stop directive.
541
542        It is a dummy method and should be overridden for actual use.
543
544        Args:
545            span (tuple of int): Start and end line of the directive.
546            msg (str): Stop message.
547        '''
548        self._log_event('stop', span, msg=msg)
549
550
551    def handle_assert(self, span):
552        '''Called when parser finds an assert directive.
553
554        It is a dummy method and should be overridden for actual use.
555
556        Args:
557            span (tuple of int): Start and end line of the directive.
558        '''
559        self._log_event('assert', span)
560
561
562    @staticmethod
563    def _log_event(event, span=(-1, -1), **params):
564        print('{0}: {1} --> {2}'.format(event, span[0], span[1]))
565        for parname, parvalue in params.items():
566            print('  {0}: ->|{1}|<-'.format(parname, parvalue))
567        print()
568
569
570    def _parse_txt(self, includespan, fname, txt):
571        self.handle_include(includespan, fname)
572        self._parse(txt)
573        self.handle_endinclude(includespan, fname)
574
575
576    def _parse(self, txt, linenr=0, directcall=False):
577        pos = 0
578        for match in _ALL_DIRECTIVES_REGEXP.finditer(txt):
579            start, end = match.span()
580            if start > pos:
581                endlinenr = linenr + txt.count('\n', pos, start)
582                self._process_text(txt[pos:start], (linenr, endlinenr))
583                linenr = endlinenr
584            endlinenr = linenr + txt.count('\n', start, end)
585            span = (linenr, endlinenr)
586            ldirtype, ldir, idirtype, idir = match.groups()
587            if directcall and (idirtype is None or idirtype != '$'):
588                msg = 'only inline eval directives allowed in direct calls'
589                raise FyppFatalError(msg, self._curfile, span)
590            elif idirtype is not None:
591                if idir is None:
592                    msg = 'missing inline directive content'
593                    raise FyppFatalError(msg, self._curfile, span)
594                dirtype = idirtype
595                content = idir
596            elif ldirtype is not None:
597                if ldir is None:
598                    msg = 'missing line directive content'
599                    raise FyppFatalError(msg, self._curfile, span)
600                dirtype = ldirtype
601                content = _CONTLINE_REGEXP.sub('', ldir)
602            else:
603                # Comment directive
604                dirtype = None
605            if dirtype == '$':
606                self.handle_eval(span, content)
607            elif dirtype == '#':
608                self._process_control_dir(content, span)
609            elif dirtype == '@':
610                self._process_direct_call(content, span)
611            else:
612                self.handle_comment(span)
613            pos = end
614            linenr = endlinenr
615        if pos < len(txt):
616            endlinenr = linenr + txt.count('\n', pos)
617            self._process_text(txt[pos:], (linenr, endlinenr))
618
619
620    def _process_text(self, txt, span):
621        escaped_txt = self._unescape(txt)
622        self.handle_text(span, escaped_txt)
623
624
625    def _process_control_dir(self, content, span):
626        match = _CONTROL_DIR_REGEXP.match(content)
627        if not match:
628            msg = "invalid control directive content '{0}'".format(content)
629            raise FyppFatalError(msg, self._curfile, span)
630        directive, param = match.groups()
631        if directive == 'if':
632            self._check_param_presence(True, 'if', param, span)
633            self.handle_if(span, param)
634        elif directive == 'else':
635            self._check_param_presence(False, 'else', param, span)
636            self.handle_else(span)
637        elif directive == 'elif':
638            self._check_param_presence(True, 'elif', param, span)
639            self.handle_elif(span, param)
640        elif directive == 'endif':
641            self._check_param_presence(False, 'endif', param, span)
642            self.handle_endif(span)
643        elif directive == 'def':
644            self._check_param_presence(True, 'def', param, span)
645            self._check_not_inline_directive('def', span)
646            self._process_def(param, span)
647        elif directive == 'enddef':
648            self._process_enddef(param, span)
649        elif directive == 'set':
650            self._check_param_presence(True, 'set', param, span)
651            self._process_set(param, span)
652        elif directive == 'del':
653            self._check_param_presence(True, 'del', param, span)
654            self._process_del(param, span)
655        elif directive == 'for':
656            self._check_param_presence(True, 'for', param, span)
657            self._process_for(param, span)
658        elif directive == 'endfor':
659            self._check_param_presence(False, 'endfor', param, span)
660            self.handle_endfor(span)
661        elif directive == 'call' or directive == 'block':
662            self._check_param_presence(True, directive, param, span)
663            self._process_call(param, span, directive == 'block')
664        elif directive == 'nextarg' or directive == 'contains':
665            self._process_nextarg(param, span, directive == 'contains')
666        elif directive == 'endcall' or directive == 'endblock':
667            self._process_endcall(param, span, directive == 'endblock')
668        elif directive == 'include':
669            self._check_param_presence(True, 'include', param, span)
670            self._check_not_inline_directive('include', span)
671            self._process_include(param, span)
672        elif directive == 'mute':
673            self._check_param_presence(False, 'mute', param, span)
674            self._check_not_inline_directive('mute', span)
675            self.handle_mute(span)
676        elif directive == 'endmute':
677            self._check_param_presence(False, 'endmute', param, span)
678            self._check_not_inline_directive('endmute', span)
679            self.handle_endmute(span)
680        elif directive == 'stop':
681            self._check_param_presence(True, 'stop', param, span)
682            self._check_not_inline_directive('stop', span)
683            self.handle_stop(span, param)
684        elif directive == 'assert':
685            self._check_param_presence(True, 'assert', param, span)
686            self._check_not_inline_directive('assert', span)
687            self.handle_assert(span, param)
688        elif directive == 'global':
689            self._check_param_presence(True, 'global', param, span)
690            self._process_global(param, span)
691        else:
692            msg = "unknown directive '{0}'".format(directive)
693            raise FyppFatalError(msg, self._curfile, span)
694
695
696    def _process_direct_call(self, callexpr, span):
697        match = _DIRECT_CALL_REGEXP.match(callexpr)
698        if not match:
699            msg = "invalid direct call expression"
700            raise FyppFatalError(msg, self._curfile, span)
701        callname = match.group('callname')
702        self.handle_call(span, callname, None, False)
703        callparams = match.group('callparams')
704        if callparams is None or not callparams.strip():
705            args = []
706        else:
707            try:
708                args = [arg.strip() for arg in _argsplit_fortran(callparams)]
709            except Exception as exc:
710                msg = 'unable to parse direct call argument'
711                raise FyppFatalError(msg, self._curfile, span) from exc
712        for arg in args:
713            match = _DIRECT_CALL_KWARG_REGEXP.match(arg)
714            argval = arg[match.end():].strip()
715            # Remove enclosing braces if present
716            if argval.startswith('{'):
717                argval = argval[1:-1]
718            keyword = match.group('kwname')
719            self.handle_nextarg(span, keyword, False)
720            self._parse(argval, linenr=span[0], directcall=True)
721        self.handle_endcall(span, callname, False)
722
723
724    def _process_def(self, param, span):
725        match = _DEF_PARAM_REGEXP.match(param)
726        if not match:
727            msg = "invalid macro definition '{0}'".format(param)
728            raise FyppFatalError(msg, self._curfile, span)
729        name = match.group('name')
730        argexpr = match.group('args')
731        self.handle_def(span, name, argexpr)
732
733
734    def _process_enddef(self, param, span):
735        if param is not None:
736            match = _IDENTIFIER_NAME_REGEXP.match(param)
737            if not match:
738                msg = "invalid enddef parameter '{0}'".format(param)
739                raise FyppFatalError(msg, self._curfile, span)
740            param = match.group('name')
741        self.handle_enddef(span, param)
742
743
744    def _process_set(self, param, span):
745        match = _SET_PARAM_REGEXP.match(param)
746        if not match:
747            msg = "invalid variable assignment '{0}'".format(param)
748            raise FyppFatalError(msg, self._curfile, span)
749        self.handle_set(span, match.group('name'), match.group('expr'))
750
751
752    def _process_global(self, param, span):
753        match = _DEL_PARAM_REGEXP.match(param)
754        if not match:
755            msg = "invalid variable specification '{0}'".format(param)
756            raise FyppFatalError(msg, self._curfile, span)
757        self.handle_global(span, param)
758
759
760    def _process_del(self, param, span):
761        match = _DEL_PARAM_REGEXP.match(param)
762        if not match:
763            msg = "invalid variable specification '{0}'".format(param)
764            raise FyppFatalError(msg, self._curfile, span)
765        self.handle_del(span, param)
766
767
768    def _process_for(self, param, span):
769        match = _FOR_PARAM_REGEXP.match(param)
770        if not match:
771            msg = "invalid for loop declaration '{0}'".format(param)
772            raise FyppFatalError(msg, self._curfile, span)
773        loopexpr = match.group('loopexpr')
774        loopvars = [s.strip() for s in loopexpr.split(',')]
775        self.handle_for(span, loopvars, match.group('iter'))
776
777
778    def _process_call(self, param, span, blockcall):
779        match = _SIMPLE_CALLABLE_REGEXP.match(param)
780        if not match:
781            msg = "invalid callable expression '{}'".format(param)
782            raise FyppFatalError(msg, self._curfile, span)
783        name, args = match.groups()
784        self.handle_call(span, name, args, blockcall)
785
786
787    def _process_nextarg(self, param, span, blockcall):
788        if param is not None:
789            match = _IDENTIFIER_NAME_REGEXP.match(param)
790            if not match:
791                msg = "invalid nextarg parameter '{0}'".format(param)
792                raise FyppFatalError(msg, self._curfile, span)
793            param = match.group('name')
794        self.handle_nextarg(span, param, blockcall)
795
796
797    def _process_endcall(self, param, span, blockcall):
798        if param is not None:
799            match = _PREFIXED_IDENTIFIER_NAME_REGEXP.match(param)
800            if not match:
801                msg = "invalid endcall parameter '{0}'".format(param)
802                raise FyppFatalError(msg, self._curfile, span)
803            param = match.group('name')
804        self.handle_endcall(span, param, blockcall)
805
806
807    def _process_include(self, param, span):
808        match = _INCLUDE_PARAM_REGEXP.match(param)
809        if not match:
810            msg = "invalid include file declaration '{0}'".format(param)
811            raise FyppFatalError(msg, self._curfile, span)
812        fname = match.group('fname')
813        for incdir in [self._curdir] + self._includedirs:
814            fpath = os.path.join(incdir, fname)
815            if os.path.exists(fpath):
816                break
817        else:
818            msg = "include file '{0}' not found".format(fname)
819            raise FyppFatalError(msg, self._curfile, span)
820        inpfp = _open_input_file(fpath, self._encoding)
821        self._includefile(span, inpfp, fpath, os.path.dirname(fpath))
822        inpfp.close()
823
824
825    def _process_mute(self, span):
826        if span[0] == span[1]:
827            msg = 'Inline form of mute directive not allowed'
828            raise FyppFatalError(msg, self._curfile, span)
829        self.handle_mute(span)
830
831
832    def _process_endmute(self, span):
833        if span[0] == span[1]:
834            msg = 'Inline form of endmute directive not allowed'
835            raise FyppFatalError(msg, self._curfile, span)
836        self.handle_endmute(span)
837
838
839    def _check_param_presence(self, presence, directive, param, span):
840        if (param is not None) != presence:
841            if presence:
842                msg = 'missing data in {0} directive'.format(directive)
843            else:
844                msg = 'forbidden data in {0} directive'.format(directive)
845            raise FyppFatalError(msg, self._curfile, span)
846
847
848    def _check_not_inline_directive(self, directive, span):
849        if span[0] == span[1]:
850            msg = 'Inline form of {0} directive not allowed'.format(directive)
851            raise FyppFatalError(msg, self._curfile, span)
852
853
854    @staticmethod
855    def _unescape(txt):
856        txt = _UNESCAPE_TEXT_REGEXP1.sub(r'\1\2\3', txt)
857        txt = _UNESCAPE_TEXT_REGEXP2.sub(r'#\1\2', txt)
858        txt = _UNESCAPE_TEXT_REGEXP3.sub(r'\1\2\3', txt)
859        return txt
860
861
862class Builder:
863    '''Builds a tree representing a text with preprocessor directives.
864    '''
865
866    def __init__(self):
867        # The tree, which should be built.
868        self._tree = []
869
870        # List of all open constructs
871        self._open_blocks = []
872
873        # Nodes to which the open blocks have to be appended when closed
874        self._path = []
875
876        # Nr. of open blocks when file was opened. Used for checking whether all
877        # blocks have been closed, when file processing finishes.
878        self._nr_prev_blocks = []
879
880        # Current node, to which content should be added
881        self._curnode = self._tree
882
883        # Current file
884        self._curfile = None
885
886
887    def reset(self):
888        '''Resets the builder so that it starts to build a new tree.'''
889        self._tree = []
890        self._open_blocks = []
891        self._path = []
892        self._nr_prev_blocks = []
893        self._curnode = self._tree
894        self._curfile = None
895
896
897    def handle_include(self, span, fname):
898        '''Should be called to signalize change to new file.
899
900        Args:
901            span (tuple of int): Start and end line of the include directive
902                or None if called the first time for the main input.
903            fname (str): Name of the file to be included.
904        '''
905        self._path.append(self._curnode)
906        self._curnode = []
907        self._open_blocks.append(
908            ('include', self._curfile, [span], fname, None))
909        self._curfile = fname
910        self._nr_prev_blocks.append(len(self._open_blocks))
911
912
913    def handle_endinclude(self, span, fname):
914        '''Should be called when processing of a file finished.
915
916        Args:
917            span (tuple of int): Start and end line of the include directive
918                or None if called the first time for the main input.
919            fname (str): Name of the file which has been included.
920        '''
921        nprev_blocks = self._nr_prev_blocks.pop(-1)
922        if len(self._open_blocks) > nprev_blocks:
923            directive, fname, spans = self._open_blocks[-1][0:3]
924            msg = '{0} directive still unclosed when reaching end of file'\
925                  .format(directive)
926            raise FyppFatalError(msg, self._curfile, spans[0])
927        block = self._open_blocks.pop(-1)
928        directive, blockfname, spans = block[0:3]
929        if directive != 'include':
930            msg = 'internal error: last open block is not \'include\' when '\
931                  'closing file \'{0}\''.format(fname)
932            raise FyppFatalError(msg)
933        if span != spans[0]:
934            msg = 'internal error: span for include and endinclude differ ('\
935                  '{0} vs {1}'.format(span, spans[0])
936            raise FyppFatalError(msg)
937        oldfname, _ = block[3:5]
938        if fname != oldfname:
939            msg = 'internal error: mismatching file name in close_file event'\
940                  " (expected: '{0}', got: '{1}')".format(oldfname, fname)
941            raise FyppFatalError(msg, fname)
942        block = directive, blockfname, spans, fname, self._curnode
943        self._curnode = self._path.pop(-1)
944        self._curnode.append(block)
945        self._curfile = blockfname
946
947
948    def handle_if(self, span, cond):
949        '''Should be called to signalize an if directive.
950
951        Args:
952            span (tuple of int): Start and end line of the directive.
953            param (str): String representation of the branching condition.
954        '''
955        self._path.append(self._curnode)
956        self._curnode = []
957        self._open_blocks.append(('if', self._curfile, [span], [cond], []))
958
959
960    def handle_elif(self, span, cond):
961        '''Should be called to signalize an elif directive.
962
963        Args:
964            span (tuple of int): Start and end line of the directive.
965            cond (str): String representation of the branching condition.
966        '''
967        self._check_for_open_block(span, 'elif')
968        block = self._open_blocks[-1]
969        directive, _, spans = block[0:3]
970        self._check_if_matches_last(directive, 'if', spans[-1], span, 'elif')
971        conds, contents = block[3:5]
972        conds.append(cond)
973        contents.append(self._curnode)
974        spans.append(span)
975        self._curnode = []
976
977
978    def handle_else(self, span):
979        '''Should be called to signalize an else directive.
980
981        Args:
982            span (tuple of int): Start and end line of the directive.
983        '''
984        self._check_for_open_block(span, 'else')
985        block = self._open_blocks[-1]
986        directive, _, spans = block[0:3]
987        self._check_if_matches_last(directive, 'if', spans[-1], span, 'else')
988        conds, contents = block[3:5]
989        conds.append('True')
990        contents.append(self._curnode)
991        spans.append(span)
992        self._curnode = []
993
994
995    def handle_endif(self, span):
996        '''Should be called to signalize an endif directive.
997
998        Args:
999            span (tuple of int): Start and end line of the directive.
1000        '''
1001        self._check_for_open_block(span, 'endif')
1002        block = self._open_blocks.pop(-1)
1003        directive, _, spans = block[0:3]
1004        self._check_if_matches_last(directive, 'if', spans[-1], span, 'endif')
1005        _, contents = block[3:5]
1006        contents.append(self._curnode)
1007        spans.append(span)
1008        self._curnode = self._path.pop(-1)
1009        self._curnode.append(block)
1010
1011
1012    def handle_for(self, span, loopvar, iterator):
1013        '''Should be called to signalize a for directive.
1014
1015        Args:
1016            span (tuple of int): Start and end line of the directive.
1017            varexpr (str): String representation of the loop variable
1018                expression.
1019            iterator (str): String representation of the iterable.
1020        '''
1021        self._path.append(self._curnode)
1022        self._curnode = []
1023        self._open_blocks.append(('for', self._curfile, [span], loopvar,
1024                                  iterator, None))
1025
1026
1027    def handle_endfor(self, span):
1028        '''Should be called to signalize an endfor directive.
1029
1030        Args:
1031            span (tuple of int): Start and end line of the directive.
1032        '''
1033        self._check_for_open_block(span, 'endfor')
1034        block = self._open_blocks.pop(-1)
1035        directive, fname, spans = block[0:3]
1036        self._check_if_matches_last(directive, 'for', spans[-1], span, 'endfor')
1037        loopvar, iterator, dummy = block[3:6]
1038        spans.append(span)
1039        block = (directive, fname, spans, loopvar, iterator, self._curnode)
1040        self._curnode = self._path.pop(-1)
1041        self._curnode.append(block)
1042
1043
1044    def handle_def(self, span, name, argexpr):
1045        '''Should be called to signalize a def directive.
1046
1047        Args:
1048            span (tuple of int): Start and end line of the directive.
1049            name (str): Name of the macro to be defined.
1050            argexpr (str): Macro argument definition or None
1051        '''
1052        self._path.append(self._curnode)
1053        self._curnode = []
1054        defblock = ('def', self._curfile, [span], name, argexpr, None)
1055        self._open_blocks.append(defblock)
1056
1057
1058    def handle_enddef(self, span, name):
1059        '''Should be called to signalize an enddef directive.
1060
1061        Args:
1062            span (tuple of int): Start and end line of the directive.
1063            name (str): Name of the enddef statement. Could be None, if enddef
1064                was specified without name.
1065        '''
1066        self._check_for_open_block(span, 'enddef')
1067        block = self._open_blocks.pop(-1)
1068        directive, fname, spans = block[0:3]
1069        self._check_if_matches_last(directive, 'def', spans[-1], span, 'enddef')
1070        defname, argexpr, dummy = block[3:6]
1071        if name is not None and name != defname:
1072            msg = "wrong name in enddef directive "\
1073                  "(expected '{0}', got '{1}')".format(defname, name)
1074            raise FyppFatalError(msg, fname, span)
1075        spans.append(span)
1076        block = (directive, fname, spans, defname, argexpr, self._curnode)
1077        self._curnode = self._path.pop(-1)
1078        self._curnode.append(block)
1079
1080
1081    def handle_call(self, span, name, argexpr, blockcall):
1082        '''Should be called to signalize a call directive.
1083
1084        Args:
1085            span (tuple of int): Start and end line of the directive.
1086            name (str): Name of the callable to call
1087            argexpr (str or None): Argument expression containing additional
1088                arguments for the call.
1089            blockcall (bool): Whether the alternative "block / contains /
1090                endblock" calling directive has been used.
1091        '''
1092        self._path.append(self._curnode)
1093        self._curnode = []
1094        directive = 'block' if blockcall else 'call'
1095        self._open_blocks.append(
1096            (directive, self._curfile, [span, span], name, argexpr, [], []))
1097
1098
1099    def handle_nextarg(self, span, name, blockcall):
1100        '''Should be called to signalize a nextarg directive.
1101
1102        Args:
1103            span (tuple of int): Start and end line of the directive.
1104            name (str or None): Name of the argument following next or
1105                None if it should be the next positional argument.
1106            blockcall (bool): Whether the alternative "block / contains /
1107                endblock" calling directive has been used.
1108        '''
1109        self._check_for_open_block(span, 'nextarg')
1110        block = self._open_blocks[-1]
1111        directive, fname, spans = block[0:3]
1112        if blockcall:
1113            opened, current = 'block', 'contains'
1114        else:
1115            opened, current = 'call', 'nextarg'
1116        self._check_if_matches_last(directive, opened, spans[-1], span, current)
1117        args, argnames = block[5:7]
1118        args.append(self._curnode)
1119        spans.append(span)
1120        if name is not None:
1121            argnames.append(name)
1122        elif argnames:
1123            msg = 'non-keyword argument following keyword argument'
1124            raise FyppFatalError(msg, fname, span)
1125        self._curnode = []
1126
1127
1128    def handle_endcall(self, span, name, blockcall):
1129        '''Should be called to signalize an endcall directive.
1130
1131        Args:
1132            span (tuple of int): Start and end line of the directive.
1133            name (str): Name of the endcall statement. Could be None, if endcall
1134                was specified without name.
1135            blockcall (bool): Whether the alternative "block / contains /
1136                endblock" calling directive has been used.
1137        '''
1138        self._check_for_open_block(span, 'endcall')
1139        block = self._open_blocks.pop(-1)
1140        directive, fname, spans = block[0:3]
1141        callname, callargexpr, args, argnames = block[3:7]
1142        if blockcall:
1143            opened, current = 'block', 'endblock'
1144        else:
1145            opened, current = 'call', 'endcall'
1146        self._check_if_matches_last(directive, opened, spans[0], span, current)
1147
1148        if name is not None and name != callname:
1149            msg = "wrong name in {0} directive "\
1150                  "(expected '{1}', got '{2}')".format(current, callname, name)
1151            raise FyppFatalError(msg, fname, span)
1152        args.append(self._curnode)
1153        # If nextarg or endcall immediately followed call, then first argument
1154        # is empty and should be removed (to allow for calls without arguments
1155        # and named first argument in calls)
1156        if args and not args[0]:
1157            if len(argnames) == len(args):
1158                del argnames[0]
1159            del args[0]
1160            del spans[1]
1161        spans.append(span)
1162        block = (directive, fname, spans, callname, callargexpr, args, argnames)
1163        self._curnode = self._path.pop(-1)
1164        self._curnode.append(block)
1165
1166
1167    def handle_set(self, span, name, expr):
1168        '''Should be called to signalize a set directive.
1169
1170        Args:
1171            span (tuple of int): Start and end line of the directive.
1172            name (str): Name of the variable.
1173            expr (str): String representation of the expression to be assigned
1174                to the variable.
1175        '''
1176        self._curnode.append(('set', self._curfile, span, name, expr))
1177
1178
1179    def handle_global(self, span, name):
1180        '''Should be called to signalize a global directive.
1181
1182        Args:
1183            span (tuple of int): Start and end line of the directive.
1184            name (str): Name of the variable(s) to make global.
1185        '''
1186        self._curnode.append(('global', self._curfile, span, name))
1187
1188
1189    def handle_del(self, span, name):
1190        '''Should be called to signalize a del directive.
1191
1192        Args:
1193            span (tuple of int): Start and end line of the directive.
1194            name (str): Name of the variable(s) to delete.
1195        '''
1196        self._curnode.append(('del', self._curfile, span, name))
1197
1198
1199    def handle_eval(self, span, expr):
1200        '''Should be called to signalize an eval directive.
1201
1202        Args:
1203            span (tuple of int): Start and end line of the directive.
1204            expr (str): String representation of the Python expression to
1205                be evaluated.
1206        '''
1207        self._curnode.append(('eval', self._curfile, span, expr))
1208
1209
1210    def handle_comment(self, span):
1211        '''Should be called to signalize a comment directive.
1212
1213        The content of the comment is not needed by the builder, but it needs
1214        the span of the comment to generate proper line numbers if needed.
1215
1216        Args:
1217            span (tuple of int): Start and end line of the directive.
1218        '''
1219        self._curnode.append(('comment', self._curfile, span))
1220
1221
1222    def handle_text(self, span, txt):
1223        '''Should be called to pass text which goes to output unaltered.
1224
1225        Args:
1226            span (tuple of int): Start and end line of the text.
1227            txt (str): Text.
1228        '''
1229        self._curnode.append(('txt', self._curfile, span, txt))
1230
1231
1232    def handle_mute(self, span):
1233        '''Should be called to signalize a mute directive.
1234
1235        Args:
1236            span (tuple of int): Start and end line of the directive.
1237        '''
1238        self._path.append(self._curnode)
1239        self._curnode = []
1240        self._open_blocks.append(('mute', self._curfile, [span], None))
1241
1242
1243    def handle_endmute(self, span):
1244        '''Should be called to signalize an endmute directive.
1245
1246        Args:
1247            span (tuple of int): Start and end line of the directive.
1248        '''
1249        self._check_for_open_block(span, 'endmute')
1250        block = self._open_blocks.pop(-1)
1251        directive, fname, spans = block[0:3]
1252        self._check_if_matches_last(directive, 'mute', spans[-1], span,
1253                                    'endmute')
1254        spans.append(span)
1255        block = (directive, fname, spans, self._curnode)
1256        self._curnode = self._path.pop(-1)
1257        self._curnode.append(block)
1258
1259
1260    def handle_stop(self, span, msg):
1261        '''Should be called to signalize a stop directive.
1262
1263        Args:
1264            span (tuple of int): Start and end line of the directive.
1265        '''
1266        self._curnode.append(('stop', self._curfile, span, msg))
1267
1268
1269    def handle_assert(self, span, cond):
1270        '''Should be called to signalize an assert directive.
1271
1272        Args:
1273            span (tuple of int): Start and end line of the directive.
1274        '''
1275        self._curnode.append(('assert', self._curfile, span, cond))
1276
1277
1278    @property
1279    def tree(self):
1280        '''Returns the tree built by the Builder.'''
1281        return self._tree
1282
1283
1284    def _check_for_open_block(self, span, directive):
1285        if len(self._open_blocks) <= self._nr_prev_blocks[-1]:
1286            msg = 'unexpected {0} directive'.format(directive)
1287            raise FyppFatalError(msg, self._curfile, span)
1288
1289
1290    def _check_if_matches_last(self, lastdir, curdir, lastspan, curspan,
1291                               directive):
1292        if curdir != lastdir:
1293            msg = "mismatching '{0}' directive (last block opened was '{1}')"\
1294                .format(directive, lastdir)
1295            raise FyppFatalError(msg, self._curfile, curspan)
1296        inline_last = lastspan[0] == lastspan[1]
1297        inline_cur = curspan[0] == curspan[1]
1298        if inline_last != inline_cur:
1299            if inline_cur:
1300                msg = 'expecting line form of directive {0}'.format(directive)
1301            else:
1302                msg = 'expecting inline form of directive {0}'.format(directive)
1303            raise FyppFatalError(msg, self._curfile, curspan)
1304        elif inline_cur and curspan[0] != lastspan[0]:
1305            msg = 'inline directives of the same construct must be in the '\
1306                  'same row'
1307            raise FyppFatalError(msg, self._curfile, curspan)
1308
1309
1310class Renderer:
1311
1312    ''''Renders a tree.
1313
1314    Args:
1315        evaluator (Evaluator, optional): Evaluator to use when rendering eval
1316            directives. If None (default), Evaluator() is used.
1317        linenums (bool, optional): Whether linenums should be generated,
1318            defaults to False.
1319        contlinenums (bool, optional): Whether linenums for continuation
1320            should be generated, defaults to False.
1321        linenumformat (str, optional): 'std', 'cpp' or 'gfortran5' depending
1322            what kind of line directives should be created. Default: 'cpp'.
1323            Format 'std' emits #line pragmas, 'cpp' resembles GNU cpps special
1324            format, and 'gfortran5' adds to cpp a workaround for a bug introduced in GFortran 5.
1325        linefolder (callable): Callable to use when folding a line.
1326    '''
1327
1328    def __init__(self, evaluator=None, linenums=False, contlinenums=False,
1329                 linenumformat=None, linefolder=None):
1330        # Evaluator to use for Python expressions
1331        self._evaluator = Evaluator() if evaluator is None else evaluator
1332        self._evaluator.updateglobals(_SYSTEM_=platform.system(),
1333            _MACHINE_=platform.machine())
1334
1335        # Whether rendered output is diverted and will be processed
1336        # further before output (if True: no line numbering and post processing)
1337        self._diverted = False
1338
1339        # Whether file name and line numbers should be kept fixed and
1340        # not updated (typically when rendering macro content)
1341        self._fixedposition = False
1342
1343        # Whether line numbering directives should be emitted
1344        self._linenums = linenums
1345
1346        # Whether line numbering directives in continuation lines are needed.
1347        self._contlinenums = contlinenums
1348
1349        # Line number formatter function and whether gfortran5 fix is needed
1350        if linenumformat is None or linenumformat in ('cpp', 'gfortran5'):
1351            self._linenumdir = linenumdir_cpp
1352            self._linenum_gfortran5 = linenumformat == 'gfortran5'
1353        else:
1354            self._linenumdir = linenumdir_std
1355            self._linenum_gfortran5 = False
1356
1357        # Callable to be used for folding lines
1358        if linefolder is None:
1359            self._linefolder = lambda line: [line]
1360        else:
1361            self._linefolder = linefolder
1362
1363
1364    def render(self, tree, divert=False, fixposition=False):
1365        '''Renders a tree.
1366
1367        Args:
1368            tree (fypp-tree): Tree to render.
1369            divert (bool): Whether output will be diverted and sent for further
1370                processing, so that no line numbering directives and
1371                postprocessing are needed at this stage. (Default: False)
1372            fixposition (bool): Whether file name and line position (variables
1373                _FILE_ and _LINE_) should be kept at their current values or
1374                should be updated continuously. (Default: False).
1375
1376        Returns: str: Rendered string.
1377        '''
1378        diverted = self._diverted
1379        self._diverted = divert
1380        fixedposition_old = self._fixedposition
1381        self._fixedposition = self._fixedposition or fixposition
1382        output, eval_inds, eval_pos = self._render(tree)
1383        if not self._diverted and eval_inds:
1384            self._postprocess_eval_lines(output, eval_inds, eval_pos)
1385        self._diverted = diverted
1386        self._fixedposition = fixedposition_old
1387        txt = ''.join(output)
1388
1389        return txt
1390
1391
1392    def _render(self, tree):
1393        output = []
1394        eval_inds = []
1395        eval_pos = []
1396        for node in tree:
1397            cmd = node[0]
1398            if cmd == 'txt':
1399                output.append(node[3])
1400            elif cmd == 'if':
1401                out, ieval, peval = self._get_conditional_content(*node[1:5])
1402                eval_inds += _shiftinds(ieval, len(output))
1403                eval_pos += peval
1404                output += out
1405            elif cmd == 'eval':
1406                out, ieval, peval = self._get_eval(*node[1:4])
1407                eval_inds += _shiftinds(ieval, len(output))
1408                eval_pos += peval
1409                output += out
1410            elif cmd == 'def':
1411                result = self._define_macro(*node[1:6])
1412                output.append(result)
1413            elif cmd == 'set':
1414                result = self._define_variable(*node[1:5])
1415                output.append(result)
1416            elif cmd == 'del':
1417                self._delete_variable(*node[1:4])
1418            elif cmd == 'for':
1419                out, ieval, peval = self._get_iterated_content(*node[1:6])
1420                eval_inds += _shiftinds(ieval, len(output))
1421                eval_pos += peval
1422                output += out
1423            elif cmd == 'call' or cmd == 'block':
1424                out, ieval, peval = self._get_called_content(*node[1:7])
1425                eval_inds += _shiftinds(ieval, len(output))
1426                eval_pos += peval
1427                output += out
1428            elif cmd == 'include':
1429                out, ieval, peval = self._get_included_content(*node[1:5])
1430                eval_inds += _shiftinds(ieval, len(output))
1431                eval_pos += peval
1432                output += out
1433            elif cmd == 'comment':
1434                output.append(self._get_comment(*node[1:3]))
1435            elif cmd == 'mute':
1436                output.append(self._get_muted_content(*node[1:4]))
1437            elif cmd == 'stop':
1438                self._handle_stop(*node[1:4])
1439            elif cmd == 'assert':
1440                result = self._handle_assert(*node[1:4])
1441                output.append(result)
1442            elif cmd == 'global':
1443                self._add_global(*node[1:4])
1444            else:
1445                msg = "internal error: unknown command '{0}'".format(cmd)
1446                raise FyppFatalError(msg)
1447        return output, eval_inds, eval_pos
1448
1449
1450    def _get_eval(self, fname, span, expr):
1451        try:
1452            result = self._evaluate(expr, fname, span[0])
1453        except Exception as exc:
1454            msg = "exception occurred when evaluating '{0}'".format(expr)
1455            raise FyppFatalError(msg, fname, span) from exc
1456        out = []
1457        ieval = []
1458        peval = []
1459        if result is not None:
1460            out.append(str(result))
1461            if not self._diverted:
1462                ieval.append(0)
1463                peval.append((span, fname))
1464        if span[0] != span[1]:
1465            out.append('\n')
1466        return out, ieval, peval
1467
1468
1469    def _get_conditional_content(self, fname, spans, conditions, contents):
1470        out = []
1471        ieval = []
1472        peval = []
1473        multiline = (spans[0][0] != spans[-1][1])
1474        for condition, content, span in zip(conditions, contents, spans):
1475            try:
1476                cond = bool(self._evaluate(condition, fname, span[0]))
1477            except Exception as exc:
1478                msg = "exception occurred when evaluating '{0}'"\
1479                      .format(condition)
1480                raise FyppFatalError(msg, fname, span) from exc
1481            if cond:
1482                if self._linenums and not self._diverted and multiline:
1483                    out.append(self._linenumdir(span[1], fname))
1484                outcont, ievalcont, pevalcont = self._render(content)
1485                ieval += _shiftinds(ievalcont, len(out))
1486                peval += pevalcont
1487                out += outcont
1488                break
1489        if self._linenums and not self._diverted and multiline:
1490            out.append(self._linenumdir(spans[-1][1], fname))
1491        return out, ieval, peval
1492
1493
1494    def _get_iterated_content(self, fname, spans, loopvars, loopiter, content):
1495        out = []
1496        ieval = []
1497        peval = []
1498        try:
1499            iterobj = iter(self._evaluate(loopiter, fname, spans[0][0]))
1500        except Exception as exc:
1501            msg = "exception occurred when evaluating '{0}'"\
1502                .format(loopiter)
1503            raise FyppFatalError(msg, fname, spans[0]) from exc
1504        multiline = (spans[0][0] != spans[-1][1])
1505        for var in iterobj:
1506            if len(loopvars) == 1:
1507                self._define(loopvars[0], var)
1508            else:
1509                for varname, value in zip(loopvars, var):
1510                    self._define(varname, value)
1511            if self._linenums and not self._diverted and multiline:
1512                out.append(self._linenumdir(spans[0][1], fname))
1513            outcont, ievalcont, pevalcont = self._render(content)
1514            ieval += _shiftinds(ievalcont, len(out))
1515            peval += pevalcont
1516            out += outcont
1517        if self._linenums and not self._diverted and multiline:
1518            out.append(self._linenumdir(spans[1][1], fname))
1519        return out, ieval, peval
1520
1521
1522    def _get_called_content(self, fname, spans, name, argexpr, contents,
1523                            argnames):
1524        posargs, kwargs = self._get_call_arguments(fname, spans, argexpr,
1525                                                   contents, argnames)
1526        try:
1527            callobj = self._evaluate(name, fname, spans[0][0])
1528            result = callobj(*posargs, **kwargs)
1529        except Exception as exc:
1530            msg = "exception occurred when calling '{0}'".format(name)
1531            raise FyppFatalError(msg, fname, spans[0]) from exc
1532        self._update_predef_globals(fname, spans[0][0])
1533        span = (spans[0][0], spans[-1][1])
1534        out = []
1535        ieval = []
1536        peval = []
1537        if result is not None:
1538            out = [str(result)]
1539            if not self._diverted:
1540                ieval = [0]
1541                peval = [(span, fname)]
1542        if span[0] != span[1]:
1543            out.append('\n')
1544        return out, ieval, peval
1545
1546
1547    def _get_call_arguments(self, fname, spans, argexpr, contents, argnames):
1548        if argexpr is None:
1549            posargs = []
1550            kwargs = {}
1551        else:
1552            # Parse and evaluate arguments passed in call header
1553            self._evaluator.openscope()
1554            try:
1555                posargs, kwargs = self._evaluate(
1556                    '__getargvalues(' + argexpr + ')', fname, spans[0][0])
1557            except Exception as exc:
1558                msg = "unable to parse argument expression '{0}'"\
1559                    .format(argexpr)
1560                raise FyppFatalError(msg, fname, spans[0]) from exc
1561            self._evaluator.closescope()
1562
1563        # Render arguments passed in call body
1564        args = []
1565        for content in contents:
1566            self._evaluator.openscope()
1567            rendered = self.render(content, divert=True)
1568            self._evaluator.closescope()
1569            if rendered.endswith('\n'):
1570                rendered = rendered[:-1]
1571            args.append(rendered)
1572
1573        # Separate arguments in call body into positional and keyword ones:
1574        if argnames:
1575            posargs += args[:len(args) - len(argnames)]
1576            offset = len(args) - len(argnames)
1577            for iargname, argname in enumerate(argnames):
1578                ind = offset + iargname
1579                if argname in kwargs:
1580                    msg = "keyword argument '{0}' already defined"\
1581                        .format(argname)
1582                    raise FyppFatalError(msg, fname, spans[ind + 1])
1583                kwargs[argname] = args[ind]
1584        else:
1585            posargs += args
1586
1587        return posargs, kwargs
1588
1589
1590    def _get_included_content(self, fname, spans, includefname, content):
1591        includefile = spans[0] is not None
1592        out = []
1593        if self._linenums and not self._diverted:
1594            if includefile or self._linenum_gfortran5:
1595                out += self._linenumdir(0, includefname, _LINENUM_NEW_FILE)
1596            else:
1597                out += self._linenumdir(0, includefname)
1598        outcont, ieval, peval = self._render(content)
1599        ieval = _shiftinds(ieval, len(out))
1600        out += outcont
1601        if self._linenums and not self._diverted and includefile:
1602            out += self._linenumdir(spans[0][1], fname, _LINENUM_RETURN_TO_FILE)
1603        return out, ieval, peval
1604
1605
1606    def _define_macro(self, fname, spans, name, argexpr, content):
1607        if argexpr is None:
1608            args = []
1609            defaults = {}
1610            varpos = None
1611            varkw = None
1612        else:
1613            # Try to create a lambda function with the argument expression
1614            self._evaluator.openscope()
1615            lambdaexpr = 'lambda ' + argexpr + ': None'
1616            try:
1617                func = self._evaluate(lambdaexpr, fname, spans[0][0])
1618            except Exception as exc:
1619                msg = "exception occurred when evaluating argument expression "\
1620                      "'{0}'".format(argexpr)
1621                raise FyppFatalError(msg, fname, spans[0]) from exc
1622            self._evaluator.closescope()
1623            try:
1624                args, defaults, varpos, varkw = _get_callable_argspec(func)
1625            except Exception as exc:
1626                msg = "invalid argument expression '{0}'".format(argexpr)
1627                raise FyppFatalError(msg, fname, spans[0]) from exc
1628            named_args = args if varpos is None else args + [varpos]
1629            named_args = named_args if varkw is None else named_args + [varkw]
1630            for arg in named_args:
1631                if arg in _RESERVED_NAMES or arg.startswith(_RESERVED_PREFIX):
1632                    msg = "invalid argument name '{0}'".format(arg)
1633                    raise FyppFatalError(msg, fname, spans[0])
1634        result = ''
1635        try:
1636            macro = _Macro(
1637                name, fname, spans, args, defaults, varpos, varkw, content,
1638                self, self._evaluator, self._evaluator.localscope)
1639            self._define(name, macro)
1640        except Exception as exc:
1641            msg = "exception occurred when defining macro '{0}'"\
1642                .format(name)
1643            raise FyppFatalError(msg, fname, spans[0]) from exc
1644        if self._linenums and not self._diverted:
1645            result = self._linenumdir(spans[1][1], fname)
1646        return result
1647
1648
1649    def _define_variable(self, fname, span, name, valstr):
1650        result = ''
1651        try:
1652            if valstr is None:
1653                expr = None
1654            else:
1655                expr = self._evaluate(valstr, fname, span[0])
1656            self._define(name, expr)
1657        except Exception as exc:
1658            msg = "exception occurred when setting variable(s) '{0}' to '{1}'"\
1659                .format(name, valstr)
1660            raise FyppFatalError(msg, fname, span) from exc
1661        multiline = (span[0] != span[1])
1662        if self._linenums and not self._diverted and multiline:
1663            result = self._linenumdir(span[1], fname)
1664        return result
1665
1666
1667    def _delete_variable(self, fname, span, name):
1668        result = ''
1669        try:
1670            self._evaluator.undefine(name)
1671        except Exception as exc:
1672            msg = "exception occurred when deleting variable(s) '{0}'"\
1673                  .format(name)
1674            raise FyppFatalError(msg, fname, span) from exc
1675        multiline = (span[0] != span[1])
1676        if self._linenums and not self._diverted and multiline:
1677            result = self._linenumdir(span[1], fname)
1678        return result
1679
1680
1681    def _add_global(self, fname, span, name):
1682        result = ''
1683        try:
1684            self._evaluator.addglobal(name)
1685        except Exception as exc:
1686            msg = "exception occurred when making variable(s) '{0}' global"\
1687                .format(name)
1688            raise FyppFatalError(msg, fname, span) from exc
1689        multiline = (span[0] != span[1])
1690        if self._linenums and not self._diverted and multiline:
1691            result = self._linenumdir(span[1], fname)
1692        return result
1693
1694
1695    def _get_comment(self, fname, span):
1696        if self._linenums and not self._diverted:
1697            return self._linenumdir(span[1], fname)
1698        return ''
1699
1700
1701    def _get_muted_content(self, fname, spans, content):
1702        self._render(content)
1703        if self._linenums and not self._diverted:
1704            return self._linenumdir(spans[-1][1], fname)
1705        return ''
1706
1707
1708    def _handle_stop(self, fname, span, msgstr):
1709        try:
1710            msg = str(self._evaluate(msgstr, fname, span[0]))
1711        except Exception as exc:
1712            msg = "exception occurred when evaluating stop message '{0}'"\
1713                .format(msgstr)
1714            raise FyppFatalError(msg, fname, span) from exc
1715        raise FyppStopRequest(msg, fname, span)
1716
1717
1718    def _handle_assert(self, fname, span, expr):
1719        result = ''
1720        try:
1721            cond = bool(self._evaluate(expr, fname, span[0]))
1722        except Exception as exc:
1723            msg = "exception occurred when evaluating assert condition '{0}'"\
1724                .format(expr)
1725            raise FyppFatalError(msg, fname, span) from exc
1726        if not cond:
1727            msg = "Assertion failed ('{0}')".format(expr)
1728            raise FyppStopRequest(msg, fname, span)
1729        if self._linenums and not self._diverted:
1730            result = self._linenumdir(span[1], fname)
1731        return result
1732
1733
1734    def _evaluate(self, expr, fname, linenr):
1735        self._update_predef_globals(fname, linenr)
1736        result = self._evaluator.evaluate(expr)
1737        self._update_predef_globals(fname, linenr)
1738        return result
1739
1740
1741    def _update_predef_globals(self, fname, linenr):
1742        self._evaluator.updatelocals(
1743            _DATE_=time.strftime('%Y-%m-%d'), _TIME_=time.strftime('%H:%M:%S'),
1744            _THIS_FILE_=fname, _THIS_LINE_=linenr + 1)
1745        if not self._fixedposition:
1746            self._evaluator.updateglobals(_FILE_=fname, _LINE_=linenr + 1)
1747
1748
1749    def _define(self, var, value):
1750        self._evaluator.define(var, value)
1751
1752
1753    def _postprocess_eval_lines(self, output, eval_inds, eval_pos):
1754        ilastproc = -1
1755        for ieval, ind in enumerate(eval_inds):
1756            span, fname = eval_pos[ieval]
1757            if ind <= ilastproc:
1758                continue
1759            iprev, eolprev = self._find_last_eol(output, ind)
1760            inext, eolnext = self._find_next_eol(output, ind)
1761            curline = self._glue_line(output, ind, iprev, eolprev, inext,
1762                                      eolnext)
1763            output[iprev + 1:inext] = [''] * (inext - iprev - 1)
1764            output[ind] = self._postprocess_eval_line(curline, fname, span)
1765            ilastproc = inext
1766
1767
1768    @staticmethod
1769    def _find_last_eol(output, ind):
1770        'Find last newline before current position.'
1771        iprev = ind - 1
1772        while iprev >= 0:
1773            eolprev = output[iprev].rfind('\n')
1774            if eolprev != -1:
1775                break
1776            iprev -= 1
1777        else:
1778            iprev = 0
1779            eolprev = -1
1780        return iprev, eolprev
1781
1782
1783    @staticmethod
1784    def _find_next_eol(output, ind):
1785        'Find last newline before current position.'
1786        # find first eol after expr. evaluation
1787        inext = ind + 1
1788        while inext < len(output):
1789            eolnext = output[inext].find('\n')
1790            if eolnext != -1:
1791                break
1792            inext += 1
1793        else:
1794            inext = len(output) - 1
1795            eolnext = len(output[-1]) - 1
1796        return inext, eolnext
1797
1798
1799    @staticmethod
1800    def _glue_line(output, ind, iprev, eolprev, inext, eolnext):
1801        'Create line from parts between specified boundaries.'
1802        curline_parts = []
1803        if iprev != ind:
1804            curline_parts = [output[iprev][eolprev + 1:]]
1805            output[iprev] = output[iprev][:eolprev + 1]
1806        curline_parts.extend(output[iprev + 1:ind])
1807        curline_parts.extend(output[ind])
1808        curline_parts.extend(output[ind + 1:inext])
1809        if inext != ind:
1810            curline_parts.append(output[inext][:eolnext + 1])
1811            output[inext] = output[inext][eolnext + 1:]
1812        return ''.join(curline_parts)
1813
1814
1815    def _postprocess_eval_line(self, evalline, fname, span):
1816        lines = evalline.split('\n')
1817        # If line ended on '\n', last element is ''. We remove it and
1818        # add the trailing newline later manually.
1819        trailing_newline = (lines[-1] == '')
1820        if trailing_newline:
1821            del lines[-1]
1822        lnum = self._linenumdir(span[0], fname) if self._linenums else ''
1823        clnum = lnum if self._contlinenums else ''
1824        linenumsep = '\n' + lnum
1825        clinenumsep = '\n' + clnum
1826        foldedlines = [self._foldline(line) for line in lines]
1827        outlines = [clinenumsep.join(lines) for lines in foldedlines]
1828        result = linenumsep.join(outlines)
1829        # Add missing trailing newline
1830        if trailing_newline:
1831            trailing = '\n'
1832            if self._linenums:
1833                # Last line was folded, but no linenums were generated for
1834                # the continuation lines -> current line position is not
1835                # in sync with the one calculated from the last line number
1836                unsync = (
1837                    len(foldedlines) and len(foldedlines[-1]) > 1
1838                    and not self._contlinenums)
1839                # Eval directive in source consists of more than one line
1840                multiline = span[1] - span[0] > 1
1841                if unsync or multiline:
1842                    # For inline eval directives span[0] == span[1]
1843                    # -> next line is span[0] + 1 and not span[1] as for
1844                    # line eval directives
1845                    nextline = max(span[1], span[0] + 1)
1846                    trailing += self._linenumdir(nextline, fname)
1847        else:
1848            trailing = ''
1849        return result + trailing
1850
1851
1852    def _foldline(self, line):
1853        if _COMMENTLINE_REGEXP.match(line) is None:
1854            return self._linefolder(line)
1855        return [line]
1856
1857
1858class Evaluator:
1859
1860    '''Provides an isolated environment for evaluating Python expressions.
1861
1862    It restricts the builtins which can be used within this environment to a
1863    (hopefully safe) subset. Additionally it defines the functions which are
1864    provided by the preprocessor for the eval directives.
1865
1866    Args:
1867        env (dict, optional): Initial definitions for the environment, defaults
1868            to None.
1869    '''
1870
1871    # Restricted builtins working in all supported Python verions. Version
1872    # specific ones are added dynamically in _get_restricted_builtins().
1873    _RESTRICTED_BUILTINS = {
1874        'abs': builtins.abs,
1875        'all': builtins.all,
1876        'any': builtins.any,
1877        'bin': builtins.bin,
1878        'bool': builtins.bool,
1879        'bytearray': builtins.bytearray,
1880        'bytes': builtins.bytes,
1881        'chr': builtins.chr,
1882        'classmethod': builtins.classmethod,
1883        'complex': builtins.complex,
1884        'delattr': builtins.delattr,
1885        'dict': builtins.dict,
1886        'dir': builtins.dir,
1887        'divmod': builtins.divmod,
1888        'enumerate': builtins.enumerate,
1889        'filter': builtins.filter,
1890        'float': builtins.float,
1891        'format': builtins.format,
1892        'frozenset': builtins.frozenset,
1893        'getattr': builtins.getattr,
1894        'globals': builtins.globals,
1895        'hasattr': builtins.hasattr,
1896        'hash': builtins.hash,
1897        'hex': builtins.hex,
1898        'id': builtins.id,
1899        'int': builtins.int,
1900        'isinstance': builtins.isinstance,
1901        'issubclass': builtins.issubclass,
1902        'iter': builtins.iter,
1903        'len': builtins.len,
1904        'list': builtins.list,
1905        'locals': builtins.locals,
1906        'map': builtins.map,
1907        'max': builtins.max,
1908        'min': builtins.min,
1909        'next': builtins.next,
1910        'object': builtins.object,
1911        'oct': builtins.oct,
1912        'ord': builtins.ord,
1913        'pow': builtins.pow,
1914        'property': builtins.property,
1915        'range': builtins.range,
1916        'repr': builtins.repr,
1917        'reversed': builtins.reversed,
1918        'round': builtins.round,
1919        'set': builtins.set,
1920        'setattr': builtins.setattr,
1921        'slice': builtins.slice,
1922        'sorted': builtins.sorted,
1923        'staticmethod': builtins.staticmethod,
1924        'str': builtins.str,
1925        'sum': builtins.sum,
1926        'super': builtins.super,
1927        'tuple': builtins.tuple,
1928        'type': builtins.type,
1929        'vars': builtins.vars,
1930        'zip': builtins.zip,
1931    }
1932
1933
1934    def __init__(self, env=None):
1935
1936        # Global scope
1937        self._globals = env if env is not None else {}
1938
1939        # Local scope(s)
1940        self._locals = None
1941        self._locals_stack = []
1942
1943        # Variables which are references to entries in global scope
1944        self._globalrefs = None
1945        self._globalrefs_stack = []
1946
1947        # Current scope (globals + locals in all embedding and in current scope)
1948        self._scope = self._globals
1949
1950        # Turn on restricted mode
1951        self._restrict_builtins()
1952
1953
1954    def evaluate(self, expr):
1955        '''Evaluate a Python expression using the `eval()` builtin.
1956
1957        Args:
1958            expr (str): String represantion of the expression.
1959
1960        Return:
1961            Python object: Result of the expression evaluation.
1962        '''
1963        result = eval(expr, self._scope)
1964        return result
1965
1966
1967    def import_module(self, module):
1968        '''Import a module into the evaluator.
1969
1970        Note: Import only trustworthy modules! Module imports are global,
1971        therefore, importing a malicious module which manipulates other global
1972        modules could affect code behaviour outside of the Evaluator as well.
1973
1974        Args:
1975            module (str): Python module to import.
1976
1977        Raises:
1978            FyppFatalError: If module could not be imported.
1979
1980        '''
1981        rootmod = module.split('.', 1)[0]
1982        try:
1983            imported = __import__(module, self._scope)
1984            self.define(rootmod, imported)
1985        except Exception as exc:
1986            msg = "failed to import module '{0}'".format(module)
1987            raise FyppFatalError(msg) from exc
1988
1989
1990    def define(self, name, value):
1991        '''Define a Python entity.
1992
1993        Args:
1994            name (str): Name of the entity.
1995            value (Python object): Value of the entity.
1996
1997        Raises:
1998            FyppFatalError: If name starts with the reserved prefix or if it is
1999                a reserved name.
2000        '''
2001        varnames = self._get_variable_names(name)
2002        if len(varnames) == 1:
2003            value = (value,)
2004        elif len(varnames) != len(value):
2005            msg = 'value for tuple assignment has incompatible length'
2006            raise FyppFatalError(msg)
2007        for varname, varvalue in zip(varnames, value):
2008            self._check_variable_name(varname)
2009            if self._locals is None:
2010                self._globals[varname] = varvalue
2011            else:
2012                if varname in self._globalrefs:
2013                    self._globals[varname] = varvalue
2014                else:
2015                    self._locals[varname] = varvalue
2016                self._scope[varname] = varvalue
2017
2018
2019    def undefine(self, name):
2020        '''Undefine a Python entity.
2021
2022        Args:
2023            name (str): Name of the entity to undefine.
2024
2025        Raises:
2026            FyppFatalError: If name starts with the reserved prefix or if it is
2027                a reserved name.
2028        '''
2029        varnames = self._get_variable_names(name)
2030        for varname in varnames:
2031            self._check_variable_name(varname)
2032            deleted = False
2033            if self._locals is None:
2034                if varname in self._globals:
2035                    del self._globals[varname]
2036                    deleted = True
2037            else:
2038                if varname in self._locals:
2039                    del self._locals[varname]
2040                    del self._scope[varname]
2041                    deleted = True
2042                elif varname in self._globalrefs and varname in self._globals:
2043                    del self._globals[varname]
2044                    del self._scope[varname]
2045                    deleted = True
2046            if not deleted:
2047                msg = "lookup for an erasable instance of '{0}' failed"\
2048                      .format(varname)
2049                raise FyppFatalError(msg)
2050
2051
2052    def addglobal(self, name):
2053        '''Define a given entity as global.
2054
2055        Args:
2056            name (str): Name of the entity to make global.
2057
2058        Raises:
2059            FyppFatalError: If entity name is invalid or if the current scope is
2060                 a local scope and entity is already defined in it.
2061        '''
2062        varnames = self._get_variable_names(name)
2063        for varname in varnames:
2064            self._check_variable_name(varname)
2065            if self._locals is not None:
2066                if varname in self._locals:
2067                    msg = "variable '{0}' already defined in local scope"\
2068                          .format(varname)
2069                    raise FyppFatalError(msg)
2070                self._globalrefs.add(varname)
2071
2072
2073    def updateglobals(self, **vardict):
2074        '''Update variables in the global scope.
2075
2076        This is a shortcut function to inject protected variables in the global
2077        scope without extensive checks (as in define()). Vardict must not
2078        contain any global entries which can be shadowed in local scopes
2079        (e.g. should only contain variables with forbidden prefix).
2080
2081        Args:
2082            **vardict: variable definitions.
2083
2084        '''
2085        self._scope.update(vardict)
2086        if self._locals is not None:
2087            self._globals.update(vardict)
2088
2089
2090    def updatelocals(self, **vardict):
2091        '''Update variables in the local scope.
2092
2093        This is a shortcut function to inject variables in the local scope
2094        without extensive checks (as in define()). Vardict must not contain any
2095        entries which have been made global via addglobal() before. In order to
2096        ensure this, updatelocals() should be called immediately after
2097        openscope(), or with variable names, which are warrantedly not globals
2098        (e.g variables starting with forbidden prefix)
2099
2100        Args:
2101            **vardict: variable definitions.
2102        '''
2103        self._scope.update(vardict)
2104        if self._locals is not None:
2105            self._locals.update(vardict)
2106
2107
2108    def openscope(self, customlocals=None):
2109        '''Opens a new (embedded) scope.
2110
2111        Args:
2112            customlocals (dict): By default, the locals of the embedding scope
2113                are visible in the new one. When this is not the desired
2114                behaviour a dictionary of customized locals can be passed,
2115                and those locals will become the only visible ones.
2116        '''
2117        self._locals_stack.append(self._locals)
2118        self._globalrefs_stack.append(self._globalrefs)
2119        if customlocals is not None:
2120            self._locals = customlocals.copy()
2121        elif self._locals is not None:
2122            self._locals = self._locals.copy()
2123        else:
2124            self._locals = {}
2125        self._globalrefs = set()
2126        self._scope = self._globals.copy()
2127        self._scope.update(self._locals)
2128
2129
2130    def closescope(self):
2131        '''Close scope and restore embedding scope.'''
2132        self._locals = self._locals_stack.pop(-1)
2133        self._globalrefs = self._globalrefs_stack.pop(-1)
2134        if self._locals is not None:
2135            self._scope = self._globals.copy()
2136            self._scope.update(self._locals)
2137        else:
2138            self._scope = self._globals
2139
2140
2141    @property
2142    def globalscope(self):
2143        'Dictionary of the global scope.'
2144        return self._globals
2145
2146
2147    @property
2148    def localscope(self):
2149        'Dictionary of the current local scope.'
2150        return self._locals
2151
2152
2153    def _restrict_builtins(self):
2154        builtindict = self._get_restricted_builtins()
2155        builtindict['__import__'] = self._func_import
2156        builtindict['defined'] = self._func_defined
2157        builtindict['setvar'] = self._func_setvar
2158        builtindict['getvar'] = self._func_getvar
2159        builtindict['delvar'] = self._func_delvar
2160        builtindict['globalvar'] = self._func_globalvar
2161        builtindict['__getargvalues'] = self._func_getargvalues
2162        self._globals['__builtins__'] = builtindict
2163
2164
2165    @classmethod
2166    def _get_restricted_builtins(cls):
2167        bidict = dict(cls._RESTRICTED_BUILTINS)
2168        return bidict
2169
2170
2171    @staticmethod
2172    def _get_variable_names(varexpr):
2173        lpar = varexpr.startswith('(')
2174        rpar = varexpr.endswith(')')
2175        if lpar != rpar:
2176            msg = "unbalanced parenthesis around variable varexpr(s) in '{0}'"\
2177                .format(varexpr)
2178            raise FyppFatalError(msg, None, None)
2179        if lpar:
2180            varexpr = varexpr[1:-1]
2181        varnames = [s.strip() for s in varexpr.split(',')]
2182        return varnames
2183
2184
2185    @staticmethod
2186    def _check_variable_name(varname):
2187        if varname.startswith(_RESERVED_PREFIX):
2188            msg = "Name '{0}' starts with reserved prefix '{1}'"\
2189                .format(varname, _RESERVED_PREFIX)
2190            raise FyppFatalError(msg, None, None)
2191        if varname in _RESERVED_NAMES:
2192            msg = "Name '{0}' is reserved and can not be redefined"\
2193                .format(varname)
2194            raise FyppFatalError(msg, None, None)
2195
2196
2197    def _func_defined(self, var):
2198        defined = var in self._scope
2199        return defined
2200
2201
2202    def _func_import(self, name, *_, **__):
2203        module = self._scope.get(name, None)
2204        if module is not None and isinstance(module, types.ModuleType):
2205            return module
2206        msg = "Import of module '{0}' via '__import__' not allowed".format(name)
2207        raise ImportError(msg)
2208
2209
2210    def _func_setvar(self, *namesvalues):
2211        if len(namesvalues) % 2:
2212            msg = 'setvar function needs an even number of arguments'
2213            raise FyppFatalError(msg)
2214        for ind in range(0, len(namesvalues), 2):
2215            self.define(namesvalues[ind], namesvalues[ind + 1])
2216
2217
2218    def _func_getvar(self, name, defvalue=None):
2219        if name in self._scope:
2220            return self._scope[name]
2221        return defvalue
2222
2223
2224    def _func_delvar(self, *names):
2225        for name in names:
2226            self.undefine(name)
2227
2228
2229    def _func_globalvar(self, *names):
2230        for name in names:
2231            self.addglobal(name)
2232
2233
2234    @staticmethod
2235    def _func_getargvalues(*args, **kwargs):
2236        return list(args), kwargs
2237
2238
2239
2240class _Macro:
2241
2242    '''Represents a user defined macro.
2243
2244    This object should only be initiatied by a Renderer instance, as it
2245    needs access to Renderers internal variables and methods.
2246
2247    Args:
2248        name (str): Name of the macro.
2249        fname (str): The file where the macro was defined.
2250        spans (str): Line spans of macro definition.
2251        argnames (list of str): Macro dummy arguments.
2252        varpos (str): Name of variable positional argument or None.
2253        varkw (str): Name of variable keyword argument or None.
2254        content (list): Content of the macro as tree.
2255        renderer (Renderer): Renderer to use for evaluating macro content.
2256        localscope (dict): Dictionary with local variables, which should be used
2257            the local scope, when the macro is called. Default: None (empty
2258            local scope).
2259    '''
2260
2261    def __init__(self, name, fname, spans, argnames, defaults, varpos, varkw,
2262                 content, renderer, evaluator, localscope=None):
2263        self._name = name
2264        self._fname = fname
2265        self._spans = spans
2266        self._argnames = argnames
2267        self._defaults = defaults
2268        self._varpos = varpos
2269        self._varkw = varkw
2270        self._content = content
2271        self._renderer = renderer
2272        self._evaluator = evaluator
2273        self._localscope = localscope if localscope is not None else {}
2274
2275
2276    def __call__(self, *args, **keywords):
2277        argdict = self._process_arguments(args, keywords)
2278        self._evaluator.openscope(customlocals=self._localscope)
2279        self._evaluator.updatelocals(**argdict)
2280        output = self._renderer.render(self._content, divert=True,
2281                                       fixposition=True)
2282        self._evaluator.closescope()
2283        if output.endswith('\n'):
2284            return output[:-1]
2285        return output
2286
2287
2288    def _process_arguments(self, args, keywords):
2289        kwdict = dict(keywords)
2290        argdict = {}
2291        nargs = min(len(args), len(self._argnames))
2292        for iarg in range(nargs):
2293            argdict[self._argnames[iarg]] = args[iarg]
2294        if nargs < len(args):
2295            if self._varpos is None:
2296                msg = "macro '{0}' called with too many positional arguments "\
2297                      "(expected: {1}, received: {2})"\
2298                      .format(self._name, len(self._argnames), len(args))
2299                raise FyppFatalError(msg, self._fname, self._spans[0])
2300            else:
2301                argdict[self._varpos] = list(args[nargs:])
2302        elif self._varpos is not None:
2303            argdict[self._varpos] = []
2304        for argname in self._argnames[:nargs]:
2305            if argname in kwdict:
2306                msg = "got multiple values for argument '{0}'".format(argname)
2307                raise FyppFatalError(msg, self._fname, self._spans[0])
2308        if nargs < len(self._argnames):
2309            for argname in self._argnames[nargs:]:
2310                if argname in kwdict:
2311                    argdict[argname] = kwdict.pop(argname)
2312                elif argname in self._defaults:
2313                    argdict[argname] = self._defaults[argname]
2314                else:
2315                    msg = "macro '{0}' called without mandatory positional "\
2316                          "argument '{1}'".format(self._name, argname)
2317                    raise FyppFatalError(msg, self._fname, self._spans[0])
2318        if kwdict and self._varkw is None:
2319            kwstr = "', '".join(kwdict.keys())
2320            msg = "macro '{0}' called with unknown keyword argument(s) '{1}'"\
2321                  .format(self._name, kwstr)
2322            raise FyppFatalError(msg, self._fname, self._spans[0])
2323        if self._varkw is not None:
2324            argdict[self._varkw] = kwdict
2325        return argdict
2326
2327
2328
2329class Processor:
2330
2331    '''Connects various objects with each other to create a processor.
2332
2333    Args:
2334        parser (Parser, optional): Parser to use for parsing text. If None
2335            (default), `Parser()` is used.
2336        builder (Builder, optional): Builder to use for building the tree
2337            representation of the text. If None (default), `Builder()` is used.
2338        renderer (Renderer, optional): Renderer to use for rendering the
2339            output. If None (default), `Renderer()` is used with a default
2340            Evaluator().
2341        evaluator (Evaluator, optional): Evaluator to use for evaluating Python
2342            expressions. If None (default), `Evaluator()` is used.
2343    '''
2344
2345    def __init__(self, parser=None, builder=None, renderer=None,
2346                 evaluator=None):
2347        self._parser = Parser() if parser is None else parser
2348        self._builder = Builder() if builder is None else builder
2349        if renderer is None:
2350            evaluator = Evaluator() if evaluator is None else evaluator
2351            self._renderer = Renderer(evaluator)
2352        else:
2353            self._renderer = renderer
2354
2355        self._parser.handle_include = self._builder.handle_include
2356        self._parser.handle_endinclude = self._builder.handle_endinclude
2357        self._parser.handle_if = self._builder.handle_if
2358        self._parser.handle_else = self._builder.handle_else
2359        self._parser.handle_elif = self._builder.handle_elif
2360        self._parser.handle_endif = self._builder.handle_endif
2361        self._parser.handle_eval = self._builder.handle_eval
2362        self._parser.handle_text = self._builder.handle_text
2363        self._parser.handle_def = self._builder.handle_def
2364        self._parser.handle_enddef = self._builder.handle_enddef
2365        self._parser.handle_set = self._builder.handle_set
2366        self._parser.handle_del = self._builder.handle_del
2367        self._parser.handle_global = self._builder.handle_global
2368        self._parser.handle_for = self._builder.handle_for
2369        self._parser.handle_endfor = self._builder.handle_endfor
2370        self._parser.handle_call = self._builder.handle_call
2371        self._parser.handle_nextarg = self._builder.handle_nextarg
2372        self._parser.handle_endcall = self._builder.handle_endcall
2373        self._parser.handle_comment = self._builder.handle_comment
2374        self._parser.handle_mute = self._builder.handle_mute
2375        self._parser.handle_endmute = self._builder.handle_endmute
2376        self._parser.handle_stop = self._builder.handle_stop
2377        self._parser.handle_assert = self._builder.handle_assert
2378
2379
2380    def process_file(self, fname):
2381        '''Processeses a file.
2382
2383        Args:
2384            fname (str): Name of the file to process.
2385
2386        Returns:
2387            str: Processed content.
2388        '''
2389        self._parser.parsefile(fname)
2390        return self._render()
2391
2392
2393    def process_text(self, txt):
2394        '''Processes a string.
2395
2396        Args:
2397            txt (str): Text to process.
2398
2399        Returns:
2400            str: Processed content.
2401        '''
2402        self._parser.parse(txt)
2403        return self._render()
2404
2405
2406    def _render(self):
2407        output = self._renderer.render(self._builder.tree)
2408        self._builder.reset()
2409        return ''.join(output)
2410
2411
2412class Fypp:
2413
2414    '''Fypp preprocessor.
2415
2416    You can invoke it like ::
2417
2418        tool = fypp.Fypp()
2419        tool.process_file('file.in', 'file.out')
2420
2421    to initialize Fypp with default options, process `file.in` and write the
2422    result to `file.out`. If the input should be read from a string, the
2423    ``process_text()`` method can be used::
2424
2425        tool = fypp.Fypp()
2426        output = tool.process_text('#:if DEBUG > 0\\nprint *, "DEBUG"\\n#:endif\\n')
2427
2428    If you want to fine tune Fypps behaviour, pass a customized `FyppOptions`_
2429    instance at initialization::
2430
2431        options = fypp.FyppOptions()
2432        options.fixed_format = True
2433        tool = fypp.Fypp(options)
2434
2435    Alternatively, you can use the command line parser ``optparse.OptionParser``
2436    to set options for Fypp. The function ``get_option_parser()`` returns you a
2437    default option parser. You can then use its ``parse_args()`` method to
2438    obtain settings by reading the command line arguments::
2439
2440        optparser = fypp.get_option_parser()
2441        options, leftover = optparser.parse_args()
2442        tool = fypp.Fypp(options)
2443
2444    The command line options can also be passed directly as a list when
2445    calling ``parse_args()``::
2446
2447        args = ['-DDEBUG=0', 'input.fpp', 'output.f90']
2448        optparser = fypp.get_option_parser()
2449        options, leftover = optparser.parse_args(args=args)
2450        tool = fypp.Fypp(options)
2451
2452    For even more fine-grained control over how Fypp works, you can pass in
2453    custom factory methods that handle construction of the evaluator, parser,
2454    builder and renderer components. These factory methods must have the same
2455    signature as the corresponding component's constructor. As an example of
2456    using a builder that's customized by subclassing::
2457
2458        class MyBuilder(fypp.Builder):
2459
2460            def __init__(self):
2461               super().__init__()
2462               ...additional initialization...
2463
2464        tool = fypp.Fypp(options, builder_factory=MyBuilder)
2465
2466
2467    Args:
2468        options (object): Object containing the settings for Fypp. You typically
2469            would pass a customized `FyppOptions`_ instance or an
2470            ``optparse.Values`` object as returned by the option parser. If not
2471            present, the default settings in `FyppOptions`_ are used.
2472        evaluator_factory (function): Factory function that returns an Evaluator
2473            object. Its call signature must match that of the Evaluator
2474            constructor. If not present, ``Evaluator`` is used.
2475        parser_factory (function): Factory function that returns a Parser
2476            object.  Its call signature must match that of the Parser
2477            constructor. If not present, ``Parser`` is used.
2478        builder_factory (function): Factory function that returns a Builder
2479            object.  Its call signature must match that of the Builder
2480            constructor. If not present, ``Builder`` is used.
2481        renderer_factory (function): Factory function that returns a Renderer
2482            object. Its call signature must match that of the Renderer
2483            constructor.  If not present, ``Renderer`` is used.
2484    '''
2485
2486    def __init__(self, options=None, evaluator_factory=Evaluator,
2487                 parser_factory=Parser, builder_factory=Builder,
2488                 renderer_factory=Renderer):
2489        syspath = self._get_syspath_without_scriptdir()
2490        self._adjust_syspath(syspath)
2491        if options is None:
2492            options = FyppOptions()
2493        if inspect.signature(evaluator_factory) == inspect.signature(Evaluator):
2494            evaluator = evaluator_factory()
2495        else:
2496            raise FyppFatalError('evaluator_factory has incorrect signature')
2497        self._encoding = options.encoding
2498        if options.modules:
2499            self._import_modules(options.modules, evaluator, syspath,
2500                                 options.moduledirs)
2501        if options.defines:
2502            self._apply_definitions(options.defines, evaluator)
2503        if inspect.signature(parser_factory) == inspect.signature(Parser):
2504            parser = parser_factory(includedirs=options.includes,
2505                                    encoding=self._encoding)
2506        else:
2507            raise FyppFatalError('parser_factory has incorrect signature')
2508        if inspect.signature(builder_factory) == inspect.signature(Builder):
2509            builder = builder_factory()
2510        else:
2511            raise FyppFatalError('builder_factory has incorrect signature')
2512
2513        fixed_format = options.fixed_format
2514        linefolding = not options.no_folding
2515        if linefolding:
2516            folding = 'brute' if fixed_format else options.folding_mode
2517            linelength = 72 if fixed_format else options.line_length
2518            indentation = 5 if fixed_format else options.indentation
2519            prefix = '&'
2520            suffix = '' if fixed_format else '&'
2521            linefolder = FortranLineFolder(linelength, indentation, folding,
2522                                           prefix, suffix)
2523        else:
2524            linefolder = DummyLineFolder()
2525        linenums = options.line_numbering
2526        contlinenums = (options.line_numbering_mode != 'nocontlines')
2527        self._create_parent_folder = options.create_parent_folder
2528        if inspect.signature(renderer_factory) == inspect.signature(Renderer):
2529            renderer = renderer_factory(
2530                evaluator, linenums=linenums, contlinenums=contlinenums,
2531                linenumformat=options.line_marker_format, linefolder=linefolder)
2532        else:
2533            raise FyppFatalError('renderer_factory has incorrect signature')
2534        self._preprocessor = Processor(parser, builder, renderer)
2535
2536
2537    def process_file(self, infile, outfile=None):
2538        '''Processes input file and writes result to output file.
2539
2540        Args:
2541            infile (str): Name of the file to read and process. If its value is
2542                '-', input is read from stdin.
2543            outfile (str, optional): Name of the file to write the result to.
2544                If its value is '-', result is written to stdout. If not
2545                present, result will be returned as string.
2546            env (dict, optional): Additional definitions for the evaluator.
2547
2548        Returns:
2549            str: Result of processed input, if no outfile was specified.
2550        '''
2551        infile = STDIN if infile == '-' else infile
2552        output = self._preprocessor.process_file(infile)
2553        if outfile is None:
2554            return output
2555        if outfile == '-':
2556            outfile = sys.stdout
2557        else:
2558            outfile = _open_output_file(outfile, self._encoding,
2559                                        self._create_parent_folder)
2560        outfile.write(output)
2561        if outfile != sys.stdout:
2562            outfile.close()
2563        return None
2564
2565
2566    def process_text(self, txt):
2567        '''Processes a string.
2568
2569        Args:
2570            txt (str): String to process.
2571            env (dict, optional): Additional definitions for the evaluator.
2572
2573        Returns:
2574            str: Processed content.
2575        '''
2576        return self._preprocessor.process_text(txt)
2577
2578
2579    @staticmethod
2580    def _apply_definitions(defines, evaluator):
2581        for define in defines:
2582            words = define.split('=', 2)
2583            name = words[0]
2584            value = None
2585            if len(words) > 1:
2586                try:
2587                    value = evaluator.evaluate(words[1])
2588                except Exception as exc:
2589                    msg = "exception at evaluating '{0}' in definition for " \
2590                          "'{1}'".format(words[1], name)
2591                    raise FyppFatalError(msg) from exc
2592            evaluator.define(name, value)
2593
2594
2595    def _import_modules(self, modules, evaluator, syspath, moduledirs):
2596        lookuppath = []
2597        if moduledirs is not None:
2598            lookuppath += [os.path.abspath(moddir) for moddir in moduledirs]
2599        lookuppath.append(os.path.abspath('.'))
2600        lookuppath += syspath
2601        self._adjust_syspath(lookuppath)
2602        for module in modules:
2603            evaluator.import_module(module)
2604        self._adjust_syspath(syspath)
2605
2606
2607    @staticmethod
2608    def _get_syspath_without_scriptdir():
2609        '''Remove the folder of the fypp binary from the search path'''
2610        syspath = list(sys.path)
2611        scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))
2612        if os.path.abspath(syspath[0]) == scriptdir:
2613            del syspath[0]
2614        return syspath
2615
2616
2617    @staticmethod
2618    def _adjust_syspath(syspath):
2619        sys.path = syspath
2620
2621
2622class FyppOptions(optparse.Values):
2623
2624    '''Container for Fypp options with default values.
2625
2626    Attributes:
2627        defines (list of str): List of variable definitions in the form of
2628            'VARNAME=VALUE'. Default: []
2629        includes (list of str): List of paths to search when looking for include
2630            files. Default: []
2631        line_numbering (bool): Whether line numbering directives should appear
2632            in the output. Default: False
2633        line_numbering_mode (str): Line numbering mode 'full' or 'nocontlines'.
2634            Default: 'full'.
2635        line_marker_format (str): Line marker format. Currently 'std',
2636            'cpp' and 'gfortran5' are supported, where 'std' emits ``#line``
2637            pragmas similar to standard tools, 'cpp' produces line directives as
2638            emitted by GNU cpp, and 'gfortran5' cpp line directives with a
2639            workaround for a bug introduced in GFortran 5. Default: 'cpp'.
2640        line_length (int): Length of output lines. Default: 132.
2641        folding_mode (str): Folding mode 'smart', 'simple' or 'brute'. Default:
2642            'smart'.
2643        no_folding (bool): Whether folding should be suppressed. Default: False.
2644        indentation (int): Indentation in continuation lines. Default: 4.
2645        modules (list of str): Modules to import at initialization. Default: [].
2646        moduledirs (list of str): Module lookup directories for importing user
2647            specified modules. The specified paths are looked up *before* the
2648            standard module locations in sys.path.
2649        fixed_format (bool): Whether input file is in fixed format.
2650            Default: False.
2651        encoding (str): Character encoding for reading/writing files. Allowed
2652            values are Pythons codec identifiers, e.g. 'ascii', 'utf-8', etc.
2653            Default: 'utf-8'. Reading from stdin and writing to stdout is always
2654            encoded according to the current locale and is not affected by this
2655            setting.
2656        create_parent_folder (bool): Whether the parent folder for the output
2657            file should be created if it does not exist. Default: False.
2658    '''
2659
2660    def __init__(self):
2661        optparse.Values.__init__(self)
2662        self.defines = []
2663        self.includes = []
2664        self.line_numbering = False
2665        self.line_numbering_mode = 'full'
2666        self.line_marker_format = 'cpp'
2667        self.line_length = 132
2668        self.folding_mode = 'smart'
2669        self.no_folding = False
2670        self.indentation = 4
2671        self.modules = []
2672        self.moduledirs = []
2673        self.fixed_format = False
2674        self.encoding = 'utf-8'
2675        self.create_parent_folder = False
2676
2677
2678class FortranLineFolder:
2679
2680    '''Implements line folding with Fortran continuation lines.
2681
2682    Args:
2683        maxlen (int, optional): Maximal line length (default: 132).
2684        indent (int, optional): Indentation for continuation lines (default: 4).
2685        method (str, optional): Folding method with following options:
2686
2687            * ``brute``: folding with maximal length of continuation lines,
2688            * ``simple``: indents with respect of indentation of first line,
2689            * ``smart``: like ``simple``, but tries to fold at whitespaces.
2690
2691        prefix (str, optional): String to use at the beginning of a continuation
2692            line (default: '&').
2693        suffix (str, optional): String to use at the end of the line preceding
2694            a continuation line (default: '&')
2695    '''
2696
2697    def __init__(self, maxlen=132, indent=4, method='smart', prefix='&',
2698                 suffix='&'):
2699        # Line length should be long enough that contintuation lines can host at
2700        # east one character apart of indentation and two continuation signs
2701        minmaxlen = indent + len(prefix) + len(suffix) + 1
2702        if maxlen < minmaxlen:
2703            msg = 'Maximal line length less than {0} when using an indentation'\
2704                  ' of {1}'.format(minmaxlen, indent)
2705            raise FyppFatalError(msg)
2706        self._maxlen = maxlen
2707        self._indent = indent
2708        self._prefix = ' ' * self._indent + prefix
2709        self._suffix = suffix
2710        if method not in ['brute', 'smart', 'simple']:
2711            raise FyppFatalError('invalid folding type')
2712        if method == 'brute':
2713            self._inherit_indent = False
2714            self._fold_position_finder = self._get_maximal_fold_pos
2715        elif method == 'simple':
2716            self._inherit_indent = True
2717            self._fold_position_finder = self._get_maximal_fold_pos
2718        elif method == 'smart':
2719            self._inherit_indent = True
2720            self._fold_position_finder = self._get_smart_fold_pos
2721
2722
2723    def __call__(self, line):
2724        '''Folds a line.
2725
2726        Can be directly called to return the list of folded lines::
2727
2728            linefolder = FortranLineFolder(maxlen=10)
2729            linefolder('  print *, "some Fortran line"')
2730
2731        Args:
2732            line (str): Line to fold.
2733
2734        Returns:
2735            list of str: Components of folded line. They should be
2736                assembled via ``\\n.join()`` to obtain the string
2737                representation.
2738        '''
2739        if self._maxlen < 0 or len(line) <= self._maxlen:
2740            return [line]
2741        if self._inherit_indent:
2742            indent = len(line) - len(line.lstrip())
2743            prefix = ' ' * indent + self._prefix
2744        else:
2745            indent = 0
2746            prefix = self._prefix
2747        suffix = self._suffix
2748        return self._split_line(line, self._maxlen, prefix, suffix,
2749                                self._fold_position_finder)
2750
2751
2752    @staticmethod
2753    def _split_line(line, maxlen, prefix, suffix, fold_position_finder):
2754        # length of continuation lines with 1 or two continuation chars.
2755        maxlen1 = maxlen - len(prefix)
2756        maxlen2 = maxlen1 - len(suffix)
2757        start = 0
2758        end = fold_position_finder(line, start, maxlen - len(suffix))
2759        result = [line[start:end] + suffix]
2760        while end < len(line) - maxlen1:
2761            start = end
2762            end = fold_position_finder(line, start, start + maxlen2)
2763            result.append(prefix + line[start:end] + suffix)
2764        result.append(prefix + line[end:])
2765        return result
2766
2767
2768    @staticmethod
2769    def _get_maximal_fold_pos(_, __, end):
2770        return end
2771
2772
2773    @staticmethod
2774    def _get_smart_fold_pos(line, start, end):
2775        linelen = end - start
2776        ispace = line.rfind(' ', start, end)
2777        # The space we waste for smart folding should be max. 1/3rd of the line
2778        if ispace != -1 and ispace >= start + (2 * linelen) // 3:
2779            return ispace
2780        return end
2781
2782
2783class DummyLineFolder:
2784
2785    '''Implements a dummy line folder returning the line unaltered.'''
2786
2787    def __call__(self, line):
2788        '''Returns the entire line without any folding.
2789
2790        Returns:
2791            list of str: Components of folded line. They should be
2792                assembled via ``\\n.join()`` to obtain the string
2793                representation.
2794        '''
2795        return [line]
2796
2797
2798def get_option_parser():
2799    '''Returns an option parser for the Fypp command line tool.
2800
2801    Returns:
2802        OptionParser: Parser which can create an optparse.Values object with
2803            Fypp settings based on command line arguments.
2804    '''
2805    defs = FyppOptions()
2806    fypp_name = 'fypp'
2807    fypp_desc = 'Preprocesses source code with Fypp directives. The input is '\
2808                'read from INFILE (default: \'-\', stdin) and written to '\
2809                'OUTFILE (default: \'-\', stdout).'
2810    fypp_version = fypp_name + ' ' + VERSION
2811    usage = '%prog [options] [INFILE] [OUTFILE]'
2812    parser = optparse.OptionParser(prog=fypp_name, description=fypp_desc,
2813                                   version=fypp_version, usage=usage)
2814
2815    msg = 'define variable, value is interpreted as ' \
2816          'Python expression (e.g \'-DDEBUG=1\' sets DEBUG to the ' \
2817          'integer 1) or set to None if omitted'
2818    parser.add_option('-D', '--define', action='append', dest='defines',
2819                      metavar='VAR[=VALUE]', default=defs.defines, help=msg)
2820
2821    msg = 'add directory to the search paths for include files'
2822    parser.add_option('-I', '--include', action='append', dest='includes',
2823                      metavar='INCDIR', default=defs.includes, help=msg)
2824
2825    msg = 'import a python module at startup (import only trustworthy modules '\
2826          'as they have access to an **unrestricted** Python environment!)'
2827    parser.add_option('-m', '--module', action='append', dest='modules',
2828                      metavar='MOD', default=defs.modules, help=msg)
2829
2830    msg = 'directory to be searched for user imported modules before '\
2831          'looking up standard locations in sys.path'
2832    parser.add_option('-M', '--module-dir', action='append',
2833                      dest='moduledirs', metavar='MODDIR',
2834                      default=defs.moduledirs, help=msg)
2835
2836    msg = 'emit line numbering markers'
2837    parser.add_option('-n', '--line-numbering', action='store_true',
2838                      dest='line_numbering', default=defs.line_numbering,
2839                      help=msg)
2840
2841    msg = 'line numbering mode, \'full\' (default): line numbering '\
2842          'markers generated whenever source and output lines are out '\
2843          'of sync, \'nocontlines\': line numbering markers omitted '\
2844          'for continuation lines'
2845    parser.add_option('-N', '--line-numbering-mode', metavar='MODE',
2846                      choices=['full', 'nocontlines'],
2847                      default=defs.line_numbering_mode,
2848                      dest='line_numbering_mode', help=msg)
2849
2850    msg = 'line numbering marker format,  currently \'std\', \'cpp\' and '\
2851          '\'gfortran5\' are supported, where \'std\' emits #line pragmas '\
2852          'similar to standard tools, \'cpp\' produces line directives as '\
2853          'emitted by GNU cpp, and \'gfortran5\' cpp line directives with a '\
2854          'workaround for a bug introduced in GFortran 5. Default: \'cpp\'.'
2855    parser.add_option('--line-marker-format', metavar='FMT',
2856                      choices=['cpp', 'gfortran5', 'std'],
2857                      dest='line_marker_format',
2858                      default=defs.line_marker_format, help=msg)
2859
2860    msg = 'maximal line length (default: 132), lines modified by the '\
2861          'preprocessor are folded if becoming longer'
2862    parser.add_option('-l', '--line-length', type=int, metavar='LEN',
2863                      dest='line_length', default=defs.line_length, help=msg)
2864
2865    msg = 'line folding mode, \'smart\' (default): indentation context '\
2866          'and whitespace aware, \'simple\': indentation context aware, '\
2867          '\'brute\': mechnical folding'
2868    parser.add_option('-f', '--folding-mode', metavar='MODE',
2869                      choices=['smart', 'simple', 'brute'], dest='folding_mode',
2870                      default=defs.folding_mode, help=msg)
2871
2872    msg = 'suppress line folding'
2873    parser.add_option('-F', '--no-folding', action='store_true',
2874                      dest='no_folding', default=defs.no_folding, help=msg)
2875
2876    msg = 'indentation to use for continuation lines (default 4)'
2877    parser.add_option('--indentation', type=int, metavar='IND',
2878                      dest='indentation', default=defs.indentation, help=msg)
2879
2880    msg = 'produce fixed format output (any settings for options '\
2881          '--line-length, --folding-method and --indentation are ignored)'
2882    parser.add_option('--fixed-format', action='store_true',
2883                      dest='fixed_format', default=defs.fixed_format, help=msg)
2884
2885    msg = 'character encoding for reading/writing files. Default: \'utf-8\'. '\
2886          'Note: reading from stdin and writing to stdout is encoded '\
2887          'according to the current locale and is not affected by this setting.'
2888    parser.add_option('--encoding', metavar='ENC', default=defs.encoding,
2889                      help=msg)
2890
2891    msg = 'create parent folders of the output file if they do not exist'
2892    parser.add_option('-p', '--create-parents', action='store_true',
2893                      dest='create_parent_folder',
2894                      default=defs.create_parent_folder, help=msg)
2895
2896    return parser
2897
2898
2899def run_fypp():
2900    '''Run the Fypp command line tool.'''
2901    options = FyppOptions()
2902    optparser = get_option_parser()
2903    opts, leftover = optparser.parse_args(values=options)
2904    infile = leftover[0] if len(leftover) > 0 else '-'
2905    outfile = leftover[1] if len(leftover) > 1 else '-'
2906    try:
2907        tool = Fypp(opts)
2908        tool.process_file(infile, outfile)
2909    except FyppStopRequest as exc:
2910        sys.stderr.write(_formatted_exception(exc))
2911        sys.exit(USER_ERROR_EXIT_CODE)
2912    except FyppFatalError as exc:
2913        sys.stderr.write(_formatted_exception(exc))
2914        sys.exit(ERROR_EXIT_CODE)
2915
2916
2917def linenumdir_cpp(linenr, fname, flag=None):
2918    """Returns a GNU cpp style line directive.
2919
2920    Args:
2921        linenr (int): Line nr (starting with zero).
2922        fname (str): File name.
2923        flag (str): Optional flag to print after the directive
2924
2925    Returns:
2926        Line number directive as string.
2927    """
2928    if flag is None:
2929        return '# {0} "{1}"\n'.format(linenr + 1, fname)
2930    return '# {0} "{1}" {2}\n'.format(linenr + 1, fname, flag)
2931
2932
2933def linenumdir_std(linenr, fname, flag=None):
2934    """Returns standard #line pragma styled line directive.
2935
2936    Args:
2937        linenr (int): Line nr (starting with zero).
2938        fname (str): File name.
2939        flag (str): Optional flag to print after the directive. Note, this
2940            option is only there to be API compatible with linenumdir_cpp(),
2941            but is ignored otherwise, since #line pragmas do not allow for
2942            extra file opening/closing flags.
2943
2944    Returns:
2945        Line number directive as string.
2946    """
2947    return "#line {0} \"{1}\"\n".format(linenr + 1, fname)
2948
2949
2950def _shiftinds(inds, shift):
2951    return [ind + shift for ind in inds]
2952
2953
2954def _open_input_file(inpfile, encoding=None):
2955    try:
2956        inpfp = io.open(inpfile, 'r', encoding=encoding)
2957    except IOError as exc:
2958        msg = "Failed to open file '{0}' for read".format(inpfile)
2959        raise FyppFatalError(msg) from exc
2960    return inpfp
2961
2962
2963def _open_output_file(outfile, encoding=None, create_parents=False):
2964    if create_parents:
2965        parentdir = os.path.abspath(os.path.dirname(outfile))
2966        if not os.path.exists(parentdir):
2967            try:
2968                os.makedirs(parentdir)
2969            except OSError as exc:
2970                if exc.errno != errno.EEXIST:
2971                    msg = "Folder '{0}' can not be created"\
2972                        .format(parentdir)
2973                    raise FyppFatalError(msg) from exc
2974    try:
2975        outfp = io.open(outfile, 'w', encoding=encoding)
2976    except IOError as exc:
2977        msg = "Failed to open file '{0}' for write".format(outfile)
2978        raise FyppFatalError(msg) from exc
2979    return outfp
2980
2981
2982# Signature objects are available from Python 3.3 (and deprecated from 3.5)
2983def _get_callable_argspec(func):
2984    sig = inspect.signature(func)
2985    args = []
2986    defaults = {}
2987    varpos = None
2988    varkw = None
2989    for param in sig.parameters.values():
2990        if param.kind == param.POSITIONAL_OR_KEYWORD:
2991            args.append(param.name)
2992            if param.default != param.empty:
2993                defaults[param.name] = param.default
2994        elif param.kind == param.VAR_POSITIONAL:
2995            varpos = param.name
2996        elif param.kind == param.VAR_KEYWORD:
2997            varkw = param.name
2998        else:
2999            msg = "argument '{0}' has invalid argument type".format(param.name)
3000            raise FyppFatalError(msg)
3001    return args, defaults, varpos, varkw
3002
3003
3004
3005def _blank_match(match):
3006    size = match.end() - match.start()
3007    return " " * size
3008
3009
3010def _argsplit_fortran(argtxt):
3011    txt = _INLINE_EVAL_REGION_REGEXP.sub(_blank_match, argtxt)
3012    splitpos = [-1]
3013    quote = None
3014    closing_brace_stack = []
3015    closing_brace = None
3016    for ind, char in enumerate(txt):
3017        if quote:
3018            if char == quote:
3019                quote = None
3020            continue
3021        if char in _QUOTES_FORTRAN:
3022            quote = char
3023            continue
3024        if char in _OPENING_BRACKETS_FORTRAN:
3025            closing_brace_stack.append(closing_brace)
3026            ind = _OPENING_BRACKETS_FORTRAN.index(char)
3027            closing_brace = _CLOSING_BRACKETS_FORTRAN[ind]
3028            continue
3029        if char in _CLOSING_BRACKETS_FORTRAN:
3030            if char == closing_brace:
3031                closing_brace = closing_brace_stack.pop(-1)
3032                continue
3033            else:
3034                msg = "unexpected closing delimiter '{0}' in expression '{1}' "\
3035                      "at position {2}".format(char, argtxt, ind + 1)
3036                raise FyppFatalError(msg)
3037        if not closing_brace and char == _ARGUMENT_SPLIT_CHAR_FORTRAN:
3038            splitpos.append(ind)
3039    if quote or closing_brace:
3040        msg = "open quotes or brackets in expression '{0}'".format(argtxt)
3041        raise FyppFatalError(msg)
3042    splitpos.append(len(txt))
3043    fragments = [argtxt[start + 1 : end]
3044                 for start, end in zip(splitpos, splitpos[1:])]
3045    return fragments
3046
3047
3048def _formatted_exception(exc):
3049    error_header_formstr = '{file}:{line}: '
3050    error_body_formstr = 'error: {errormsg} [{errorclass}]'
3051    if not isinstance(exc, FyppError):
3052        return error_body_formstr.format(
3053            errormsg=str(exc), errorclass=exc.__class__.__name__)
3054    out = []
3055    if exc.fname is not None:
3056        if exc.span[1] > exc.span[0] + 1:
3057            line = '{0}-{1}'.format(exc.span[0] + 1, exc.span[1])
3058        else:
3059            line = '{0}'.format(exc.span[0] + 1)
3060        out.append(error_header_formstr.format(file=exc.fname, line=line))
3061    out.append(error_body_formstr.format(errormsg=exc.msg,
3062                                         errorclass=exc.__class__.__name__))
3063    if exc.__cause__ is not None:
3064        out.append('\n' + _formatted_exception(exc.__cause__))
3065    out.append('\n')
3066    return ''.join(out)
3067
3068
3069if __name__ == '__main__':
3070    run_fypp()
3071