cp2k/tools/precommit/fprettify/__init__.py

2128 lines
84 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
###############################################################################
# This file is part of fprettify.
# Copyright (C) 2016-2019 Patrick Seewald, CP2K developers group
#
# fprettify is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# fprettify is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with fprettify. If not, see <http://www.gnu.org/licenses/>.
###############################################################################
"""
Impose white space conventions and indentation based on scopes / subunits
normalization of white spaces supported for following operators:
- relational operators:
.EQ. .NE. .LT. .LE. .GT. .GE.
== /= < <= > >=
- logical operators:
.AND. .OR. .EQV. .NEQV.
.NOT.
- bracket delimiters
- commas and semicolons:
- arithmetic operators:
* / ** + -
- other operators:
% - (sign) = (function argument)
= (assignment) => (pointer assignment)
supported criteria for alignment / indentation:
Fortran lines:
- if, else, endif
- do, enddo
- select case, case, end select
- select rank, rank, end select
- subroutine, end subroutine
- function, end function
- module, end module
- program, end program
- interface, end interface
- type, end type
Actual lines (parts of Fortran lines separated by linebreaks):
- bracket delimiters (.), (/./), and [.]
- assignments by value = and pointer =>.
LIMITATIONS
- assumes that all subunits are explicitly ended within same file,
no treatment of #include statements
- can not deal with f77 constructs (files are ignored)
FIXME's
- internal errors should not happen
- wrap regular expression parser. This allows to extend parser by constructs
that are not regular expressions (and support e.g. forall construct).
- strip whitespaces once and for all and then assume no trailing / leading
whitespaces
- open files only when needed
"""
import re
import sys
import logging
import os
import io
sys.stdin = io.TextIOWrapper(
sys.stdin.detach(), encoding='UTF-8', line_buffering=True)
sys.stdout = io.TextIOWrapper(
sys.stdout.detach(), encoding='UTF-8', line_buffering=True)
from .fparse_utils import (VAR_DECL_RE, OMP_COND_RE,
InputStream, CharFilter,
FprettifyException, FprettifyParseException, FprettifyInternalException,
CPP_RE, NOTFORTRAN_LINE_RE, NOTFORTRAN_FYPP_LINE_RE, FYPP_LINE_RE, RE_FLAGS,
STR_OPEN_RE, parser_re, FYPP_WITHOUT_PREPRO_RE)
# recognize fortran files by extension
FORTRAN_EXTENSIONS = [".f", ".for", ".ftn",
".f90", ".f95", ".f03", ".fpp"]
FORTRAN_EXTENSIONS += [_.upper() for _ in FORTRAN_EXTENSIONS]
# constants, mostly regular expressions:
FORMATTER_ERROR_MESSAGE = (" Wrong usage of formatting-specific directives"
" '&', '!&', '!&<' or '!&>'.")
LINESPLIT_MESSAGE = ("auto indentation failed due to chars limit, "
"line should be split")
EOL_STR = r"\s*;?\s*$" # end of fortran line
EOL_SC = r"\s*;\s*$" # whether line is ended with semicolon
SOL_STR = r"^\s*" # start of fortran line
STATEMENT_LABEL_RE = re.compile(r"^\s*(\d+)", RE_FLAGS)
# regular expressions for parsing statements that start, continue or end a
# subunit:
IF_RE = re.compile(
SOL_STR + r"(\w+\s*:)?\s*IF\s*\(.*\)\s*THEN" + EOL_STR, RE_FLAGS)
ELSE_RE = re.compile(
SOL_STR + r"ELSE(\s*IF\s*\(.*\)\s*THEN)?" + EOL_STR, RE_FLAGS)
ENDIF_RE = re.compile(SOL_STR + r"END\s*IF(\s+\w+)?" + EOL_STR, RE_FLAGS)
DO_RE = re.compile(SOL_STR + r"(\w+\s*:)?\s*DO(" + EOL_STR + r"|\s+\w)", RE_FLAGS)
ENDDO_RE = re.compile(SOL_STR + r"END\s*DO(\s+\w+)?" + EOL_STR, RE_FLAGS)
SELCASE_RE = re.compile(
SOL_STR + r"SELECT\s*(CASE|RANK|TYPE)\s*\(.*\)" + EOL_STR, RE_FLAGS)
CASE_RE = re.compile(
SOL_STR + r"((CASE|RANK|TYPE\s+IS|CLASS\s+IS)\s*(\(.*\)|DEFAULT)|CLASS\s+DEFAULT)" + EOL_STR, RE_FLAGS)
ENDSEL_RE = re.compile(SOL_STR + r"END\s*SELECT" + EOL_STR, RE_FLAGS)
ASSOCIATE_RE = re.compile(SOL_STR + r"ASSOCIATE\s*\(.*\)" + EOL_STR, RE_FLAGS)
ENDASSOCIATE_RE = re.compile(SOL_STR + r"END\s*ASSOCIATE" + EOL_STR, RE_FLAGS)
BLK_RE = re.compile(SOL_STR + r"(\w+\s*:)?\s*BLOCK" + EOL_STR, RE_FLAGS)
ENDBLK_RE = re.compile(SOL_STR + r"END\s*BLOCK(\s+\w+)?" + EOL_STR, RE_FLAGS)
SUBR_RE = re.compile(
r"^([^\"']* )?SUBROUTINE\s+\w+\s*(\(.*\))?" + EOL_STR, RE_FLAGS)
ENDSUBR_RE = re.compile(
SOL_STR + r"END\s*SUBROUTINE(\s+\w+)?" + EOL_STR, RE_FLAGS)
FCT_RE = re.compile(
r"^([^\"']* )?FUNCTION\s+\w+\s*(\(.*\))?(\s*RESULT\s*\(\w+\))?" + EOL_STR,
RE_FLAGS)
ENDFCT_RE = re.compile(
SOL_STR + r"END\s*FUNCTION(\s+\w+)?" + EOL_STR, RE_FLAGS)
MOD_RE = re.compile(SOL_STR + r"MODULE\s+\w+" + EOL_STR, RE_FLAGS)
ENDMOD_RE = re.compile(SOL_STR + r"END\s*MODULE(\s+\w+)?" + EOL_STR, RE_FLAGS)
SMOD_RE = re.compile(SOL_STR + r"SUBMODULE\s*\(\w+\)\s+\w+" + EOL_STR, RE_FLAGS)
ENDSMOD_RE = re.compile(SOL_STR + r"END\s*SUBMODULE(\s+\w+)?" + EOL_STR, RE_FLAGS)
TYPE_RE = re.compile(
SOL_STR +
r"TYPE(\s*,\s*(BIND\s*\(\s*C\s*\)|EXTENDS\s*\(.*\)|ABSTRACT|PUBLIC|PRIVATE))*(\s*,\s*)?(\s*::\s*|\s+)\w+" + EOL_STR,
RE_FLAGS)
ENDTYPE_RE = re.compile(SOL_STR + r"END\s*TYPE(\s+\w+)?" + EOL_STR, RE_FLAGS)
PROG_RE = re.compile(SOL_STR + r"PROGRAM\s+\w+" + EOL_STR, RE_FLAGS)
ENDPROG_RE = re.compile(
SOL_STR + r"END\s*PROGRAM(\s+\w+)?" + EOL_STR, RE_FLAGS)
INTERFACE_RE = re.compile(
r"^([^\"']* )?INTERFACE(\s+\w+|\s+(OPERATOR|ASSIGNMENT)\s*\(.*\))?" + EOL_STR, RE_FLAGS)
ENDINTERFACE_RE = re.compile(
SOL_STR + r"END\s*INTERFACE(\s+\w+|\s+(OPERATOR|ASSIGNMENT)\s*\(.*\))?" + EOL_STR, RE_FLAGS)
CONTAINS_RE = re.compile(SOL_STR + r"CONTAINS" + EOL_STR, RE_FLAGS)
ENUM_RE = re.compile(
SOL_STR + r"ENUM(\s*,\s*(BIND\s*\(\s*C\s*\)))?((\s*::\s*|\s+)\w+)?" + EOL_STR,
RE_FLAGS)
ENDENUM_RE = re.compile(SOL_STR + r"END\s*ENUM(\s+\w+)?" + EOL_STR, RE_FLAGS)
ENDANY_RE = re.compile(SOL_STR + r"END" + EOL_STR, RE_FLAGS)
# Regular expressions for where and forall block constructs
FORALL_RE = re.compile(SOL_STR + r"(\w+\s*:)?\s*FORALL\s*\(.*\)" + EOL_STR, RE_FLAGS)
ENDFORALL_RE = re.compile(SOL_STR + r"END\s*FORALL(\s+\w+)?" + EOL_STR, RE_FLAGS)
WHERE_RE = re.compile(SOL_STR + r"(\w+\s*:)?\s*WHERE\s*\(.*\)" + EOL_STR, RE_FLAGS)
ELSEWHERE_RE = re.compile(SOL_STR + r"ELSE\s*WHERE(\(.*\))?(\s*\w+)?" + EOL_STR, RE_FLAGS)
ENDWHERE_RE = re.compile(SOL_STR + r"END\s*WHERE(\s+\w+)?" + EOL_STR, RE_FLAGS)
# Regular expressions for preprocessor directives
FYPP_DEF_RE = re.compile(SOL_STR + r"#:DEF\s+", RE_FLAGS)
FYPP_ENDDEF_RE = re.compile(SOL_STR + r"#:ENDDEF", RE_FLAGS)
FYPP_IF_RE = re.compile(SOL_STR + r"#:IF\s+", RE_FLAGS)
FYPP_ELIF_ELSE_RE = re.compile(SOL_STR + r"#:(ELIF\s+|ELSE)", RE_FLAGS)
FYPP_ENDIF_RE = re.compile(SOL_STR + r"#:ENDIF", RE_FLAGS)
FYPP_FOR_RE = re.compile(SOL_STR + r"#:FOR\s+", RE_FLAGS)
FYPP_ENDFOR_RE = re.compile(SOL_STR + r"#:ENDFOR", RE_FLAGS)
FYPP_BLOCK_RE = re.compile(SOL_STR + r"#:BLOCK\s+", RE_FLAGS)
FYPP_ENDBLOCK_RE = re.compile(SOL_STR + r"#:ENDBLOCK", RE_FLAGS)
FYPP_CALL_RE = re.compile(SOL_STR + r"#:CALL\s+", RE_FLAGS)
FYPP_ENDCALL_RE = re.compile(SOL_STR + r"#:ENDCALL", RE_FLAGS)
FYPP_MUTE_RE = re.compile(SOL_STR + r"#:MUTE", RE_FLAGS)
FYPP_ENDMUTE_RE = re.compile(SOL_STR + r"#:ENDMUTE", RE_FLAGS)
PRIVATE_RE = re.compile(SOL_STR + r"PRIVATE\s*::", RE_FLAGS)
PUBLIC_RE = re.compile(SOL_STR + r"PUBLIC\s*::", RE_FLAGS)
END_RE = re.compile(SOL_STR + r"(END)\s*(IF|DO|SELECT|ASSOCIATE|BLOCK|SUBROUTINE|FUNCTION|MODULE|SUBMODULE|TYPE|PROGRAM|INTERFACE|ENUM|WHERE|FORALL)", RE_FLAGS)
# intrinsic statements with parenthesis notation that are not functions
INTR_STMTS_PAR = (r"(ALLOCATE|DEALLOCATE|"
r"OPEN|CLOSE|READ|WRITE|"
r"FLUSH|ENDFILE|REWIND|BACKSPACE|INQUIRE|"
r"FORALL|WHERE|ASSOCIATE|NULLIFY)")
# regular expressions for parsing linebreaks
LINEBREAK_STR = r"(&)[\s]*(?:!.*)?$"
# regular expressions for parsing operators
# Note: +/- in real literals and sign operator is ignored
PLUSMINUS_RE = re.compile(r"(?<=[\w\)\]])\s*(\+|-)\s*", RE_FLAGS)
# Note: ** or // (or any multiples of * or /) are ignored
# we also ignore any * or / before a :: because we may be seeing 'real*8'
MULTDIV_RE = re.compile(
r"(?<=[\w\)\]])\s*((?<!\*)\*(?!\*)|(?<!/)/(?!/))(?=[\s\w\(])(?!.*::)", RE_FLAGS)
REL_OP_RE = re.compile(
r"(?<!\()\s*(\.(?:EQ|NE|LT|LE|GT|GE)\.|(?:==|\/=|<(?!=)|<=|(?<!=)>(?!=)|>=))\s*(?!\))",
RE_FLAGS)
LOG_OP_RE = re.compile(r"\s*(\.(?:AND|OR|EQV|NEQV)\.)\s*", RE_FLAGS)
PRINT_RE = re.compile(r"(?:(?<=\bPRINT)|(?<=\bREAD))\s*(\*,?)\s*", RE_FLAGS)
# regular expressions for parsing delimiters
DEL_OPEN_STR = r"(\(\/?|\[)"
DEL_OPEN_RE = re.compile(r"^" + DEL_OPEN_STR, RE_FLAGS)
DEL_CLOSE_STR = r"(\/?\)|\])"
DEL_CLOSE_RE = re.compile(r"^" + DEL_CLOSE_STR, RE_FLAGS)
# empty line regex
EMPTY_RE = re.compile(SOL_STR + r"$", RE_FLAGS)
PREPRO_NEW_SCOPE = [parser_re(FYPP_DEF_RE), parser_re(FYPP_IF_RE), parser_re(FYPP_FOR_RE),
parser_re(FYPP_BLOCK_RE), parser_re(FYPP_CALL_RE), parser_re(FYPP_MUTE_RE)]
PREPRO_CONTINUE_SCOPE = [None, parser_re(FYPP_ELIF_ELSE_RE), None, None, None, None]
PREPRO_END_SCOPE = [parser_re(FYPP_ENDDEF_RE), parser_re(FYPP_ENDIF_RE), parser_re(FYPP_ENDFOR_RE),
parser_re(FYPP_ENDBLOCK_RE), parser_re(FYPP_ENDCALL_RE),
parser_re(FYPP_ENDMUTE_RE)]
class plusminus_parser(parser_re):
"""parser for +/- in addition
"""
def __init__(self, regex):
self._re = regex
self._re_excl = re.compile(r"\b(\d+\.?\d*|\d*\.?\d+)[de]" + EOL_STR, RE_FLAGS)
def split(self, line):
partsplit = self._re.split(line)
partsplit_out = []
# exclude splits due to '+/-' in real literals
for n, part in enumerate(partsplit):
if re.search(r"^(\+|-)$", part):
if self._re_excl.search(partsplit[n-1]):
if n==1: partsplit_out = [partsplit[n-1]]
if n + 1 >= len(partsplit) or not partsplit_out:
raise FprettifyParseException("non-standard expression involving + or -",'',0)
partsplit_out[-1] += part + partsplit[n+1]
else:
if n==1: partsplit_out = [partsplit[n-1]]
if n + 1 >= len(partsplit):
raise FprettifyParseException("non-standard expression involving + or -",'',0)
partsplit_out += [part, partsplit[n+1]]
if not partsplit_out: partsplit_out = partsplit
return partsplit_out
# two-sided operators
LR_OPS_RE = [REL_OP_RE, LOG_OP_RE, plusminus_parser(PLUSMINUS_RE), MULTDIV_RE, PRINT_RE]
USE_RE = re.compile(
SOL_STR + "USE(\s+|(,.+?)?::\s*)\w+?((,.+?=>.+?)+|,\s*only\s*:.+?)?$" + EOL_STR, RE_FLAGS)
# markups to deactivate formatter
NO_ALIGN_RE = re.compile(SOL_STR + r"&\s*[^\s*]+")
class where_parser(parser_re):
"""parser for where / forall construct
"""
def search(self, line):
match = self._re.search(line)
if match:
level = 0
for pos, char in CharFilter(line):
[what_del_open, what_del_close] = get_curr_delim(line, pos)
if what_del_open:
if what_del_open.group() == r'(': level += 1
if what_del_close and what_del_close.group() == r')':
if level == 1:
if EMPTY_RE.search(line[pos+1:]):
return True
else:
return False
else:
level += -1
return False
forall_parser = where_parser
def build_scope_parser(fypp=True, mod=True):
parser = {}
parser['new'] = \
[parser_re(IF_RE), parser_re(DO_RE), parser_re(SELCASE_RE), parser_re(SUBR_RE),
parser_re(FCT_RE),
parser_re(INTERFACE_RE), parser_re(TYPE_RE), parser_re(ENUM_RE), parser_re(ASSOCIATE_RE),
None, parser_re(BLK_RE), where_parser(WHERE_RE), forall_parser(FORALL_RE)]
parser['continue'] = \
[parser_re(ELSE_RE), None, parser_re(CASE_RE), parser_re(CONTAINS_RE),
parser_re(CONTAINS_RE),
None, parser_re(CONTAINS_RE), None, None,
None, None, parser_re(ELSEWHERE_RE), None]
parser['end'] = \
[parser_re(ENDIF_RE), parser_re(ENDDO_RE), parser_re(ENDSEL_RE), parser_re(ENDSUBR_RE),
parser_re(ENDFCT_RE),
parser_re(ENDINTERFACE_RE), parser_re(ENDTYPE_RE), parser_re(ENDENUM_RE), parser_re(ENDASSOCIATE_RE),
parser_re(ENDANY_RE,spec=False), parser_re(ENDBLK_RE), parser_re(ENDWHERE_RE), parser_re(ENDFORALL_RE)]
if mod:
parser['new'].extend([parser_re(MOD_RE), parser_re(SMOD_RE), parser_re(PROG_RE)])
parser['continue'].extend([parser_re(CONTAINS_RE), parser_re(CONTAINS_RE), parser_re(CONTAINS_RE)])
parser['end'].extend([parser_re(ENDMOD_RE), parser_re(ENDSMOD_RE), parser_re(ENDPROG_RE)])
if fypp:
parser['new'].extend(PREPRO_NEW_SCOPE)
parser['continue'].extend(PREPRO_CONTINUE_SCOPE)
parser['end'].extend(PREPRO_END_SCOPE)
return parser
# match namelist names
NML_RE = re.compile(r"(/\w+/)", RE_FLAGS)
# find namelists and data statements
NML_STMT_RE = re.compile(SOL_STR + r"NAMELIST.*/.*/", RE_FLAGS)
DATA_STMT_RE = re.compile(SOL_STR + r"DATA\s+\w", RE_FLAGS)
## Regexp for f90 keywords'
F90_KEYWORDS_RE = re.compile(r"\b(" + "|".join((
"allocatable", "allocate", "assign", "assignment", "backspace",
"block", "call", "case", "character", "close", "common", "complex",
"contains", "continue", "cycle", "data", "deallocate",
"dimension", "do", "double", "else", "elseif", "elsewhere", "end",
"enddo", "endfile", "endif", "entry", "equivalence", "exit",
"external", "forall", "format", "function", "goto", "if",
"implicit", "include", "inquire", "integer", "intent",
"interface", "intrinsic", "logical", "module", "namelist", "none",
"nullify", "only", "open", "operator", "optional", "parameter",
"pause", "pointer", "precision", "print", "private", "procedure",
"program", "public", "read", "real", "recursive", "result", "return",
"rewind", "save", "select", "sequence", "stop", "subroutine",
"target", "then", "type", "use", "where", "while", "write",
## F95 keywords.
"elemental", "pure",
## F2003
"abstract", "associate", "asynchronous", "bind", "class",
"deferred", "enum", "enumerator", "extends", "extends_type_of",
"final", "generic", "import", "non_intrinsic", "non_overridable",
"nopass", "pass", "protected", "same_type_as", "value", "volatile",
## F2008.
"contiguous", "submodule", "concurrent", "codimension",
"sync all", "sync memory", "critical", "image_index",
)) + r")\b", RE_FLAGS)
## Regexp whose first part matches F90 intrinsic procedures.
## Add a parenthesis to avoid catching non-procedures.
F90_PROCEDURES_RE = re.compile(r"\b(" + "|".join((
"abs", "achar", "acos", "adjustl", "adjustr", "aimag", "aint",
"all", "allocated", "anint", "any", "asin", "associated",
"atan", "atan2", "bit_size", "btest", "ceiling", "char", "cmplx",
"conjg", "cos", "cosh", "count", "cshift", "date_and_time", "dble",
"digits", "dim", "dot_product", "dprod", "eoshift", "epsilon",
"exp", "exponent", "floor", "fraction", "huge", "iachar", "iand",
"ibclr", "ibits", "ibset", "ichar", "ieor", "index", "int", "ior",
"ishft", "ishftc", "kind", "lbound", "len", "len_trim", "lge", "lgt",
"lle", "llt", "log", "log10", "logical", "matmul", "max",
"maxexponent", "maxloc", "maxval", "merge", "min", "minexponent",
"minloc", "minval", "mod", "modulo", "mvbits", "nearest", "nint",
"not", "pack", "precision", "present", "product", "radix",
## Real is taken out here to avoid highlighting declarations.
"random_number", "random_seed", "range", ## "real"
"repeat", "reshape", "rrspacing", "scale", "scan",
"selected_int_kind", "selected_real_kind", "set_exponent",
"shape", "sign", "sin", "sinh", "size", "spacing", "spread", "sqrt",
"sum", "system_clock", "tan", "tanh", "tiny", "transfer",
"transpose", "trim", "ubound", "unpack", "verify",
## F95 intrinsic functions.
"null", "cpu_time",
## F2003.
"move_alloc", "command_argument_count", "get_command",
"get_command_argument", "get_environment_variable",
"selected_char_kind", "wait", "flush", "new_line",
"extends", "extends_type_of", "same_type_as", "bind",
## F2003 ieee_arithmetic intrinsic module.
"ieee_support_underflow_control", "ieee_get_underflow_mode",
"ieee_set_underflow_mode",
## F2003 iso_c_binding intrinsic module.
"c_loc", "c_funloc", "c_associated", "c_f_pointer",
"c_f_procpointer",
## F2008.
"bge", "bgt", "ble", "blt", "dshiftl", "dshiftr", "leadz", "popcnt",
"poppar", "trailz", "maskl", "maskr", "shifta", "shiftl", "shiftr",
"merge_bits", "iall", "iany", "iparity", "storage_size",
"bessel_j0", "bessel_j1", "bessel_jn",
"bessel_y0", "bessel_y1", "bessel_yn",
"erf", "erfc", "erfc_scaled", "gamma", "hypot", "log_gamma",
"norm2", "parity", "findloc", "is_contiguous",
"sync images", "lock", "unlock", "image_index",
"lcobound", "ucobound", "num_images", "this_image",
## F2008 iso_fortran_env module.
"compiler_options", "compiler_version",
## F2008 iso_c_binding module.
"c_sizeof"
)) + r")\b", RE_FLAGS)
F90_MODULES_RE = re.compile(r"\b(" + "|".join((
## F2003/F2008 module names
"iso_fortran_env",
"iso_c_binding",
"ieee_exceptions",
"ieee_arithmetic",
"ieee_features"
)) + r")\b", RE_FLAGS)
## Regexp matching intrinsic operators
F90_OPERATORS_RE = re.compile(r"(" + "|".join([r"\." + a + r"\." for a in (
"and", "eq", "eqv", "false", "ge", "gt", "le", "lt", "ne",
"neqv", "not", "or", "true"
)]) + r")", RE_FLAGS)
## Regexp for Fortran intrinsic constants
F90_CONSTANTS_RE = re.compile(r"\b(" + "|".join((
## F2003 iso_fortran_env constants.
"input_unit", "output_unit", "error_unit",
"iostat_end", "iostat_eor",
"numeric_storage_size", "character_storage_size",
"file_storage_size",
## F2003 iso_c_binding constants.
"c_int", "c_short", "c_long", "c_long_long", "c_signed_char",
"c_size_t",
"c_int8_t", "c_int16_t", "c_int32_t", "c_int64_t",
"c_int_least8_t", "c_int_least16_t", "c_int_least32_t",
"c_int_least64_t",
"c_int_fast8_t", "c_int_fast16_t", "c_int_fast32_t",
"c_int_fast64_t",
"c_intmax_t", "c_intptr_t",
"c_float", "c_double", "c_long_double",
"c_float_complex", "c_double_complex", "c_long_double_complex",
"c_bool", "c_char",
"c_null_char", "c_alert", "c_backspace", "c_form_feed",
"c_new_line", "c_carriage_return", "c_horizontal_tab",
"c_vertical_tab",
"c_ptr", "c_funptr", "c_null_ptr", "c_null_funptr",
## F2008 iso_fortran_env constants.
"character_kinds", "int8", "int16", "int32", "int64",
"integer_kinds", "iostat_inquire_internal_unit",
"logical_kinds", "real_kinds", "real32", "real64", "real128",
"lock_type", "atomic_int_kind", "atomic_logical_kind",
)) + r")\b", RE_FLAGS)
F90_INT_RE = r"[-+]?[0-9]+"
F90_FLOAT_RE = r"[-+]?([0-9]+\.[0-9]*|\.[0-9]+)"
F90_NUMBER_RE = "(" + F90_INT_RE + "|" + F90_FLOAT_RE + ")"
F90_FLOAT_EXP_RE = F90_NUMBER_RE + r"[eEdD]" + F90_NUMBER_RE
F90_NUMBER_ALL_RE = "(" + F90_NUMBER_RE + "|" + F90_FLOAT_EXP_RE + ")"
F90_NUMBER_ALL_REC = re.compile(F90_NUMBER_ALL_RE, RE_FLAGS)
## F90_CONSTANTS_TYPES_RE = re.compile(r"\b" + F90_NUMBER_ALL_RE + "_(" + "|".join([a + r"\b" for a in (
F90_CONSTANTS_TYPES_RE = re.compile(
r"(" + F90_NUMBER_ALL_RE + ")*_(" + "|".join((
## F2003 iso_fortran_env constants.
## F2003 iso_c_binding constants.
"c_int", "c_short", "c_long", "c_long_long", "c_signed_char",
"c_size_t",
"c_int8_t", "c_int16_t", "c_int32_t", "c_int64_t",
"c_int_least8_t", "c_int_least16_t", "c_int_least32_t",
"c_int_least64_t",
"c_int_fast8_t", "c_int_fast16_t", "c_int_fast32_t",
"c_int_fast64_t",
"c_intmax_t", "c_intptr_t",
"c_float", "c_double", "c_long_double",
"c_float_complex", "c_double_complex", "c_long_double_complex",
"c_bool", "c_char",
## F2008 iso_fortran_env constants.
"character_kinds", "int8", "int16", "int32", "int64",
"integer_kinds",
"logical_kinds", "real_kinds", "real32", "real64", "real128",
"lock_type", "atomic_int_kind", "atomic_logical_kind",
)) + r")\b", RE_FLAGS)
class F90Indenter(object):
"""
Parses encapsulation of subunits / scopes line by line
and updates the indentation.
"""
def __init__(self, scope_parser, first_indent, rel_indent, filename):
# scopes / subunits:
self._scope_storage = []
# indents for all fortran lines:
self._indent_storage = []
# indents of actual lines of current fortran line
self._line_indents = []
self._parser = scope_parser
self._filename = filename
self._aligner = F90Aligner(filename)
# no lines have been processed yet:
self._initial = True
# implicit scopes: we define implicit scopes, as many as match
# first_indent and rel_indent. This allows for, e.g., a properly
# indented "END FUNCTION" without matching "FUNCTION" statement:
if rel_indent > 0:
for n_impl in range(first_indent % rel_indent, first_indent + 1, rel_indent):
self._indent_storage += [n_impl]
if not self._indent_storage:
self._indent_storage = [0]
def process_lines_of_fline(self, f_line, lines, rel_ind, rel_ind_con,
line_nr, indent_fypp=True, manual_lines_indent=None):
"""
Process all lines that belong to a Fortran line `f_line`.
Impose a relative indent of `rel_ind` for current Fortran line,
and `rel_ind_con` for line continuation.
By default line continuations are auto-aligned by F90Aligner
:param f_line: fortran line
:param lines: actual lines belonging to f_line
:param rel_ind: relative scope indent size for this line
:rel_ind_con: relative continuation indent size for this line
:line_nr: line number
:indent_fypp: whether or not to include fypp preprocessor lines
:manual_lines_indent: don't use F90Aligner but manually impose
indents for continuations
"""
if (self._initial and
(PROG_RE.match(f_line) or MOD_RE.match(f_line))):
self._indent_storage[-1] = 0
self._line_indents = [0] * len(lines)
br_indent_list = [0] * len(lines)
# local variables to avoid self hassle:
line_indents = self._line_indents
scopes = self._scope_storage
indents = self._indent_storage
filename = self._filename
# check statements that start new scope
is_new = False
valid_new = False
f_filter = CharFilter(f_line, filter_fypp=not indent_fypp)
f_line_filtered = f_filter.filter_all()
for new_n, newre in enumerate(self._parser['new']):
if newre and newre.search(f_line_filtered) and \
not self._parser['end'][new_n].search(f_line_filtered):
what_new = new_n
is_new = True
valid_new = True
scopes.append(what_new)
log_message("{}: {}".format(what_new, f_line),
"debug", filename, line_nr)
# check statements that continue scope
is_con = False
valid_con = False
for con_n, conre in enumerate(self._parser['continue']):
if conre and conre.search(f_line_filtered):
what_con = con_n
is_con = True
log_message("{}: {}".format(
what_con, f_line), "debug", filename, line_nr)
if len(scopes) > 0:
what = scopes[-1]
if what == what_con:
valid_con = True
# check statements that end scope
is_end = False
valid_end = False
for end_n, endre in enumerate(self._parser['end']):
if endre and endre.search(f_line_filtered):
what_end = end_n
is_end = True
log_message("{}: {}".format(
what_end, f_line), "debug", filename, line_nr)
if len(scopes) > 0:
what = scopes.pop()
if what == what_end or not self._parser['end'][what_end].spec:
valid_end = True
log_message("{}: {}".format(
what_end, f_line), "debug", filename, line_nr)
else:
valid_end = True
# fypp preprocessor scopes may be within continuation lines
if indent_fypp and len(lines) > 1 and not FYPP_LINE_RE.search(f_line_filtered):
for new_n, newre in enumerate(PREPRO_NEW_SCOPE):
for l in lines:
if(newre and newre.search(l)):
is_new = True
valid_new = True
scopes.append(new_n)
for end_n, endre in enumerate(PREPRO_END_SCOPE):
for l in lines:
if(endre and endre.search(l)):
is_end = True
valid_end = True
if len(scopes) > 0:
what = scopes.pop()
# deal with line breaks
if not manual_lines_indent:
self._aligner.process_lines_of_fline(
f_line, lines, rel_ind_con, line_nr)
br_indent_list = self._aligner.get_lines_indent()
else:
br_indent_list = manual_lines_indent
for pos in range(0, len(lines) - 1):
line_indents[pos + 1] = br_indent_list[pos + 1]
if is_new and not is_end:
if not valid_new:
log_message('invalid scope opening statement',
"info", filename, line_nr)
line_indents = [ind + indents[-1] for ind in line_indents]
indents.append(rel_ind + indents[-1])
elif (not is_new) and (is_con or is_end):
valid = valid_con if is_con else valid_end
if not valid:
line_indents = [ind + indents[-1] for ind in line_indents]
log_message('invalid scope closing statement',
"info", filename, line_nr)
else:
if len(indents) > 1 or self._initial:
line_indents = [ind + indents[-2 + self._initial]
for ind in line_indents]
if is_end and valid:
if len(indents) > 1:
indents.pop()
else:
indents[-1] = 0
else:
line_indents = [ind + indents[-1] for ind in line_indents]
# we have processed first line:
self._initial = False
# reassigning self.* to the updated variables
self._line_indents = line_indents
self._scope_storage = scopes
self._indent_storage = indents
def get_fline_indent(self):
"""after processing, retrieve the indentation of the full Fortran line."""
return self._indent_storage[-1]
def get_lines_indent(self):
"""after processing, retrieve the indents of all line parts."""
return self._line_indents
class F90Aligner(object):
"""
Alignment of continuations of a broken line,
based on the following heuristics:
if line break in brackets
We are parsing the level of nesting
and align to most inner bracket delimiter.
else if line is an assignment
alignment to '=' or '=>'.
note: assignment operator recognized as any '=' that is not
part of another operator and that is not enclosed in bracket
else if line is a declaration
alignment to '::'
else
default indent
"""
def __init__(self, filename):
self._filename = filename
self.__init_line(0)
def __init_line(self, line_nr):
"""initialization before processing new line"""
self._line_nr = line_nr
self._line_indents = [0]
self._level = 0
self._br_indent_list = [0]
def process_lines_of_fline(self, f_line, lines, rel_ind, line_nr):
"""
process all lines that belong to a Fortran line `f_line`,
`rel_ind` is the relative indentation size.
"""
self.__init_line(line_nr)
is_decl = VAR_DECL_RE.search(f_line) or PUBLIC_RE.search(f_line) or PRIVATE_RE.match(f_line)
is_use = USE_RE.search(f_line)
for pos, line in enumerate(lines):
self.__align_line_continuations(
line, is_decl, is_use, rel_ind, self._line_nr + pos)
if pos + 1 < len(lines):
self._line_indents.append(self._br_indent_list[-1])
if len(self._br_indent_list) > 2 or self._level:
log_message('unpaired bracket delimiters',
"info", self._filename, self._line_nr)
def get_lines_indent(self):
"""after processing, retrieve the indents of all line parts."""
return self._line_indents
def __align_line_continuations(self, line, is_decl, is_use, indent_size, line_nr):
"""align continuation lines."""
indent_list = self._br_indent_list
level = self._level
filename = self._filename
pos_eq = 0
pos_ldelim = []
pos_rdelim = []
ldelim = []
rdelim = []
# find delimiters that are not ended on this line.
# find proper alignment to most inner delimiter
# or alignment to assignment operator
rel_ind = indent_list[-1] # indentation of prev. line
end_of_delim = -1
for pos, char in CharFilter(line):
what_del_open = None
what_del_close = None
if pos > end_of_delim:
[what_del_open, what_del_close] = get_curr_delim(line, pos)
if what_del_open:
what_del_open = what_del_open.group()
end_of_delim = pos + len(what_del_open) - 1
level += 1
indent_list.append(pos + len(what_del_open) + rel_ind)
pos_ldelim.append(pos)
ldelim.append(what_del_open)
if what_del_close:
what_del_close = what_del_close.group()
end_of_delim = pos + len(what_del_close) - 1
if level > 0:
level += -1
indent_list.pop()
else:
log_message('unpaired bracket delimiters',
"info", filename, line_nr)
if pos_ldelim:
pos_ldelim.pop()
what_del_open = ldelim.pop()
valid = False
if what_del_open == r"(":
valid = what_del_close == r")"
if what_del_open == r"(/":
valid = what_del_close == r"/)"
if what_del_open == r"[":
valid = what_del_close == r"]"
if not valid:
log_message('unpaired bracket delimiters',
"info", filename, line_nr)
else:
pos_rdelim.append(pos)
rdelim.append(what_del_close)
if char == ',' and not level and pos_eq > 0:
# a top level comma removes previous alignment position.
# (see issue #11)
pos_eq = 0
indent_list.pop()
if not level and not is_decl and char == '=' and not REL_OP_RE.search(
line[max(0, pos - 1):min(pos + 2, len(line))]):
# should only have one assignment per line!
if pos_eq > 0:
raise FprettifyInternalException(
"found more than one assignment in the same Fortran line", filename, line_nr)
is_pointer = line[pos + 1] == '>'
pos_eq = pos + 1
# don't align if assignment operator directly before
# line break
if not re.search(r"=>?\s*" + LINEBREAK_STR, line,
RE_FLAGS):
indent_list.append(
pos_eq + 1 + is_pointer + indent_list[-1])
elif is_decl and line[pos:pos + 2] == '::' and not re.search(r"::\s*" + LINEBREAK_STR, line, RE_FLAGS):
indent_list.append(pos + 3 + indent_list[-1])
elif is_use and line[pos] == ':' and not re.search(r":\s*" + LINEBREAK_STR, line, RE_FLAGS):
indent_list.append(pos + 2 + indent_list[-1])
# Don't align if delimiter opening directly before line break
if level and re.search(DEL_OPEN_STR + r"\s*" + LINEBREAK_STR, line,
RE_FLAGS):
if len(indent_list) > 1:
indent_list[-1] = indent_list[-2]
else:
indent_list[-1] = 0
if not indent_list[-1]:
indent_list[-1] = indent_size
self._level = level
def inspect_ffile_format(infile, indent_size, strict_indent, indent_fypp=False, orig_filename=None):
"""
Determine indentation by inspecting original Fortran file.
This is mainly for finding aligned blocks of DO/IF statements.
Also check if it has f77 constructs.
:param infile: open file
:param indent_size: the default indent size
:orig_filename: filename used for messages
:returns: [ target indent sizes for each line,
indent of first line (offset) ]
"""
if not orig_filename:
orig_filename = infile.name
num_labels = False
indents = []
stream = InputStream(infile, filter_fypp=not indent_fypp, orig_filename=orig_filename)
prev_offset = 0
first_indent = -1
while 1:
f_line, _, lines = stream.next_fortran_line()
if not lines:
break
f_line, lines, label = preprocess_labels(f_line, lines)
offset = len(lines[0]) - len(lines[0].lstrip(' '))
if f_line.strip() and first_indent == -1:
first_indent = offset
indents.append(offset - prev_offset)
# don't impose indentation for blocked do/if constructs:
if (IF_RE.search(f_line) or DO_RE.search(f_line)):
if (prev_offset != offset or strict_indent):
indents[-1] = indent_size
else:
indents[-1] = indent_size
prev_offset = offset
return indents, first_indent
def replace_relational_single_fline(f_line, cstyle):
"""
format a single Fortran line - replaces scalar relational
operators in logical expressions to either Fortran or C-style.
.lt. <--> <
.le. <--> <=
.gt. <--> >
.ge. <--> >=
.eq. <--> ==
.ne. <--> /=
"""
new_line = f_line
# only act on lines that do contain a relation
if REL_OP_RE.search(f_line):
# check that relation is not inside quotes, a string, or commented
# (think of underlining a heading with === or things like markup being printed which we do not replace)
pos_prev = -1
pos = -1
line_parts = ['']
for pos, char in CharFilter(f_line):
if pos > pos_prev + 1: # skipped string
line_parts.append(f_line[pos_prev + 1:pos].strip()) # append string
line_parts.append('')
line_parts[-1] += char
pos_prev = pos
if pos + 1 < len(f_line):
line_parts.append(f_line[pos + 1:])
for pos, part in enumerate(line_parts):
# exclude comments, strings:
if not STR_OPEN_RE.match(part):
# also exclude / if we see a namelist and data statement
if cstyle:
part = re.sub(r"\.LT\.", "< ", part, flags=RE_FLAGS)
part = re.sub(r"\.LE\.", "<= ", part, flags=RE_FLAGS)
part = re.sub(r"\.GT\.", "> ", part, flags=RE_FLAGS)
part = re.sub(r"\.GE\.", ">= ", part, flags=RE_FLAGS)
part = re.sub(r"\.EQ\.", "== ", part, flags=RE_FLAGS)
part = re.sub(r"\.NE\.", "/= ", part, flags=RE_FLAGS)
else:
part = re.sub(r"<=", ".le.", part, flags=RE_FLAGS)
part = re.sub(r"<", ".lt.", part, flags=RE_FLAGS)
part = re.sub(r">=", ".ge.", part, flags=RE_FLAGS)
part = re.sub(r">", ".gt.", part, flags=RE_FLAGS)
part = re.sub(r"==", ".eq.", part, flags=RE_FLAGS)
part = re.sub(r"\/=", ".ne.", part, flags=RE_FLAGS)
line_parts[pos] = part
new_line = ''.join(line_parts)
return new_line
def replace_keywords_single_fline(f_line, case_dict):
"""
format a single Fortran line - change case of keywords
"""
new_line = f_line
# Collect words list
pos_prev = -1
pos = -1
line_parts = ['']
for pos, char in CharFilter(f_line):
if pos > pos_prev + 1: # skipped string
line_parts.append(f_line[pos_prev + 1:pos].strip()) # append string
line_parts.append('')
line_parts[-1] += char
pos_prev = pos
if pos + 1 < len(f_line):
line_parts.append(f_line[pos + 1:])
line_parts = [[a] if STR_OPEN_RE.match(a) else re.split(F90_OPERATORS_RE,a)
for a in line_parts] # problem, split "."
line_parts = [b for a in line_parts for b in a]
## line_parts = [[a] if STR_OPEN_RE.match(a) else re.split('(\W)',a)
## for a in line_parts] # problem, split "."
line_parts = [[a] if STR_OPEN_RE.match(a)
else re.split('([^a-zA-Z0-9_.])',a)
for a in line_parts]
line_parts = [b for a in line_parts for b in a]
swapcase = lambda s, a: s if a==0 else (s.lower() if a==1 else s.upper())
nbparts = len(line_parts)
for pos, part in enumerate(line_parts):
# exclude comments, strings:
if part.strip() and not STR_OPEN_RE.match(part):
if F90_KEYWORDS_RE.match(part):
part = swapcase(part, case_dict['keywords'])
elif F90_MODULES_RE.match(part):
part = swapcase(part, case_dict['procedures'])
elif F90_PROCEDURES_RE.match(part):
ok = False
for pos2 in range(pos+1, nbparts):
part2 = line_parts[pos2]
if part2.strip() and not (part2 == '\n' or STR_OPEN_RE.match(part2)):
ok = (part2 == '(')
break
if ok:
part = swapcase(part, case_dict['procedures'])
elif F90_OPERATORS_RE.match(part):
part = swapcase(part, case_dict['operators'])
elif F90_CONSTANTS_RE.match(part):
part = swapcase(part, case_dict['constants'])
elif F90_CONSTANTS_TYPES_RE.match(part):
part = swapcase(part, case_dict['constants'])
elif F90_NUMBER_ALL_REC.match(part):
part = swapcase(part, case_dict['constants'])
line_parts[pos] = part
new_line = ''.join(line_parts)
return new_line
def format_single_fline(f_line, whitespace, whitespace_dict, linebreak_pos,
ampersand_sep, scope_parser, format_decl, filename, line_nr,
auto_format=True):
"""
format a single Fortran line - imposes white space formatting
and inserts linebreaks.
Takes a logical Fortran line `f_line` as input as well as the positions
of the linebreaks (`linebreak_pos`), and the number of
separating whitespace characters before ampersand (`ampersand_sep`).
`filename` and `line_nr` just for error messages.
The higher `whitespace`, the more white space characters inserted -
whitespace = 0, 1, 2, 3 are currently supported.
whitespace formatting can additionally controlled more fine-grained
via a dictionary of bools (whitespace_dict)
auto formatting can be turned off by setting `auto_format` to False.
"""
# define whether to put whitespaces around operators:
mapping = {
'comma': 0, # 0: comma, semicolon
'assignments': 1, # 1: assignment operators
'relational': 2, # 2: relational operators
'logical': 3, # 3: logical operators
'plusminus': 4, # 4: arithm. operators plus and minus
'multdiv': 5, # 5: arithm. operators multiply and divide
'print': 6, # 6: print / read statements
'type': 7, # 7: select type components
'intrinsics': 8, # 8: intrinsics
'decl': 9 # 9: declarations
}
if whitespace == 0:
spacey = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
elif whitespace == 1:
spacey = [1, 1, 1, 1, 0, 0, 1, 0, 1, 1]
elif whitespace == 2:
spacey = [1, 1, 1, 1, 1, 0, 1, 0, 1, 1]
elif whitespace == 3:
spacey = [1, 1, 1, 1, 1, 1, 1, 0, 1, 1]
elif whitespace == 4:
spacey = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
else:
raise NotImplementedError("unknown value for whitespace")
if whitespace_dict:
# iterate over dictionary and override settings for 'spacey'
for key, value in mapping.items():
if whitespace_dict[key] == True:
spacey[value] = 1
elif whitespace_dict[key] == False:
spacey[value] = 0
line = f_line
line_orig = line
if auto_format:
line = rm_extra_whitespace(line, format_decl)
line = add_whitespace_charwise(line, spacey, scope_parser, format_decl, filename, line_nr)
line = add_whitespace_context(line, spacey)
lines_out = split_reformatted_line(
line_orig, linebreak_pos, ampersand_sep, line, filename, line_nr)
return lines_out
def rm_extra_whitespace(line, format_decl):
"""rm all unneeded whitespace chars, except for declarations"""
line_ftd = ''
pos_prev = -1
pos = -1
for pos, char in CharFilter(line):
if format_decl:
is_decl = False
else:
is_decl = line[pos:].lstrip().startswith('::') or line[
:pos].rstrip().endswith('::')
if pos > pos_prev + 1: # skipped string
line_ftd = line_ftd + line[pos_prev + 1:pos]
if char == ' ':
# remove double spaces:
if line_ftd and (re.search(r'[\w]', line_ftd[-1]) or is_decl):
line_ftd = line_ftd + char
else:
if (line_ftd and line_ftd[-1] == ' ' and
(not re.search(r'[\w]', char) and not is_decl)):
line_ftd = line_ftd[:-1] # remove spaces except between words
line_ftd = line_ftd + char
pos_prev = pos
line_ftd = line_ftd + line[pos+1:]
return line_ftd
def add_whitespace_charwise(line, spacey, scope_parser, format_decl, filename, line_nr):
"""add whitespace character wise (no need for context aware parsing)"""
line_ftd = line
pos_eq = []
end_of_delim = -1
level = 0
for pos, char in CharFilter(line):
# offset w.r.t. unformatted line
offset = len(line_ftd) - len(line)
# format delimiters
what_del_open = None
what_del_close = None
if pos > end_of_delim:
[what_del_open, what_del_close] = get_curr_delim(line, pos)
if what_del_open or what_del_close:
sep1 = 0
sep2 = 0
if what_del_open:
delim = what_del_open.group()
else:
delim = what_del_close.group()
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + len(delim) + offset:]
# format opening delimiters
if what_del_open:
level += 1 # new scope
# add separating whitespace before opening delimiter
# with some exceptions:
# FIXME: duplication of regex, better to include them into
# INTR_STMTS_PAR
if ((not re.search((r"(" + DEL_OPEN_STR +
r"|[\w\*/=\+\-:])\s*$"),
line[:pos], RE_FLAGS) and
not EMPTY_RE.search(line[:pos])) or
re.search(SOL_STR + r"(\w+\s*:)?(ELSE)?\s*IF\s*$",
line[:pos], RE_FLAGS) or
re.search(SOL_STR + r"(\w+\s*:)?\s*DO\s+WHILE\s*$",
line[:pos], RE_FLAGS) or
re.search(SOL_STR + r"(SELECT)?\s*CASE\s*$",
line[:pos], RE_FLAGS) or
re.search(SOL_STR + r"(SELECT)?\s*RANK\s*$",
line[:pos], RE_FLAGS) or
re.search(SOL_STR + r"SELECT\s*TYPE\s*$",
line[:pos], RE_FLAGS) or
re.search(SOL_STR + r"CLASS\s*DEFAULT\s*$",
line[:pos], RE_FLAGS) or
re.search(SOL_STR + r"(TYPE|CLASS)\s+IS\s*$",
line[:pos], RE_FLAGS) or
re.search(r"(?<!%)\b" + INTR_STMTS_PAR + r"\s*$",
line[:pos], RE_FLAGS)):
sep1 = 1 * spacey[8]
# format closing delimiters
else:
if level > 0:
level += -1 # close scope
else:
log_message('unpaired bracket delimiters',
"info", filename, line_nr)
# add separating whitespace after closing delimiter
# with some exceptions:
if not re.search(r"^\s*(" + DEL_CLOSE_STR + r"|[,%:/\*])",
line[pos + 1:], RE_FLAGS):
sep2 = 1
elif re.search(r"^\s*::", line[pos + 1:], RE_FLAGS):
sep2 = len(rhs) - len(rhs.lstrip(' ')) if not format_decl else 1
# where delimiter token ends
end_of_delim = pos + len(delim) - 1
line_ftd = lhs.rstrip(' ') + ' ' * sep1 + \
delim + ' ' * sep2 + rhs.lstrip(' ')
# format commas and semicolons
if char in [',', ';']:
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + offset:]
line_ftd = lhs.rstrip(' ') + char + ' ' * \
spacey[0] + rhs.lstrip(' ')
line_ftd = line_ftd.rstrip(' ')
# format type selector %
if char == "%":
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + offset:]
line_ftd = lhs.rstrip(' ') \
+ ' ' * spacey[7] \
+ char \
+ ' ' * spacey[7] \
+ rhs.lstrip(' ')
line_ftd = line_ftd.rstrip(' ')
# format '::'
if format_decl and line[pos:pos+2] == "::":
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 2 + offset:]
line_ftd = lhs.rstrip(' ') \
+ ' ' * spacey[9] \
+ '::' + ' ' * spacey[9] \
+ rhs.lstrip(' ')
line_ftd = line_ftd.rstrip(' ')
# format .NOT.
if re.search(r"^\.NOT\.", line[pos:pos + 5], RE_FLAGS):
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 5 + offset:]
line_ftd = lhs.rstrip(
' ') + line[pos:pos + 5] + ' ' * spacey[3] + rhs.lstrip(' ')
# strip whitespaces from '=' and prepare assignment operator
# formatting:
if char == '=' and not REL_OP_RE.search(line[pos - 1:pos + 2]):
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + offset:]
line_ftd = lhs.rstrip(' ') + '=' + rhs.lstrip(' ')
is_pointer = line[pos + 1] == '>'
if (not level) or is_pointer: # remember position of assignment operator
pos_eq.append(len(lhs.rstrip(' ')))
line = line_ftd
for pos in pos_eq:
offset = len(line_ftd) - len(line)
is_pointer = line[pos + 1] == '>'
lhs = line_ftd[:pos + offset]
rhs = line_ftd[pos + 1 + is_pointer + offset:]
if is_pointer:
assign_op = '=>' # pointer assignment
else:
assign_op = '=' # assignment
line_ftd = (lhs.rstrip(' ') +
' ' * spacey[1] + assign_op +
' ' * spacey[1] + rhs.lstrip(' '))
# offset w.r.t. unformatted line
is_end = False
if END_RE.search(line_ftd):
for endre in scope_parser['end']:
if endre and endre.search(line_ftd):
is_end = True
if is_end:
line_ftd = END_RE.sub(r'\1' + ' '*spacey[8] + r'\2', line_ftd)
if level != 0:
log_message('unpaired bracket delimiters', "info", filename, line_nr)
return line_ftd
def add_whitespace_context(line, spacey):
"""
for context aware whitespace formatting we extract line parts that are
not comments or strings in order to be able to apply a context aware regex.
"""
pos_prev = -1
pos = -1
line_parts = ['']
for pos, char in CharFilter(line):
if pos > pos_prev + 1: # skipped string
line_parts.append(line[pos_prev + 1:pos].strip()) # append string
line_parts.append('')
line_parts[-1] += char
pos_prev = pos
if pos + 1 < len(line):
line_parts.append(line[pos + 1:])
# format namelists with spaces around /
if NML_STMT_RE.match(line):
for pos, part in enumerate(line_parts):
# exclude comments, strings:
if not STR_OPEN_RE.match(part):
partsplit = NML_RE.split(part)
line_parts[pos] = (' '.join(partsplit))
# Two-sided operators
for n_op, lr_re in enumerate(LR_OPS_RE):
for pos, part in enumerate(line_parts):
# exclude comments, strings:
if not STR_OPEN_RE.match(part):
# also exclude / if we see a namelist and data statement
if not ( NML_STMT_RE.match(line) or DATA_STMT_RE.match(line) ):
partsplit = lr_re.split(part)
line_parts[pos] = (' ' * spacey[n_op + 2]).join(partsplit)
line = ''.join(line_parts)
for newre in [IF_RE, DO_RE, BLK_RE]:
if newre.search(line) and re.search(SOL_STR + r"\w+\s*:", line):
line = ': '.join(_.strip() for _ in line.split(':', 1))
# format ':' for labels and use only statements
if USE_RE.search(line):
line = re.sub(r'(only)\s*:\s*', r'\g<1>:' + ' ' *
spacey[0], line, flags=RE_FLAGS)
return line
def split_reformatted_line(line_orig, linebreak_pos_orig, ampersand_sep, line, filename, line_nr):
"""
Infer linebreak positions of formatted line from linebreak positions in
original line and split line.
"""
# shift line break positions from original to reformatted line
pos_new = 0
pos_old = 0
linebreak_pos_orig.sort(reverse=True)
linebreak_pos_ftd = []
while 1:
if pos_new == len(line) or pos_old == len(line_orig):
break
if line[pos_new] != line_orig[pos_old]:
raise FprettifyInternalException(
"failed at finding line break position", filename, line_nr)
if linebreak_pos_orig and pos_old > linebreak_pos_orig[-1]:
linebreak_pos_orig.pop()
linebreak_pos_ftd.append(pos_new)
continue
pos_new += 1
while pos_new < len(line) and line[pos_new] == ' ':
pos_new += 1
pos_old += 1
while pos_old < len(line_orig) and line_orig[pos_old] == ' ':
pos_old += 1
linebreak_pos_ftd.insert(0, 0)
# We split line into parts and we insert ampersands at line end, but not
# for empty lines and comment lines
lines_split = [(line[l:r].rstrip(' ') +
' ' * ampersand_sep[pos] + '&' * min(1, r - l))
for pos, (l, r) in enumerate(zip(linebreak_pos_ftd[0:-1],
linebreak_pos_ftd[1:]))]
lines_split.append(line[linebreak_pos_ftd[-1]:])
return lines_split
def diff(a, b, a_name, b_name):
# type: (str, str, str, str) -> str
"""Return a unified diff string between strings `a` and `b`."""
import difflib
a_lines = [line + "\n" for line in a.splitlines()]
b_lines = [line + "\n" for line in b.splitlines()]
return "".join(
difflib.unified_diff(a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5)
)
def reformat_inplace(filename, stdout=False, diffonly=False, **kwargs): # pragma: no cover
"""reformat a file in place."""
if filename == '-':
infile = io.StringIO()
infile.write(sys.stdin.read())
else:
infile = io.open(filename, 'r', encoding='utf-8')
newfile = io.StringIO()
reformat_ffile(infile, newfile,
orig_filename=filename, **kwargs)
if diffonly:
infile.seek(0)
newfile.seek(0)
diff_contents=diff(infile.read(),newfile.read(),filename,filename)
sys.stdout.write(diff_contents)
else:
if stdout:
sys.stdout.write(newfile.getvalue())
else:
outfile = io.open(filename, 'r', encoding='utf-8')
# write to outfile only if content has changed
import hashlib
hash_new = hashlib.md5()
hash_new.update(newfile.getvalue().encode('utf-8'))
hash_old = hashlib.md5()
hash_old.update(outfile.read().encode('utf-8'))
outfile.close()
if hash_new.digest() != hash_old.digest():
outfile = io.open(filename, 'w', encoding='utf-8')
outfile.write(newfile.getvalue())
def reformat_ffile(infile, outfile, impose_indent=True, indent_size=3, strict_indent=False, impose_whitespace=True,
case_dict={},
impose_replacements=False, cstyle=False, whitespace=2, whitespace_dict={}, llength=132,
strip_comments=False, format_decl=False, orig_filename=None, indent_fypp=True, indent_mod=True):
"""main method to be invoked for formatting a Fortran file."""
# note: whitespace formatting and indentation may require different parsing rules
# (e.g. preprocessor statements may be indented but not whitespace formatted)
# therefore we invoke reformat_ffile independently for:
# 1) whitespace formatting
# 2) indentation
if not orig_filename:
orig_filename = infile.name
# 1) whitespace formatting
oldfile = infile
newfile = infile
if impose_whitespace:
_impose_indent = False
newfile = io.StringIO()
reformat_ffile_combined(oldfile, newfile, _impose_indent, indent_size, strict_indent, impose_whitespace,
case_dict,
impose_replacements, cstyle, whitespace, whitespace_dict, llength,
strip_comments, format_decl, orig_filename, indent_fypp, indent_mod)
oldfile = newfile
# 2) indentation
if impose_indent:
_impose_whitespace = False
_impose_replacements = False
newfile = io.StringIO()
reformat_ffile_combined(oldfile, newfile, impose_indent, indent_size, strict_indent, _impose_whitespace,
case_dict,
_impose_replacements, cstyle, whitespace, whitespace_dict, llength,
strip_comments, format_decl, orig_filename, indent_fypp, indent_mod)
outfile.write(newfile.getvalue())
def reformat_ffile_combined(infile, outfile, impose_indent=True, indent_size=3, strict_indent=False, impose_whitespace=True,
case_dict={},
impose_replacements=False, cstyle=False, whitespace=2, whitespace_dict={}, llength=132,
strip_comments=False, format_decl=False, orig_filename=None, indent_fypp=True, indent_mod=True):
if not orig_filename:
orig_filename = infile.name
if not impose_indent:
indent_fypp = False
scope_parser = build_scope_parser(fypp=indent_fypp, mod=indent_mod)
infile.seek(0)
req_indents, first_indent = inspect_ffile_format(
infile, indent_size, strict_indent, indent_fypp, orig_filename)
infile.seek(0)
# initialization
# special cases for indentation:
# indent_special = 0: parse syntax and impose indent
# indent_special = 1: no indentation
# indent_special = 2: use indent from previous line
# indent_special = 3: take indent from input file (leave as is)
indent_special = 0
if impose_indent:
indenter = F90Indenter(scope_parser, first_indent, indent_size, orig_filename)
else:
indent_special = 3
impose_case = not all(v == 0 for v in case_dict.values())
nfl = 0 # fortran line counter
use_same_line = False
stream = InputStream(infile, not indent_fypp, orig_filename=orig_filename)
skip_blank = False
in_format_off_block = False
while 1:
f_line, comments, lines = stream.next_fortran_line()
if not lines:
break
nfl += 1
orig_lines = lines
if indent_special != 3:
indent = [0] * len(lines)
else:
indent = [len(l) - len((l.lstrip(' ')).lstrip('&')) for l in lines]
comment_lines = format_comments(lines, comments, strip_comments)
auto_align, auto_format, in_format_off_block = parse_fprettify_directives(
lines, comment_lines, in_format_off_block, orig_filename, stream.line_nr)
f_line, lines, is_omp_conditional = preprocess_omp(
f_line, lines)
f_line, lines, label = preprocess_labels(f_line, lines)
lines, do_format, prev_indent, is_blank, is_special = preprocess_line(
f_line, lines, comments, orig_filename, stream.line_nr, indent_fypp)
if is_special[0]:
indent_special = 3
if prev_indent and indent_special == 0:
indent_special = 2
if is_blank and skip_blank:
continue
if (not do_format):
if indent_special == 2:
# inherit indent from previous line
indent[:] = [indenter.get_fline_indent()]*len(indent)
elif indent_special == 0:
indent_special = 1
else:
if not auto_align:
manual_lines_indent = get_manual_alignment(lines)
else:
manual_lines_indent = []
lines, pre_ampersand, ampersand_sep = remove_pre_ampersands(
lines, is_special, orig_filename, stream.line_nr)
linebreak_pos = get_linebreak_pos(lines, filter_fypp=not indent_fypp)
f_line = f_line.strip(' ')
if impose_replacements:
f_line = replace_relational_single_fline(f_line, cstyle)
if impose_case:
f_line = replace_keywords_single_fline(f_line, case_dict)
if impose_whitespace:
lines = format_single_fline(
f_line, whitespace, whitespace_dict, linebreak_pos, ampersand_sep,
scope_parser, format_decl, orig_filename, stream.line_nr, auto_format)
lines = append_comments(lines, comment_lines, is_special)
# target indent for next line
rel_indent = req_indents[nfl] if nfl < len(req_indents) else 0
if indent_special != 3:
indenter.process_lines_of_fline(
f_line, lines, rel_indent, indent_size,
stream.line_nr, indent_fypp, manual_lines_indent)
indent = indenter.get_lines_indent()
lines, indent = prepend_ampersands(lines, indent, pre_ampersand)
if any(is_special):
for pos, line in enumerate(lines):
if is_special[pos]:
indent[pos] = len(line) - len(line.lstrip(' '))
lines[pos] = line.lstrip(' ')
lines = remove_trailing_whitespace(lines)
write_formatted_line(outfile, indent, lines, orig_lines, indent_special, llength,
use_same_line, is_omp_conditional, label, orig_filename, stream.line_nr)
do_indent, use_same_line = pass_defaults_to_next_line(f_line)
if impose_indent:
if do_indent:
indent_special = 0
else:
indent_special = 1
# rm subsequent blank lines
skip_blank = EMPTY_RE.search(
f_line) and not any(comments) and not is_omp_conditional and not label
def format_comments(lines, comments, strip_comments):
comments_ftd = []
for line, comment in zip(lines, comments):
has_comment = bool(comment.strip())
if has_comment:
if strip_comments:
sep = not comment.strip() == line.strip()
else:
line_minus_comment = line.replace(comment,"")
sep = len(line_minus_comment.rstrip('\n')) - len(line_minus_comment.rstrip())
else:
sep = 0
if line.strip(): # empty lines between linebreaks are ignored
comments_ftd.append(' ' * sep + comment.strip())
return comments_ftd
def parse_fprettify_directives(lines, comment_lines, in_format_off_block, filename, line_nr):
"""
parse formatter directives '!&' and line continuations starting with an
ampersand.
"""
auto_align = not any(NO_ALIGN_RE.search(_) for _ in lines)
auto_format = not (in_format_off_block or any(
_.lstrip().startswith('!&') for _ in comment_lines))
if not auto_format:
auto_align = False
if (len(lines)) == 1:
valid_directive = True
if lines[0].strip().startswith('!&<'):
if in_format_off_block:
valid_directive = False
else:
in_format_off_block = True
if lines[0].strip().startswith('!&>'):
if not in_format_off_block:
valid_directive = False
else:
in_format_off_block = False
if not valid_directive:
raise FprettifyParseException(
FORMATTER_ERROR_MESSAGE, filename, line_nr)
return [auto_align, auto_format, in_format_off_block]
def preprocess_omp(f_line, lines):
"""convert omp conditional to normal fortran"""
is_omp_conditional = bool(OMP_COND_RE.search(f_line))
if is_omp_conditional:
f_line = OMP_COND_RE.sub(' ', f_line, count=1)
lines = [OMP_COND_RE.sub(' ', l, count=1) for l in lines]
return [f_line, lines, is_omp_conditional]
def preprocess_labels(f_line, lines):
"""remove statement labels"""
match = STATEMENT_LABEL_RE.search(f_line)
if match:
label = match.group(1)
else:
label = ''
if label:
f_line = STATEMENT_LABEL_RE.sub(len(label)*' ', f_line, count=1)
lines[0] = STATEMENT_LABEL_RE.sub(len(label)*' ', lines[0], count=1)
return [f_line, lines, label]
def preprocess_line(f_line, lines, comments, filename, line_nr, indent_fypp):
"""preprocess lines: identification and formatting of special cases"""
is_blank = False
prev_indent = False
do_format = False
# is_special: special directives that should not be treated as Fortran
# currently supported: fypp preprocessor directives or comments for FORD documentation
is_special = [False]*len(lines)
for pos, line in enumerate(lines):
line_strip = line.lstrip()
if indent_fypp:
is_special[pos] = line_strip.startswith('!!') or \
(FYPP_LINE_RE.search(line_strip) if pos > 0 else False)
else:
is_special[pos] = FYPP_LINE_RE.search(line_strip) or line_strip.startswith('!!')
# if first line is special, all lines should be special
if is_special[0]: is_special = [True]*len(lines)
if EMPTY_RE.search(f_line): # empty lines including comment lines
if any(comments):
if lines[0].startswith(' '):
# indent comment lines only if they were not indented before.
prev_indent = True
else:
is_blank = True
lines = [l.strip(' ') if not is_special[n] else l for n, l in enumerate(lines)]
else:
do_format = True
return [lines, do_format, prev_indent, is_blank, is_special]
def pass_defaults_to_next_line(f_line):
"""defaults to be transferred from f_line to next line"""
if re.search(r";\s*$", f_line, RE_FLAGS):
# if line ended with semicolon, don't indent next line
do_indent = False
use_same_line = True
else:
do_indent = True
use_same_line = False
return [do_indent, use_same_line]
def remove_trailing_whitespace(lines):
"""remove trailing whitespaces from lines"""
lines = [re.sub(r"\s+$", '\n', l, RE_FLAGS)
for l in lines]
return lines
def prepend_ampersands(lines, indent, pre_ampersand):
"""prepend ampersands and correct indent"""
for pos, line in enumerate(lines):
amp_insert = pre_ampersand[pos]
if amp_insert:
indent[pos] += -1
lines[pos] = amp_insert + line.lstrip()
return [lines, indent]
def append_comments(lines, comment_lines, is_special):
"""append comments to lines"""
for pos, (line, comment) in enumerate(zip(lines, comment_lines)):
if pos < len(lines) - 1:
has_nl = True # has next line
if not line.strip() and not is_special[pos]: comment = comment.lstrip()
else:
has_nl = not re.search(EOL_SC, line)
lines[pos] = lines[pos].rstrip(' ') + comment + '\n' * has_nl
return lines
def get_linebreak_pos(lines, filter_fypp=True):
"""extract linebreak positions in Fortran line from lines"""
linebreak_pos = []
if filter_fypp:
notfortran_re = NOTFORTRAN_LINE_RE
else:
notfortran_re = NOTFORTRAN_FYPP_LINE_RE
for line in lines:
found = None
for char_pos, _ in CharFilter(line, filter_strings=False):
if re.match(LINEBREAK_STR, line[char_pos:], RE_FLAGS):
found = char_pos
if found:
linebreak_pos.append(found)
elif notfortran_re.search(line.lstrip(' ')):
linebreak_pos.append(0)
linebreak_pos = [sum(linebreak_pos[0:_ + 1]) -
1 for _ in range(0, len(linebreak_pos))]
return linebreak_pos
def remove_pre_ampersands(lines, is_special, filename, line_nr):
"""
remove and return preceding ampersands ('pre_ampersand'). Also return
number of whitespace characters before ampersand of previous line
('ampersand_sep').
Note: Don't do any whitespace formatting on ampersands if next line starts
with an ampersand but remember the original number of spaces
(ampersand_sep). This "special rule" is necessary since ampersands starting
a line can be used to break literals, so changing the number of whitespaces
before the ampersand ending the previous line may lead to invalid syntax or
may change the number of whitespace characters in a string.
"""
pre_ampersand = []
ampersand_sep = []
for pos, line in enumerate(lines):
match = re.search(SOL_STR + r'(&\s*)', line)
if match:
pre_ampersand.append(match.group(1))
# amount of whitespace before ampersand of previous line:
m = re.search(r'(\s*)&[\s]*(?:!.*)?$', lines[pos - 1])
if not m:
raise FprettifyParseException(
"Bad continuation line format", filename, line_nr)
sep = len(m.group(1))
ampersand_sep.append(sep)
else:
pre_ampersand.append('')
if pos > 0:
# use default 1 whitespace character before ampersand
ampersand_sep.append(1)
lines = [l.strip(' ').strip('&') if not s else l for l, s in zip(lines, is_special)]
return [lines, pre_ampersand, ampersand_sep]
def get_manual_alignment(lines):
"""extract manual indents for line continuations from line"""
manual_lines_indent = [
len(l) - len(l.lstrip(' ').lstrip('&')) for l in lines]
manual_lines_indent = [ind - manual_lines_indent[0]
for ind in manual_lines_indent]
return manual_lines_indent
def write_formatted_line(outfile, indent, lines, orig_lines, indent_special, llength, use_same_line, is_omp_conditional, label, filename, line_nr):
"""Write reformatted line to file"""
for ind, line, orig_line in zip(indent, lines, orig_lines):
# get actual line length excluding comment:
line_length = 0
for line_length, _ in CharFilter(line):
pass
line_length += 1
if indent_special != 1:
ind_use = ind
else:
if use_same_line:
ind_use = 1
else:
ind_use = 0
if CPP_RE.search(line.lstrip()):
ind_use = 0
if label:
label_use = label + ' '
label = '' # no label for continuation lines
else:
label_use = ''
if ind_use + line_length <= (llength+1): # llength (default 132) plus 1 newline char
outfile.write('!$ ' * is_omp_conditional + label_use +
' ' * (ind_use - 3 * is_omp_conditional - len(label_use) +
len(line) - len(line.lstrip(' '))) +
line.lstrip(' '))
elif line_length <= (llength+1):
outfile.write('!$ ' * is_omp_conditional + label_use + ' ' *
((llength+1) - 3 * is_omp_conditional - len(label_use) -
len(line.lstrip(' '))) + line.lstrip(' '))
log_message(LINESPLIT_MESSAGE+" (limit: "+str(llength)+")", "warning",
filename, line_nr)
else:
outfile.write(orig_line)
log_message(LINESPLIT_MESSAGE+" (limit: "+str(llength)+")", "warning",
filename, line_nr)
def get_curr_delim(line, pos):
"""get delimiter token in line starting at pos, if it exists"""
what_del_open = DEL_OPEN_RE.search(line[pos:pos + 2])
what_del_close = DEL_CLOSE_RE.search(line[pos:pos + 2])
return [what_del_open, what_del_close]
def set_fprettify_logger(level):
"""setup custom logger"""
logger = logging.getLogger('fprettify-logger')
logger.setLevel(level)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(level)
formatter = logging.Formatter(
'%(levelname)s: File %(ffilename)s, line %(fline)s\n %(message)s')
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
def log_exception(e, message):
"""log an exception and a message"""
log_message(message, "exception", e.filename, e.line_nr)
def log_message(message, level, filename, line_nr):
"""log a message"""
logger = logging.getLogger('fprettify-logger')
logger_d = {'ffilename': filename, 'fline': line_nr}
logger_to_use = getattr(logger, level)
logger_to_use(message, extra=logger_d)
def run(argv=sys.argv): # pragma: no cover
"""Command line interface"""
try:
import configargparse as argparse
except ImportError:
import argparse
def str2bool(str):
"""helper function to convert strings to bool"""
if str.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif str.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
return None
def get_config_file_list(filename):
"""helper function to create list of config files found in parent directories"""
config_file_list = []
dir = os.path.dirname(filename)
while True:
config_file = os.path.join(dir, '.fprettify.rc')
if os.path.isfile(config_file):
config_file_list.insert(0, config_file)
parent=os.path.dirname(dir)
if parent == dir:
break
dir = parent
return config_file_list
arguments = {'prog': argv[0],
'description': 'Auto-format modern Fortran source files.',
'formatter_class': argparse.ArgumentDefaultsHelpFormatter}
if argparse.__name__ == "configargparse":
arguments['args_for_setting_config_path'] = ['-c', '--config-file']
arguments['description'] = arguments['description'] + " Config files ('.fprettify.rc') in the home (~) directory and any such files located in parent directories of the input file will be used. When the standard input is used, the search is started from the current directory."
def get_arg_parser(args):
"""helper function to create the parser object"""
parser = argparse.ArgumentParser(**args)
parser.add_argument("-i", "--indent", type=int, default=3,
help="relative indentation width")
parser.add_argument("-l", "--line-length", type=int, default=132,
help="column after which a line should end, viz. -ffree-line-length-n for GCC")
parser.add_argument("-w", "--whitespace", type=int,
choices=range(0, 5), default=2, help="Presets for the amount of whitespace - "
" 0: minimal whitespace"
" | 1: operators (except arithmetic), print/read"
" | 2: operators, print/read, plus/minus"
" | 3: operators, print/read, plus/minus, muliply/divide"
" | 4: operators, print/read, plus/minus, muliply/divide, type component selector")
parser.add_argument("--whitespace-comma", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for comma/semicolons")
parser.add_argument("--whitespace-assignment", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for assignments")
parser.add_argument("--whitespace-decl", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for declarations (requires '--enable-decl')")
parser.add_argument("--whitespace-relational", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for relational operators")
parser.add_argument("--whitespace-logical", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for logical operators")
parser.add_argument("--whitespace-plusminus", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for plus/minus arithmetic")
parser.add_argument("--whitespace-multdiv", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for multiply/divide arithmetic")
parser.add_argument("--whitespace-print", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for print/read statements")
parser.add_argument("--whitespace-type", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for select type components")
parser.add_argument("--whitespace-intrinsics", type=str2bool, nargs="?", default="None", const=True,
help="boolean, en-/disable whitespace for intrinsics like if/write/close")
parser.add_argument("--strict-indent", action='store_true', default=False, help="strictly impose indentation even for nested loops")
parser.add_argument("--enable-decl", action="store_true", default=False, help="enable whitespace formatting of declarations ('::' operator).")
parser.add_argument("--disable-indent", action='store_true', default=False, help="don't impose indentation")
parser.add_argument("--disable-whitespace", action='store_true', default=False, help="don't impose whitespace formatting")
parser.add_argument("--enable-replacements", action='store_true', default=False, help="replace relational operators (e.g. '.lt.' <--> '<')")
parser.add_argument("--c-relations", action='store_true', default=False, help="C-style relational operators ('<', '<=', ...)")
parser.add_argument("--case", nargs=4, default=[0,0,0,0], type=int, help="Enable letter case formatting of intrinsics by specifying which of "
"keywords, procedures/modules, operators and constants (in this order) should be lowercased or uppercased - "
" 0: do nothing"
" | 1: lowercase"
" | 2: uppercase")
parser.add_argument("--strip-comments", action='store_true', default=False, help="strip whitespaces before comments")
parser.add_argument('--disable-fypp', action='store_true', default=False,
help="Disables the indentation of fypp preprocessor blocks.")
parser.add_argument('--disable-indent-mod', action='store_true', default=False,
help="Disables the indentation after module / program.")
parser.add_argument("-d","--diff", action='store_true', default=False,
help="Write file differences to stdout instead of formatting inplace")
parser.add_argument("-s", "--stdout", action='store_true', default=False,
help="Write to stdout instead of formatting inplace")
group = parser.add_mutually_exclusive_group()
group.add_argument("-S", "--silent", "--no-report-errors", action='store_true',
default=False, help="Don't write any errors or warnings to stderr")
group.add_argument("-D", "--debug", action='store_true',
default=False, help=argparse.SUPPRESS)
parser.add_argument("path", type=str, nargs='*',
help="Paths to files to be formatted inplace. If no paths are given, stdin (-) is used by default. Path can be a directory if --recursive is used.", default=['-'])
parser.add_argument('-r', '--recursive', action='store_true',
default=False, help="Recursively auto-format all Fortran files in subdirectories of specified path; recognized filename extensions: {}". format(", ".join(FORTRAN_EXTENSIONS)))
parser.add_argument('-e', '--exclude', action='append',
default=[], type=str,
help="File or directory patterns to be excluded when searching for Fortran files to format")
parser.add_argument('-f', '--fortran', type=str, action='append', default=[],
help="Overrides default fortran extensions recognized by --recursive. Repeat this option to specify more than one extension.")
parser.add_argument('--version', action='version',
version='%(prog)s 0.3.7')
return parser
parser = get_arg_parser(arguments)
args = parser.parse_args(argv[1:])
def build_ws_dict(args):
"""helper function to build whitespace dictionary"""
ws_dict = {}
ws_dict['comma'] = args.whitespace_comma
ws_dict['assignments'] = args.whitespace_assignment
ws_dict['decl'] = args.whitespace_decl
ws_dict['relational'] = args.whitespace_relational
ws_dict['logical'] = args.whitespace_logical
ws_dict['plusminus'] = args.whitespace_plusminus
ws_dict['multdiv'] = args.whitespace_multdiv
ws_dict['print'] = args.whitespace_print
ws_dict['type'] = args.whitespace_type
ws_dict['intrinsics'] = args.whitespace_intrinsics
return ws_dict
case_dict = {
'keywords' : args.case[0],
'procedures' : args.case[1],
'operators' : args.case[2],
'constants' : args.case[3]
}
# support legacy input:
if 'stdin' in args.path and not os.path.isfile('stdin'):
args.path = ['-' if _ == 'stdin' else _ for _ in args.path]
for directory in args.path:
if directory == '-':
if args.recursive:
sys.stderr.write("--recursive requires a directory.\n")
sys.exit()
else:
if not os.path.exists(directory):
sys.stderr.write("directory " + directory +
" does not exist!\n")
sys.exit()
if not os.path.isfile(directory) and directory != '-' and not args.recursive:
sys.stderr.write("file " + directory + " does not exist!\n")
sys.exit()
if not args.recursive:
filenames = [directory]
else:
if args.fortran:
ext = args.fortran
else:
ext = FORTRAN_EXTENSIONS
filenames = []
from fnmatch import fnmatch
for dirpath, dirnames, files in os.walk(directory,topdown=True):
# Prune excluded patterns from list of child directories
dirnames[:] = [dirname for dirname in dirnames if not any(
[fnmatch(dirname,exclude_pattern) or fnmatch(os.path.join(dirpath,dirname),exclude_pattern)
for exclude_pattern in args.exclude]
)]
for ffile in [os.path.join(dirpath, f) for f in files
if any(f.endswith(_) for _ in ext)
and not any([
fnmatch(f,exclude_pattern)
for exclude_pattern in args.exclude])]:
filenames.append(ffile)
for filename in filenames:
# reparse arguments using the file's list of config files
filearguments = arguments
if argparse.__name__ == "configargparse":
filearguments['default_config_files'] = ['~/.fprettify.rc'] + get_config_file_list(os.path.abspath(filename) if filename != '-' else os.getcwd())
file_argparser = get_arg_parser(filearguments)
file_args = file_argparser.parse_args(argv[1:])
ws_dict = build_ws_dict(file_args)
stdout = file_args.stdout or directory == '-'
diffonly=file_args.diff
if file_args.debug:
level = logging.DEBUG
elif args.silent:
level = logging.CRITICAL
else:
level = logging.WARNING
set_fprettify_logger(level)
try:
reformat_inplace(filename,
stdout=stdout,
diffonly=diffonly,
impose_indent=not file_args.disable_indent,
indent_size=file_args.indent,
strict_indent=file_args.strict_indent,
impose_whitespace=not file_args.disable_whitespace,
impose_replacements=file_args.enable_replacements,
cstyle=file_args.c_relations,
case_dict=case_dict,
whitespace=file_args.whitespace,
whitespace_dict=ws_dict,
llength=1024 if file_args.line_length == 0 else file_args.line_length,
strip_comments=file_args.strip_comments,
format_decl=file_args.enable_decl,
indent_fypp=not file_args.disable_fypp,
indent_mod=not file_args.disable_indent_mod)
except FprettifyException as e:
log_exception(e, "Fatal error occured")