Source code for sphinxfortran.fortran_autodoc
# -*- coding: utf8 -*-
"""Sphinx extension for autodocumenting fortran codes.
"""
# Copyright or © or Copr. Actimar/IFREMER (2010-2015)
#
# This software is a computer program whose purpose is to provide
# utilities for handling oceanographic and atmospheric data,
# with the ultimate goal of validating the MARS model from IFREMER.
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
#
from sphinx.directives import Directive
from docutils.parsers.rst.directives import unchanged
from docutils import nodes
from docutils.statemachine import string2lines
from sphinx.util.console import bold
from glob import glob
from numpy.f2py.crackfortran import crackfortran, fortrantypes
import re, os, sys
from fortran_domain import FortranDomain
# Fortran parser and formatter
# ----------------------------
[docs]class F90toRst(object):
'''Fortran 90 parser and restructeredtext formatter
:Parameters:
- **ffiles**: Fortran files (glob expression allowed) or dir (or list of)
:Options:
- **ic**: Indentation char.
- **ulc**: Underline char for titles.
- **sst**: Subsection type.
- **vl**: Verbose level (0=quiet).
'''
_re_unended_match = re.compile(r'(.*)&\s*',re.I).match
_re_unstarted_match = re.compile(r'\s*&(.*)',re.I).match
_re_comment_match = re.compile(r'\s*!(.*)',re.I).match
_re_space_prefix_match = re.compile(r'^(\s*).*$',re.I).match
# _re_sub_numvardesc_match = re.compile(r'\$(\d+)',re.I).match
# _re_vardesc_match = re.compile(r'\s*(?P<varname>\w+)[\s,&]*!\s*(?P<vardesc>.+)',re.I).match
# _re_type_var_findall = re.compile(r'(\w+)',re.I).match
_fmt_vardesc = ':%(role)s %(vtype)s %(vname)s%(vdim)s%(vattr)s: %(vdesc)s'
#_fmt_vardesc = '**%(vname)s** %(vdim)s :: %(vtype)s %(vattr)s: %(vdesc)s'
_fmt_vattr = " [%(vattr)s]"
_fmt_fvardesc = '%(vtype)s%(vdim)s %(vattr)s%(vdesc)s'
#_fmt_fvardim = ', **dim%(vdim)s**'
# _re_call_subr_search = re.compile(r'\bcall\s+(\w+)\(',re.I).search
# _re_call_func_findall = re.compile(r'\b(\w+)\(',re.I).search
def __init__(self, ffiles, ic='\t', ulc='-', vl=0, encoding='utf8', sst='rubric'):
# Be sure to have a list
if not isinstance(ffiles, list):
ffiles = [ffiles]
# Read and store them
self.src = {}
self.ffiles = ffiles
for ff in ffiles:
f = open(ff)
self.src[ff] = []
for l in f.readlines():
try:
self.src[ff].append(l[:-1].decode(encoding) )
except:
raise F90toRstException('Encoding error\n file = %s\n line = %s'%(ff,l))
#self.src[ff] = [l.decode(encoding) for l in f.readlines()]
#self.src[ff] = [l.decode(encoding) for l in f.read().split('\n')]
f.close()
# Crack files
global verbose, quiet
import numpy.f2py.crackfortran
self._verbose = numpy.f2py.crackfortran.verbose = verbose = vl
numpy.f2py.crackfortran.quiet = quiet = 1-verbose
self.crack = []
for ff in ffiles: self.crack.extend(crackfortran(ff))
# Build index
self.build_index()
# Scan all files to get description, etc
self.scan()
# Add function 'call from' to index
self.build_callfrom_index()
# Other inits
self.rst = {}
self._ic = ic
self._ulc = ulc
self._sst = sst
# Indexing ---
[docs] def build_index(self):
"""Register modules, functions, types and module variables for quick access
Index constituents are:
.. attribute:: modules
Dictionary where each key is a module name, and each value is the cracked block.
.. attribute:: routines
Module specific functions and subroutines
.. attribute:: types
Module specific types
.. attribute:: variables
Module specific variables
"""
# Containers
self.modules = {}
self.types = {}
self.routines = self.functions = self.subroutines = {}
self.variables = {}
self.programs = {}
# Loop on all blocks and subblocks
for block in self.crack:
# Modules
if block['block']=='module':
# Index module
module = block['name']
self.modules[module] = block
# Loop inside module
for subblock in block['body']:
# Types and routines as subblock
if subblock['block'] in ['function', 'type', 'subroutine']:
# Index
container = getattr(self, subblock['block']+'s')
container[subblock['name']] = subblock
subblock['module'] = module
# Variables
for varname, bvar in subblock['vars'].items():
bvar['name'] = varname
# Function aliases from "use only" (rescan)
for bfunc in self.routines.values(): bfunc['aliases'] = []
for subblock in block['body']:
if not subblock['block'] == 'use': continue
for monly in block['use'].values():
if not monly: continue
for fname, falias in monly['map'].items():
self.routines[falias] = self.routines[fname]
if falias not in self.routines[fname]['aliases']:
self.routines[fname]['aliases'].append(falias)
# Module variables
for varname, bvar in block['vars'].items():
if not varname in self.routines:
self.variables[varname] = bvar
#self.variables.pop(varname)
bvar['name'] = varname
bvar['module'] = module
# Local functions, subroutines and programs
elif block['block'] in ['function', 'subroutine', 'program']:
# Index
container = getattr(self, block['block']+'s')
container[block['name']] = block
# Variables
for varname, bvar in block['vars'].items():
bvar['name'] = varname
# Regular expression for fast search
# - function calls
subs = [block['name'].lower() for block in self.routines.values() if block['block']=='subroutine']
self._re_callsub_findall = subs and \
re.compile(r'call\s+(%s)\b'%('|'.join(subs)), re.I).findall or (lambda line: [])
funcs = [block['name'].lower() for block in self.routines.values() if block['block']=='function']
self._re_callfunc_findall = funcs and \
re.compile(r'\b(%s)\s*\('%('|'.join(funcs)), re.I).findall or (lambda line: [])
# - function variables
for block in self.routines.values():
vars = (r'|\b').join(block['sortvars']) + r'|\$(?P<varnum>\d+)'
sreg = r'[\w\*\-:]*(?:@param\w*)?(?P<varname>\b%s\b)\W+(?P<vardesc>.*)'%vars
block['vardescmatch'] = re.compile(sreg).match
# - variables with description
# for block in self.types.values()+self.modules.values():
for block in self.types.values()+self.modules.values()+self.routines.values():
#sreg = r'\b(?P<varname>%s)\b[\W\d]*!\s*(?P<vardesc>.*)'%'|'.join(block['sortvars'])
#sreg = r'[\W\(\),\b\*=\-\&]*?:?:[ \t\&]*(?P<varname>%s)\b[\w\s\(\)\*,_=]*!\s*(?P<vardesc>.*)'%'|'.join(block['sortvars'])
#sreg = r'.*[\W\(\),\b\*=\-\&]*?:?:[ \t\&]*(?P<varname>%s)\b[\w\s\(\)\*,_=\.]*!\s*(?P<vardesc>.*)'%'|'.join(block['sortvars'])
# reversed+sorted is a hack to avoid conflicts when variables share the same prefix
if block['sortvars']:
sreg = r'.*(?P<varname>%s)\s*(?P<dims>\([\*:,\w]+\))?[^!\)]*!\s*(?P<vardesc>.*)\s*'%'|'.join(reversed(sorted(block['sortvars'])))
block['vardescsearch'] = re.compile(sreg, re.I).search
else:
block['vardescsearch'] = lambda x: None
[docs] def build_callfrom_index(self):
"""For each function, index which function call it"""
for bfunc in self.routines.values():
bfunc['callfrom'] = []
for bfuncall in self.routines.values()+self.programs.values():
if bfunc['name'] in bfuncall['callto']:
bfunc['callfrom'].append(bfuncall['name'])
[docs] def filter_by_srcfile(self, sfile, mode=None, objtype=None, **kwargs):
"""Search for subblocks according to origin file
:Params:
- **sfile**: Source file name.
- **mode**, optional: Mode for searching for sfile.
If ``"strict"``, exact match is needed, else only basename.
- **objtype**, optional: Restrict search to one or a list of object types
(i.e. ``"function"``, ``"program"``, etc).
"""
if mode is None: mode = 'basename'
if objtype and not isinstance(objtype, list):
objtype = [objtype]
bb = []
if mode!='strict':
sfile = os.path.basename(sfile)
for b in self.crack:
if objtype and objtype != 'all' and b['block'] not in objtype: continue
bfile = b['from'].split(':')[0] # remove module name
if mode!='strict':
bfile = os.path.basename(bfile)
if sfile==bfile: bb.append(b)
return bb
[docs] def scan(self):
"""Scan """
# Loop on all blocks
for block in self.crack:
# Modules
if block['block']=='module':
# Get source lines
modsrc = self.get_blocksrc(block)
# Get module description
block['desc'] = self.get_comment(modsrc, aslist=True)
# Scan types and routines
for subblock in block['body']:
self.scan_container(subblock, insrc=modsrc)
# Scan module variables
self.strip_blocksrc(block, ['type', 'function', 'subroutine'], src=modsrc)
if modsrc:
for line in modsrc:
if line.strip().startswith('!'): continue
m = block['vardescsearch'](line)
if m:
block['vars'][m.group('varname').lower()]['desc'] = m.group('vardesc')
for bvar in block['vars'].values():
bvar.setdefault('desc', '')
# Routines
elif block['block'] in ['function', 'subroutine', 'program']:
self.scan_container(block)
[docs] def scan_container(self, block, insrc=None):
"""Scan a block of program, routines or type"""
# Check block type
if block['block'] not in ['type', 'function', 'subroutine', 'program']: return
# Source lines
subsrc = self.get_blocksrc(block, insrc)
# Comment
block['desc'] = self.get_comment(subsrc, aslist=True)
# Callable objects
if block['block'] in ['function', 'subroutine', 'program']:
# With signature : description of variables in comment
if block['block'] in ['function', 'subroutine']:
varname = None
for iline, line in enumerate(block['desc']):
if 'vardescmatch' in block:
m = block['vardescmatch'](line)
if m: # There is a variable and its description
# Name of variable
varname = m.group('varname')
# Numeric name
if m.group('varnum'): # $[1-9]+
ivar = int(m.group('varnum'))
ivar = ivar-1
if ivar<0 or ivar>=len(block['args']): continue
block['desc'][iline] = line.replace(varname, block['args'][ivar])
varname = block['args'][ivar]
# Store description
ifirst = len(line)-len(line.strip())
if varname != '':
block['vars'][varname]['desc'] = m.group('vardesc')
elif line.strip() and varname is not None and \
(len(line)-len(line.strip()))>ifirst: # Description continuation?
block['vars'][varname]['desc'].append(' '+line.strip())
else: varname = None
# Index calls
block['callto'] = []
if subsrc is not None:
self.join_src(subsrc)
for line in subsrc[1:-1]:
if line.strip().startswith('!'): continue
line = line.lower()
for fn in self._re_callsub_findall(line)+self._re_callfunc_findall(line):
if fn not in block['callto']:
block['callto'].append(fn)
pass
# Get description of variables (overwrite for functions and subroutines)
if block['block'] in ['function', 'subroutine', 'type'] and subsrc is not None:
for line in subsrc:
if line.strip().startswith('!'): continue
if 'vardescsearch' in block:
m = block['vardescsearch'](line)
if m:
block['vars'][m.group('varname').lower()]['desc'] = m.group('vardesc')
# Fill empty descriptions
for bvar in block['vars'].values():
bvar.setdefault('desc', '')
del subsrc
# Getting info ---
[docs] def get_module(self, block):
"""Get the name of the current module"""
while block['block']!='module':
if block['parentblock']=='unknown':
break
block = block['parentblock']
return block['name']
[docs] def get_src(self, block):
"""Get the source lines of the file including this block"""
srcfile = block['from'].split(':')[0]
return self.src[srcfile]
[docs] def join_src(self, src):
"""Join unended lines that does not finish with a comment"""
for iline, line in enumerate(src):
m = self._re_unended_match(line)
if m:
thisline = m.group(1)
m = self._re_unstarted_match(src[iline+1])
nextline = m.group(1) if m else src[iline+1]
src[iline] = thisline+nextline
del src[iline+1]
return src
[docs] def get_blocksrc(self, block, src=None, istart=0, getidx=False, stopmatch=None, exclude=None):
"""Extract an identified block of source code
:Parameters:
- *block*: Cracked block
:Options:
- *src*: List of source line including the block
- *istart*: Start searching from this line number
:Return:
``None`` or a list of lines
"""
# Set up
if src is None: src = self.get_src(block)
blocktype = block['block'].lower()
blockname = block['name'].lower()
ftypes = '(?:(?:%s).*\s+)?'%fortrantypes if blocktype=='function' else ''
rstart = re.compile(r"^\s*%s%s\s+%s\b.*$"%(ftypes, blocktype, blockname), re.I).match
rend = re.compile(r"^\s*end\s+%s\b.*$"%blocktype, re.I).match
if isinstance(stopmatch, str):
stopmatch = re.compile(stopmatch).match
# Beginning
for ifirst in xrange(istart, len(src)):
# Simple stop on match
if stopmatch and stopmatch(src[ifirst]): return
# Ok, now check
if rstart(src[ifirst]): break
else:
return
# End
for ilast in xrange(ifirst, len(src)):
if stopmatch and stopmatch(src[ilast]): break
if rend(src[ilast].lower()): break
# Extraction
mysrc = list(src[ifirst:ilast+1])
# Block exclusions
self.strip_blocksrc(block, exclude, src=mysrc)
if getidx: return mysrc, (ifirst, ilast)
return mysrc
[docs] def strip_blocksrc(self, block, exc, src=None):
"""Strip blocks from source lines
:Parameters:
- *block*:
- *exc* list of block type to remove
:Options:
- *src*: list of source lines
:Example:
>>> obj.strip_blocksrc(lines, 'type')
>>> obj.strip_blocksrc(lines, ['function', 'type']
"""
if src is None: self.get_blocksrc(block)
if exc is None: return
if not isinstance(exc, list): exc = [exc]
for subblock in block['body']:
if subblock['block'] in exc:
subsrc = self.get_blocksrc(subblock, src=src, getidx=True)
if subsrc is None: continue # Already stripped
del src[subsrc[1][0]:subsrc[1][1]]
del subsrc
[docs] def get_comment(self, src, iline=1, aslist=False, stripped=False, getilast=False, rightafter=True):
"""Search for and return the comment starting after ``iline`` in ``src``
:Params:
- **src**: A list of lines.
- **iline**, optional: Index of first line to read.
- **aslist**, optional: Return the comment as a list.
- **stripped**, optional: Strip each line of comment.
- **getilast**, optional: Get also index of last line of comment.
- **rightafter**, optional: Suppose the comment right after
the signature line. If True, it prevents from reading a comment
that is not a description of the routine.
:Return:
- ``scomment``: string or list
- OR ``scomment,ilast``: if ``getilast is True``
"""
scomment = []
if src:
in_a_breaked_line = src[0].strip().endswith('&')
for iline in xrange(iline, len(src)):
line = src[iline].strip()
# Breaked line
if line.startswith('&'): continue
# Manage no comment line
m = self._re_comment_match(line)
if m is None:
if not scomment:
# Not the end of signature
if line.endswith('&'):
in_a_breaked_line = True
continue
if in_a_breaked_line:
in_a_breaked_line = False
continue
# Empty line but we continue searching
if not rightafter and not line:
continue
# Stop searching
break
# Get comment part
comment = m.group(1)
# Strip?
if stripped: comment = comment.strip()
# Load and remove space prefix
if not scomment:
prefix = self._re_space_prefix_match(comment).group(1)
if comment.startswith(prefix): comment = comment[len(prefix):]
# Save comment
scomment.append(comment)
if not aslist:
scomment = self.format_lines(scomment, nlc=' ')
if getilast: return scomment, iline
return scomment
[docs] def get_synopsis(self, block, nmax=2):
"""Get the first ``nmax`` non empty lines of the function, type or module comment as 1 line.
If the header has more than ``nmax`` lines, the first one is taken and appended of '...'.
If description if empty, it returns an empty string.
"""
sd = []
for line in block['desc']:
line = line.strip()
if not line:
if not sd: continue
break
sd.append(line)
if len(sd)>nmax:
if sd[-1].endswith('.'): sd[-1] += '...'
break
if not sd: return ''
sd = ' '.join(sd)
return sd
[docs] def get_blocklist(self, choice, module):
"""Get the list of types, variables or function of a module"""
choice = choice.lower()
if not choice.endswith('s'): choice += 's'
assert choice in ['types', 'variables', 'functions', 'subroutines'], "Wrong type of declaration"
module = module.lower()
assert module in self.modules.keys(), "Wrong module name"
baselist = getattr(self, choice).values()
return [v for v in baselist if 'module' in v and v['module'] == module.lower()]
# Formating ---
[docs] def set_ulc(self, ulc):
"""Set the underline character for title inside module description"""
self._ulc = ulc
[docs] def get_ulc(self):
"""Get the underline character for title inside module description"""
return self._ulc
ulc = property(get_ulc, set_ulc, doc='Underline character for title inside module description')
ic = property(get_ic, set_ic, doc='Indentation character')
sst = property(get_sst, set_sst, doc='Subsection type ("title" or "rubric")')
[docs] def format_lines(self, lines, indent=0, bullet=None, nlc='\n', strip=False):
"""Convert a list of lines to text"""
if not lines: return ''
# Bullet
if bullet is True: bullet='-'
bullet = (str(bullet)+' ') if bullet else ''
# Get current indentation for reduction
if isinstance(lines, basestring): lines = [lines]
# Split lines
tmp = []
for line in lines:
if not line:
tmp.append(line)
else:
tmp.extend(line.splitlines())
lines = tmp ; del tmp
# Expand tabs
lines = [line.expandtabs(4) for line in lines]
# Strip
if strip:
tmp = []
for line in lines:
if not tmp and not line:
continue
tmp.append(line)
lines = tmp ; del tmp
if not lines: return ''
# Column of first non space car
goodlines = [(len(line)-len(line.lstrip())) for line in lines if line.expandtabs().strip()]
firstchar = goodlines and min(goodlines) or 0 ; del goodlines
# Redent
mylines = [self.indent(indent)+bullet+line[firstchar:] for line in lines]
# Create text block
text = nlc.join(mylines)+nlc
del mylines
return text
[docs] def format_title(self, text, ulc=None, indent=0):
"""Create a simple rst titlec with indentation
:Parameters:
- *text*: text of the title
:Options:
- *ulc*: underline character (default to attribute :attr:`ucl`)
:Example:
>>> print o.format_title('My title', '-')
My title
--------
"""
if ulc is None: ulc = self.ulc
return self.format_lines([text, ulc*len(text)], indent=indent)+'\n'
[docs] def format_rubric(self, text, indent=0):
"""Create a simple rst rubric with indentation
:Parameters:
- *text*: text of the rubric
:Example:
>>> print o.format_rubric('My title', '-')
.. rubric:: My rubric
"""
return self.format_lines('.. rubric:: '+text, indent=indent)+'\n'
[docs] def format_subsection(self, text, indent=indent, **kwargs):
"""Format a subsection for describing list of subroutines, types, etc"""
if self.sst=='title':
return self.format_title(text, indent=indent, **kwargs)
return self.format_rubric(text, indent=indent, **kwargs)
[docs] def format_declaration(self, dectype, name, description=None, indent=0, bullet=None, options=None):
"""Create an simple rst declaration
:Example:
>>> print format_declaration('var', 'myvar', 'my description', indent=1, bullet='-')
- .. f:var:: myvar
my description
"""
declaration = self.format_lines('.. f:%(dectype)s:: %(name)s'%locals(), bullet=bullet, indent=indent)
if options:
declaration += self.format_options(options, indent=indent+1)
declaration += '\n'
if description:
declaration += self.format_lines(description, indent=indent+1)
return declaration+'\n'
[docs] def format_options(self, options, indent=0):
"""Format directive options"""
options = [':%s: %s'%option for option in options.items() if option[1] is not None]
return self.format_lines(options, indent=indent)
[docs] def format_funcref(self, fname, current_module=None, aliasof=None, module=None):
"""Format the reference link to a module function
Formatting may vary depending on if function is local
and is an alias.
:Example:
>>> print obj.format_type('myfunc')
:f:func:`~mymodule.myfunc`
"""
# Alias?
fname = fname.lower()
if aliasof is not None:
falias = fname
fname = aliasof
if fname in self.routines and fname in self.routines[fname].get('aliases', []):
falias = fname
fname = self.routines[fname]['name']
else:
falias = None
# Local reference ?
if module is None and fname in self.routines:
module = self.routines[fname].get('module')
if module is None or (current_module is not None and module==current_module):
if falias:
return ':f:func:`%(falias)s<%(fname)s>`'%locals()
return ':f:func:`%(fname)s`'%locals()
# Remote reference
from fortran_domain import f_sep
if falias:
':f:func:`%(falias)s<~%(module)s%(f_sep)s%(fname)s>`'%locals()
return ':f:func:`~%(module)s%(f_sep)s%(fname)s`'%locals()
[docs] def format_use(self, block, indent=0, short=False):
"""Format use statement
:Parameters:
- *block*: a module block
"""
# TODO: format aliases bug
use = ''
if 'use' in block:
if short:
use = ':use: '
else:
use = self.format_subsection('Needed modules', indent=indent)
lines = []
for mname, monly in block['use'].items():
# Reference to the module
line = (self.indent(indent) if not short else '')+':f:mod:`%s`'%mname
# Reference to the routines
if monly:
funcs = []
for fname, falias in monly['map'].items():
func = self.format_funcref(fname, module=mname)
if fname!=falias:
falias = self.format_funcref(falias, module=mname, aliasof=fname)
func = '%s => %s' % (falias, func)
funcs.append(func)
line += ' (%s)'% ', '.join(funcs)
# Short description
if mname in self.modules and not short:
sdesc = self.get_synopsis(self.modules[mname])
if sdesc:
line += ': '+sdesc
# Append
lines.append(line)
if short:
use += ', '.join(lines)
use = self.format_lines(use,indent)
else:
use += self.format_lines(lines, indent, bullet='-') + '\n'
del lines
return use
#def format_arithm(self, expr):
#"""Format an arithmetic expression"""
#ops = re.findall(r'(\W+)',expr)
#nums = re.split(r'\W+', expr)
#if expr.startswith(ops[0]):
#nums = ['']+nums
#if len(nums)!=len(ops): ops.append('')
#newexpr = ''
#for num, op in zip(nums, ops):
#if num.isalpha():
#num = ' :f:var:`%s` '%num
#if '*' in op:
#op = op.replace('*', '\*')
#newexpr += num+op
#return newexpr
[docs] def format_argdim(self, block):
"""Format the dimension of a variable
:Parameters:
- *block*: a variable block
"""
if 'dimension' in block:
return'(%s)'%(','.join([s.strip('()') for s in block['dimension']]))
#return'(%s)'%(','.join([s.replace(':','\:').strip('()') for s in block['dimension']]))
return ''
[docs] def format_argattr(self, block):
"""Filter and format the attributes (optional, in/out/inout, etc) of a variable
:Parameters:
- *block*: a variable block
"""
vattr = []
if 'intent' in block and block['intent']:
vattr.append('/'.join(block['intent']))
if 'attrspec' in block and block['attrspec']:
newattrs = []
for attr in block['attrspec']:
# if '=' in block:
# if attr=='optional':
# continue
# elif attr=='parameter':
# attr += '='+block['=']
# #attr += '='+self.format_arithm(block['='])
# if attr in []:
# attr = attr.upper()
newattrs.append(attr)
if '=' in block:
newattrs.append('default='+block['='])
if 'private' in newattrs and 'public' in newattrs:
newattrs.remove('private')
block['attrspec'] = newattrs
vattr.append('/'.join(block['attrspec']))
if not vattr: return ''
vattr = ','.join(vattr)
return self._fmt_vattr%locals() if vattr else ''
[docs] def format_argtype(self, block):
if not 'typespec' in block: return ''
vtype = block['typespec']
if vtype=='type': vtype = block['typename']
return vtype
[docs] def format_argfield(self, blockvar, role=None, block=None):
"""Format the description of a variable
:Parameters:
- *block*: a variable block
"""
vname = blockvar['name']
vtype = self.format_argtype(blockvar)#['typespec']
#if vtype=='type': vtype = block['typename']
vdim = self.format_argdim(blockvar)
if ':' in vdim:
vdim = vdim.replace(':','*')
vattr = self.format_argattr(blockvar)
vdesc = blockvar['desc'] if 'desc' in blockvar else ''
optional = 'attrspec' in blockvar and 'optional' in blockvar['attrspec']
if not role:
if block and vname in [block['name'], block.get('result')]:
role = 'r'
else:
role = 'o' if optional else 'p'
return self._fmt_vardesc%locals()
[docs] def format_type(self, block, indent=0, bullet=True):
"""Format the description of a module type
:Parameters:
- *block*: block of the type
"""
# Declaration and description
declaration = self.format_declaration('type', block['name'], block['desc'], indent=indent, bullet=bullet)+'\n'
# Variables
vlines = []
for bvar in block['vars'].values():
vlines.append(self.format_argfield(bvar, role='f'))
variables = self.format_lines(vlines, indent=indent+1)+'\n'
del vlines
return declaration+variables
[docs] def get_varopts(self, block):
"""Get options for variable declaration as a dict"""
options = {}
vdim = self.format_argdim(block)
#if vdim!='': vdim = self._fmt_fvardim%locals()
if vdim:
options['shape'] = vdim
options['type'] = self.format_argtype(block)
vattr = self.format_argattr(block).strip(' []')
if vattr:
options['attrs'] = vattr
return options
#def format_vardesc(self, block):
#"""Format the specification of a variable at top of its declaration content"""
#specs = self.format_varspecs(block)
#vdesc = block.get('desc', '')
#return specs+'\n'+vdesc+'\n'
[docs] def format_var(self, block, indent=0, bullet=True):
"""Format the description of a module type
:Parameters:
- *block*: block of the variable
"""
# Description of the variable
options = self.get_varopts(block)
description = block.get('desc', None)
if 'name' in block:
declaration = self.format_declaration('variable', block['name'], description=description,
options=options, indent=indent, bullet=bullet)
else:
declaration = ''
## Description of the sub-variables
#if block['typespec']=='type':
#xxxx
#variables = []
#btype = self.types[block['typename']]
#for bvar in btype['vars'].values():
#vname = '%s%%%s'%(block['name'], bvar['name'])
#desc = self.format_vardesc(bvar, indent=0, vname=vname)
#variables.append(self.format_declaration('var', vname, desc, indent=indent+1, bullet=True)[:-1])
#variables = '\n'.join(variables)
#else:
#variables = ''
return declaration#+variables
[docs] def format_signature(self, block):
signature = ''
nopt = 0
for i, var in enumerate(block['args']):
optional = 'optional' in block['vars'][var]['attrspec'] and '=' not in block['vars'][var] \
if 'attrspec' in block['vars'][var] else False
signature += '[' if optional else ''
signature += ', ' if i else ''
if optional: nopt +=1
signature += var
signature += nopt*']'
return signature
[docs] def format_routine(self, block, indent=0):
"""Format the description of a function, a subroutine or a program"""
# Declaration of a subroutine or function
if isinstance(block, basestring):
if block not in self.programs.keys()+self.routines.keys():
raise F90toRstException('Unknown function, subroutine or program: %s'%block)
if block in self.programs:
block = self.programs[block]
else:
block = self.routines[block]
elif block['name'] not in self.modules.keys()+self.routines.keys()+self.programs.keys():
raise F90toRstException('Unknown %s: %s'%(block['block'], block['name']))
name = block['name']
blocktype = block['block']
signature = '(%s)'%self.format_signature(block) if blocktype!='program' else ''
declaration = self.format_declaration(blocktype,
'%(name)s%(signature)s'%locals(), indent=indent)
#declaration = self.indent(indent)+'.. f:%(blocktype)s:: %(name)s%(signature)s\n\n'%locals()
# Treat variables in comment (subroutines and functions only)
comments = list(block['desc'])+['']
if blocktype!='program' :
found = []
for iline in xrange(len(comments)):
if 'vardescmatch' in block:
m = block['vardescmatch'](comments[iline])
if m:
varname = m.group('varname')
found.append(varname)
if varname != '':
comments[iline] = self.format_argfield(block['vars'][varname], block=block)
for varname in block['args']+block['sortvars']:
if varname not in found:
comments.append(self.format_argfield(block['vars'][varname], block=block))
found.append(varname)
# Description
description = self.format_lines(comments, indent+1)
# Add use of modules
use = self.format_use(block, indent=indent+1, short=True)
# Add calls
calls = []
module = block.get('module')
# - call froms
if blocktype in ['function', 'subroutine']:
if 'callfrom' in block and block['callfrom']:
callfrom = []
for fromname in block['callfrom']:
if fromname in self.routines:
cf = self.format_funcref(fromname, module)
else:
cf = ':f:prog:`%s`'%fromname
callfrom.append(cf)
#callfrom += ', '.join([self.format_funcref(getattr(self, routines[fn]['name'], module) for fn in block['callfrom']])
callfrom = ':from: ' + ', '.join(callfrom)
calls.append(callfrom)
# - call tos
if block['callto']:
callto = ', '.join([self.format_funcref(fn, module) for fn in block['callto']])
#callto = ', '.join([self.format_funcref(self.routines[fn]['name'], module) for fn in block['callto']])
if callto == '': callto = 'None'
callto = ':to: '+callto
calls.append(callto)
calls = '\n'+self.format_lines(calls, indent=indent+1)
return declaration+description+use+calls+'\n\n'
format_function = format_routine
format_subroutine = format_routine
[docs] def format_quickaccess(self, module, indent=indent):
"""Format an abstract of all types, variables and routines of a module"""
if not isinstance(module, basestring): module = module['name']
# Title
title = self.format_subsection('Quick access', indent=indent)+'\n'
# Types
decs = []
tlist = self.get_blocklist('types', module)
tlist.sort()
if tlist:
decs.append(':Types: '+', '.join([':f:type:`%s`'%tt['name'] for tt in tlist]))
# Variables
vlist = self.get_blocklist('variables', module)
vlist.sort()
if vlist:
decs.append(':Variables: '+', '.join([':f:var:`%s`'%vv['name'] for vv in vlist]))
# Functions and subroutines
flist = self.get_blocklist('functions', module)
flist.sort()
if flist:
decs.append(':Routines: '+', '.join([':f:func:`~%s/%s`'%(module, ff['name']) for ff in flist]))
if decs: return self.format_lines(title+'\n'.join(decs))+'\n\n'
return ''
[docs] def format_types(self, block, indent=0):
"""Format the description of all fortran types"""
types = []
for subblock in block['body']:
if subblock['block']=='type':
types.append(self.format_type(subblock, indent=indent))
if types:
types = self.format_subsection('Types', indent=indent)+'\n'.join(types)
else:
types = ''
return types
[docs] def format_variables(self, block, indent=0):
"""Format the description of all variables (global or module)"""
variables = ''
if block['vars']:
for bvar in block['vars'].values():
variables += self.format_var(bvar, indent=indent)
variables = self.format_subsection('Variables', indent=indent)+variables+'\n\n'
return variables
[docs] def format_description(self, block, indent=0):
"""Format the description of an object"""
description = ''
if block['desc']:
description = self.format_subsection('Description', indent=indent)
description += self.format_lines(block['desc'],indent=indent, strip=True)+'\n'
return description
[docs] def format_routines(self, block, indent=0):
"""Format the list of all subroutines and functions"""
routines = ''
blocks = block if isinstance(block, list) else block['body']
fdecs = []
for subblock in blocks: #block['body']:
if subblock['block'] in ['function', 'subroutine']:
fdecs.append(self.format_routine(subblock, indent))
if fdecs:
fdecs = '\n'.join(fdecs)
routines = self.format_subsection('Subroutines and functions', indent=indent)+fdecs
return routines
[docs] def format_module(self, block, indent=0):
"""Recursively format a module and its declarations"""
# Declaration of the module
if isinstance(block, basestring):
if block not in self.modules:
raise F90toRstException('Unknown module: %s'%block)
block = self.modules[block]
elif block['name'] not in self.modules:
raise F90toRstException('Unknown module: %'%block['name'])
modname = block['name']
declaration = self.format_declaration('module', modname, indent=indent,
options=dict(synopsis = self.get_synopsis(block).strip() or None))
# Description
description = self.format_description(block, indent=indent)
# Quick access
quickaccess = self.format_quickaccess(modname, indent=indent)
# Use of other modules
use = self.format_use(block, indent=indent)
# Types
types = self.format_types(block, indent=indent)
# Variables
variables = self.format_variables(block, indent=indent)
# Subroutines and functions
routines = self.format_routines(block, indent=indent)
return declaration + description + quickaccess + use + types + variables + routines
[docs] def format_srcfile(self, srcfile, indent=0, objtype=None, search_mode='basename', **kwargs):
"""Format all declaration of a file, except modules"""
rst = ''
if objtype is not None and not isinstance(objtype, (list, tuple)):
objtype = [objtype]
# Programs
if objtype is None or 'program' in objtype:
bprog = self.filter_by_srcfile(srcfile, objtype='program', mode=search_mode)
if bprog:
rst += self.format_subsection('Program', indent=indent)+'\n'
rst += self.format_routine(bprog[0], indent=indent)+'\n'
# Modules
if objtype is None or 'module' in objtype:
bmod = self.filter_by_srcfile(srcfile, objtype='module', mode=search_mode)
if bmod:
rst += self.format_subsection('Module', indent=indent)+'\n'
rst += self.format_module(bmod[0], indent=indent)+'\n'
# Functions and subroutines
oal = ['function', 'subroutine']
oo = [o for o in oal if o in objtype] if objtype is not None else oal
if oo:
brouts = self.filter_by_srcfile(srcfile, objtype=oo, mode=search_mode)
rst += self.format_routines(brouts, indent=indent)+'\n'
return rst
def __getitem__(self, module):
return self.format_module(self.modules[module])
# Sphinx directive ---
[docs]def list_files(fortran_src, exts=['f', 'f90', 'f95'], absolute=True):
"""Get the list of fortran files"""
# Extensions (suffixes)
if not isinstance(exts, list):
exts = list(exts)
for e in exts:
if e.lower() not in exts: exts.append(e.lower())
if e.upper() not in exts: exts.append(e.upper())
exts = list(set(exts))
# List the files using globs
ffiles = []
for fg in fortran_src:
if not isinstance(fg, basestring): continue
if os.path.isdir(fg):
for ext in exts:
ffiles.extend(glob(os.path.join(fg,'*.'+ext)))
else:
ffiles.extend(glob(fg))
if absolute:
ffiles = [os.path.abspath(ffile) for ffile in ffiles]
ffiles.sort()
return ffiles
[docs]def fortran_parse(app):
env = app.builder.env
if isinstance(app.config.fortran_src, (str, list)):
app.info(bold('parsing fortran sources...'), nonl=True)
# Sources a list
if not isinstance(app.config.fortran_src, list):
app.config.fortran_src = [app.config.fortran_src]
# All files
ffiles = list_files(app.config.fortran_src, app.config.fortran_ext)
# Parse files
if not ffiles:
app.info(" no fortran files found")
app.config._f90torst = None
else:
app.config.fortran_indent = fmt_indent(app.config.fortran_indent)
app.config._f90torst = F90toRst(ffiles, ic=app.config.fortran_indent,
ulc=app.config.fortran_title_underline, encoding=app.config.fortran_encoding)
app.info(' done')
app._status.flush()
else:
app.warn("wrong list of fortran 90 source specifications: "+str(app.config.fortran_src))
app.config._f90torst = None
# app.config._f90files = []
[docs]def fmt_indent(string):
if string is None: return
if isinstance(string, int): string = ' '*string
if string == 'tab': string = '\t'
elif string == 'space': string = ' '
return string
#class fortran_module(nodes.General, nodes.Element):
#pass
[docs]class FortranAutoModuleDirective(Directive):
has_content = True
option_spec = dict(title_underline=unchanged, indent=fmt_indent,
subsection_type=unchanged)
required_arguments = 1
optional_arguments = 0
[docs] def run(self):
# Get environment
f90torst = self.state.document.settings.env.config._f90torst
if f90torst is None: return []
# Check module name
module = self.arguments[0]
if module not in f90torst.modules:
# print dir(self)
print 'Wrong fortran module name: '+module
self.state_machine.reporter.warning('Wrong fortran module name: '+module, line=self.lineno)
# self.warn('Wrong fortran module name: '+module)
# Options
ic = f90torst.ic
ulc = f90torst.ulc
if self.options.get('indent'):
f90torst.ic = self.options['indent']
if self.options.get('title_underline'):
f90torst.ulc = self.options['title_underline']
if self.options.get('subsection_type'):
f90torst.ulc = self.options['subsection_type']
# Get rst
raw_text = f90torst.format_module(module)
# Insert it
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1)
include_lines = string2lines(raw_text, convert_whitespace=1)
self.state_machine.insert_input(include_lines,source)
# Restore defaults
if 'indent' in self.options: f90torst.ic = ic
if 'title_underline' in self.options: f90torst.ulc = ulc
if 'subsection_type' in self.options: f90torst.sst = sst
return []
[docs]class FortranAutoObjectDirective(Directive):
"""Generic directive for fortran object auto-documentation
Redefine :attr:`_warning` and :attr:`_objtype` attribute when subcassling.
.. attribute:: _warning
Warning message when object is not found, like:
>>> _warning = 'Wrong function or subroutine name: %s'
.. attribute:: _objtype
Type of fortran object.
If "toto" is set as object type, then :class:`F90toRst` must have
attribute :attr:`totos` containg index of all related fortran objects,
and method :meth:`format_totos` for formatting the object.
"""
has_content = False
option_spec = {}
required_arguments = 1
optional_arguments = 0
_warning = 'Wrong routine name: %s'
_objtype = 'routine'
[docs] def run(self):
# Get environment
f90torst = self.state.document.settings.env.config._f90torst
if f90torst is None: return []
# Check object name
objname = self.arguments[0].lower()
from fortran_domain import f_sep
if f_sep in objname: objname = objname.split(f_sep)[-1] # remove module name
objects = getattr(f90torst, self._objtype+'s')
if objname not in objects:
print self._warning%objname
self.state_machine.reporter.warning(self._warning%objname, line=self.lineno)
# self.warn(self._warning%objname)
# Get rst
raw_text = getattr(f90torst, 'format_'+self._objtype)(objname)
# Check if inside module
b = objects[objname]
if 'parent_block' in b:
curmod_text = '.. f:currentmodule:: %s\n\n'%b['parent_block']['name']
raw_text = curmod_text+raw_text
# Insert it
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1)
include_lines = string2lines(raw_text, convert_whitespace=1)
self.state_machine.insert_input(include_lines,source)
return []
[docs]class FortranAutoFunctionDirective(FortranAutoObjectDirective):
_warning = 'Wrong function name: %s'
_objtype = 'function'
[docs]class FortranAutoSubroutineDirective(FortranAutoObjectDirective):
_warning = 'Wrong subroutine name: %s'
_objtype = 'subroutine'
[docs]class FortranAutoTypeDirective(FortranAutoObjectDirective):
_warning = 'Wrong type name: %s'
_objtype = 'type'
[docs]class FortranAutoVariableDirective(FortranAutoObjectDirective):
_warning = 'Wrong variable name: %s'
_objtype = 'variable'
[docs]class FortranAutoProgramDirective(Directive):
has_content = False
option_spec = {}
required_arguments = 1
optional_arguments = 0
[docs] def run(self):
print 'test1'
self.state_machine.reporter.warning('test2', line=self.lineno)
# Get environment
f90torst = self.state.document.settings.env.config._f90torst
if f90torst is None: return []
# Check routine name
program = self.arguments[0].lower()
if program not in f90torst.programs:
print 'Wrong program name: '+program
self.state_machine.reporter.warning('Wrong program name: '+program, line=self.lineno)
# self.warning('Wrong program name: '+program)
# Get rst
raw_text = f90torst.format_routine(program)
# Insert it
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1)
include_lines = string2lines(raw_text, convert_whitespace=1)
self.state_machine.insert_input(include_lines,source)
return []
[docs]class FortranAutoSrcfileDirective(Directive):
has_content = False
option_spec = dict(search_mode=unchanged, objtype=unchanged)
required_arguments = 1
optional_arguments = 0
[docs] def run(self):
# Get environment
f90torst = self.state.document.settings.env.config._f90torst
if f90torst is None: return []
# Options
search_mode = self.options.get('search_mode')
objtype = self.options.get('objtype')
if objtype:
objtype = objtype.split(' ,')
# Get rst
srcfile = self.arguments[0].lower()
raw_text = f90torst.format_srcfile(srcfile, search_mode=search_mode, objtype=objtype)
if not raw_text:
msg = 'No valid content found for file: '+srcfile
print msg
self.state_machine.reporter.warning(msg, line=self.lineno)
# self.warning('No valid content found for file: '+srcfile)
# Insert it
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1)
include_lines = string2lines(raw_text, convert_whitespace=1)
self.state_machine.insert_input(include_lines,source)
return []
[docs]def setup(app):
app.add_description_unit('ftype', 'ftype', indextemplate='pair: %s; Fortran type', )
app.add_description_unit('fvar', 'fvar', indextemplate='pair: %s; Fortran variable', )
app.add_config_value('fortran_title_underline', '-', False)
app.add_config_value('fortran_indent', 4, False)
app.add_config_value('fortran_subsection_type', 'rubric', False)
app.add_config_value('fortran_src', ['.'], False)
app.add_config_value('fortran_ext', ['f90', 'f95'], False)
app.add_config_value('fortran_encoding', 'utf8', False)
#app.add_directive('fortran_module', IncludeFortranDirective)
FortranDomain.directives.update(
automodule=FortranAutoModuleDirective,
autoroutine=FortranAutoObjectDirective,
autofunction=FortranAutoFunctionDirective,
autosubroutine=FortranAutoSubroutineDirective,
autoprogram=FortranAutoProgramDirective,
autosrcfile=FortranAutoSrcfileDirective,
)
app.connect('builder-inited', fortran_parse)