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