Source code for fortdepend.fort_depend

from __future__ import print_function

import os
import sys

# Terminal colours
from colorama import Fore

from .smartopen import smart_open
from .units import FortranFile, FortranModule
from .graph import Graph

# Python 2/3 compatibility
try:
    input = raw_input
except NameError:
    pass

DEPFILE_HEADER = "# This file is generated automatically. DO NOT EDIT!"
DEFAULT_IGNORED_MODULES = ["iso_c_binding", "iso_fortran_env"]


[docs]class FortranProject: """Read a set of Fortran source files and produce a set of `FortranFile`, `FortranModule` and the dependencies between them Example: This is the main class for interacting with a Fortran project. The minimal "useful" thing is: >>> import fortdepend >>> my_project = fortdepend.FortranProject() >>> my_project.write_depends() This will read all the .f90 and .F90 files in the current directory and write the dependencies to "makefile.dep" Args: name (str): Name of the project (default: name of current directory) exclude_files (list of str): List of files to exclude files (list of str): List of files to include (default: all in current directory) ignore_modules (list of str): List of module names to ignore_mod (default: iso_c_binding and iso_fortran_env) macros (dict, list or str): Preprocessor macro definitions cpp_includes (list of str): List of directories to add to preprocessor search path use_preprocessor (bool): Use the preprocessor (default: True) verbose (bool): Print more messages (default: False) """ def __init__( self, name=None, exclude_files=None, files=None, ignore_modules=None, macros=None, cpp_includes=None, use_preprocessor=True, verbose=False, ): if name is None: self.name = os.path.basename(os.getcwd()) else: self.name = name if files is None: files = self.get_source() elif not isinstance(files, list): files = [files] if exclude_files is not None: if not isinstance(exclude_files, list): exclude_files = [exclude_files] files = set(files) - set(exclude_files) self.files = { filename: FortranFile( filename=filename, macros=macros, readfile=True, cpp_includes=cpp_includes, use_preprocessor=use_preprocessor, ) for filename in files } self.modules = self.get_modules() self.programs = { k: v for k, v in self.modules.items() if v.unit_type == "program" } self.remove_ignored_modules(ignore_modules) self.depends_by_module = self.get_depends_by_module(verbose) self.depends_by_file = self.get_depends_by_file(verbose)
[docs] def get_source(self, extensions=None): """Return a list of filenames ending with any of extensions Args: extensions: List of file extensions (defaults to [".f90", ".F90"]) """ if extensions is None: extensions = [".f90", ".F90"] elif not isinstance(extensions, list): extensions = [extensions] tmp = os.listdir(".") files = [] for ext in extensions: files.extend([x for x in tmp if x.endswith(ext)]) return files
[docs] def get_modules(self): """Return a dict of all the modules found in the project Works by iterating over the list of `FortranFile` and merging their dicts of `FortranModule` Returns: dict of module name (str) and `FortranModule` objects """ mod_dict = {} for source_file in self.files.values(): mod_dict.update(source_file.modules) return mod_dict
[docs] def get_depends_by_module(self, verbose=False): """Return the set of which modules each module directly depends on Args: verbose: Print progress messages Returns: dict of `FortranModule` and a list of `FortranModule` """ depends = {} for module in self.modules.values(): graph = [] for used_mod in module.uses: try: graph.append(self.modules[used_mod]) except KeyError: new_module = FortranModule(unit_type="module", name=used_mod) graph.append(new_module) print( Fore.RED + "Error" + Fore.RESET + " module " + Fore.GREEN + used_mod + Fore.RESET + " not defined in any files. Creating empty ", file=sys.stderr, ) depends[module] = sorted(graph, key=lambda f: f.source_file.filename) if verbose: for module_ in sorted(depends.keys(), key=lambda f: f.source_file.filename): print( Fore.GREEN + module_.name + Fore.RESET + " depends on :" + Fore.BLUE ) for dep in depends[module_]: print("\t" + dep.name) print(Fore.RESET) return depends
[docs] def get_depends_by_file(self, verbose=False): """Return the set of which files each file directly depends on Args: verbose: Print progress messages Returns: dict of `FortranFile` and a list of `FortranFile` """ depends = {} for source_file in self.files.values(): graph = [] for mod in source_file.uses: try: mod_file = self.modules[mod].source_file # Don't add self as a dependency if mod_file.filename.lower() == source_file.filename.lower(): continue graph.append(mod_file) except KeyError: print( Fore.RED + "Error" + Fore.RESET + " module " + Fore.GREEN + mod + Fore.RESET + " not defined in any files. Skipping...", file=sys.stderr, ) depends[source_file] = sorted(graph, key=lambda f: f.filename) if verbose: for file_ in sorted(depends.keys(), key=lambda f: f.filename): print( Fore.GREEN + file_.filename + Fore.RESET + " depends on :" + Fore.BLUE ) for dep in depends[file_]: print("\t" + dep.filename) print(Fore.RESET) return depends
[docs] def get_all_used_files(self, module_name): """Get the complete set of files that a module requires, either directly or indirectly Args: module_name (str): A module name Returns: list of filenames (str) """ used_modules = self._get_all_used_modules(module_name, state=[]) used_files = [ self.modules[module].source_file.filename for module in used_modules ] module_filename = self.modules[module_name].source_file.filename return sorted(set(used_files + [module_filename]))
[docs] def get_all_used_modules(self, module_name): """Get the complete set of modules that module_name requires, either directly or indirectly Args: module_name (str): A module name Returns: list of module names (str) """ return self._get_all_used_modules(module_name, state=[])
def _get_all_used_modules(self, module_name, state): """Implementation for get_all_used_modules and get_all_used_files. Needs to keep track of some additional state which doesn't need to be exposed to users """ for module in self.modules[module_name].uses: try: if module in state: continue state.append(module) if self.modules[module].uses: state.extend(self._get_all_used_modules(module, state)) except KeyError: print( Fore.RED + "Error" + Fore.RESET + " module " + Fore.GREEN + module + Fore.RESET + " not defined in any files. Skipping...", file=sys.stderr, ) return sorted(set(state))
[docs] def write_depends( self, filename="makefile.dep", overwrite=False, build="", skip_programs=False ): """Write the dependencies to file Args: filename (str): Name of the output file overwrite (bool): Overwrite existing dependency file [False] build (str): Directory to prepend to filenames skip_programs (bool): Don't write dependencies for programs """ def _format_dependencies(target, target_extension, dep_list): _, filename = os.path.split(target) target_name = os.path.splitext(filename)[0] + target_extension listing = "\n{} : ".format(os.path.join(build, target_name)) for dep in dep_list: _, depfilename = os.path.split(dep) depobjectname = os.path.splitext(depfilename)[0] + ".o" listing += " \\\n\t{}".format(os.path.join(build, depobjectname)) listing += "\n" return listing # Test file doesn't exist if os.path.exists(filename): if not (overwrite): print( Fore.RED + "Warning: file '{}' exists.".format(filename) + Fore.RESET ) opt = input("Overwrite? Y... for yes.") if opt.lower().startswith("y"): pass else: return with smart_open(filename, "w") as f: f.write(DEPFILE_HEADER + "\n") if not skip_programs: for program in self.programs.keys(): program_deps = self.get_all_used_files(program) listing = _format_dependencies(program, "", program_deps) f.write(listing) for file_ in sorted(self.depends_by_file.keys(), key=lambda f: f.filename): dep_list = [dep.filename for dep in self.depends_by_file[file_]] listing = _format_dependencies(file_.filename, ".o", dep_list) f.write(listing)
[docs] def make_graph(self, filename=None, format="svg", view=True): """Draw a graph of the project using graphviz Args: filename (str): Name of the output file format (str): Image format (default: 'svg') view (bool): Immediately display the graph [True] """ if filename is None: filename = self.name + ".dot" graph = Graph( self.depends_by_module, filename=filename, format=format, view=view ) graph.draw()
[docs] def remove_ignored_modules(self, ignore_modules=None): """Remove the modules in iterable ignore_modules from all dependencies Args: ignore_modules (iterable of str): module names to ignore (default: iso_c_binding and iso_fortran_env) """ if ignore_modules is None: ignore_modules = [] elif not isinstance(ignore_modules, list): ignore_modules = [ignore_modules] ignored_modules = ignore_modules + DEFAULT_IGNORED_MODULES # Remove from module dict for ignore_mod in map(str.lower, ignored_modules): self.modules.pop(ignore_mod, None) # Remove from 'used' modules for module in self.modules.values(): try: module.uses.remove(ignore_mod) except ValueError: pass # Remove from 'used' files for source_file in self.files.values(): try: source_file.uses.remove(ignore_mod) except ValueError: pass