Skip to content
Snippets Groups Projects
sourcelink.py 10.3 KiB
Newer Older
from __future__ import (absolute_import, division, print_function)
from six import iteritems
import mantid
from .base import AlgorithmBaseDirective #pylint: disable=unused-import

from mantiddoc.tools.git_last_modified import get_file_last_modified_time


class SourceLinkError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return str(self.value)
class SourceLinkDirective(AlgorithmBaseDirective):
    """
    Obtains the github links to the .cpp and .h or .py files depending on the
    name and version.
    Overriding the filename for searching:
    .. sourcelink::
        :filename: SINQTranspose3D
    suppressing sanity checks:
    .. sourcelink::
        :sanity_checks: 0

    specifying specific files
    (paths should use / and start from but not include the Mantid directory):
    .. sourcelink::
         :h: Framework/ICat/inc/MantidICat/CatalogSearch.h
         :cpp: Framework/ICat/src/CatalogSearch.cpp
    suppressing a specific file type (None is case insensitive):
    .. sourcelink::
      :filename: FilterEventsByLogValuePreNexus
      :py: None
    """

    required_arguments, optional_arguments = 0, 0
    option_spec = {
        "filename": str,
        "sanity_checks": int,
        "cpp": str,
        "h": str,
        "py": str
    }
    #IMPORTANT: keys must match the option spec above
    # - apart from filename and sanity_checks
    file_types = {
        "h": "C++ header",
        "cpp": "C++ source",
        "py": "Python"
    }

    # will be filled in

    def execute(self):
        """
        Called by Sphinx when the ..sourcelink:: directive is encountered.
        """
        file_paths = {}
        error_string = ""
        sanity_checks = self.options.get("sanity_checks", 1)
        file_name = self.options.get("filename", None)
            # build a sensible default
            file_name = self.algorithm_name()
            if (self.algorithm_version() != 1) and (self.algorithm_version() is not None):
                file_name += str(self.algorithm_version())
        for extension in self.file_types.keys():
            file_paths[extension] = self.options.get(extension, None)
            if file_paths[extension] is None:
                try:
                    fname = self.find_source_file(file_name, extension)
                    file_paths[extension] = (
                            fname, get_file_last_modified_time(self.git_cache, self.source_root, fname)) \
                            if fname is not None else None
                except SourceLinkError as err:
                    error_string += str(err) + "\n"
            elif file_paths[extension].lower() == "none":
                # the users has specifically chosen to suppress this - set it to a "proper" None
                # but do not search for this file
            else:
                # prepend the base framework directory
                fname = os.path.join(self.source_root, file_paths[extension])
                file_paths[extension] = (fname, get_file_last_modified_time(self.git_cache, self.source_root, fname))
                if not os.path.exists(file_paths[extension][0]):
                    error_string += "Cannot find {} file at {}\n".format(
                        extension, file_paths[extension][0])

        # throw accumulated errors now if you have any
            raise SourceLinkError(error_string)

        self.output_to_page(file_paths, file_name, sanity_checks)

        return []

    def find_source_file(self, file_name, extension):
        """
        Searches the source code for a matching filename with the correct extension
        """
        # parse the source tree if it has not already been done
        if not self.file_lookup:
            self.parse_source_tree()

        try:
            path_list = self.file_lookup[file_name][extension]
            if len(path_list) == 1:
                return path_list[0]
            else:
                suggested_path = "os_agnostic_path_to_file_from_source_root"
Peterson, Peter's avatar
Peterson, Peter committed
                    suggested_path = path_list[0].replace(self.source_root, "")
                raise SourceLinkError("Found multiple possibilities for " +
                                      file_name + "." + extension + "\n" +
                                      "Possible matches" +  str(path_list) +
                                      "\n" +
                                      "Specify one using the " + extension +
                                      " option\n" +
                                      "e.g. \n" +
                                      ".. sourcelink:\n" +
                                      "      :" + extension + ": " + suggested_path)
            return self.file_lookup[file_name][extension]
        except KeyError:
            # value is not present
    @property
    def source_root(self):
        returns the root source directory
        if self.__source_root is None:
            env = self.state.document.settings.env
            direc = env.srcdir #= C:\Mantid\Code\Mantid\docs\source
            direc = os.path.join(direc, "..", "..") # assume root is two levels up
            direc = os.path.abspath(direc)
            self.__source_root = direc #pylint: disable=protected-access
        return self.__source_root

    def parse_source_tree(self):
        """
        Fills the file_lookup dictionary after parsing the source code
        env = self.state.document.settings.env
        builddir = env.doctreedir # there should be a better setting option
        builddir = os.path.join(builddir, "..", "..")
        builddir = os.path.abspath(builddir)

        for dir_name, _, file_list in os.walk(self.source_root):
            if dir_name.startswith(builddir):
                continue # don't check or add to the cache
            for fname in file_list:
                (base_name, file_extensions) = os.path.splitext(fname)
                #strip the dot from the extension
                file_extensions = file_extensions[1:]
                #build the data object that is e.g.
                #file_lookup["Rebin2"]["cpp"] = ['C:\Mantid\Code\Mantid\Framework\Algorithms\src\Rebin2.cpp','possible other location']
                if file_extensions in self.file_types.keys():
                    if base_name not in self.file_lookup.keys():
                        self.file_lookup[base_name] = {}
                    if file_extensions not in self.file_lookup[base_name].keys():
                        self.file_lookup[base_name][file_extensions] = []
                    self.file_lookup[base_name][file_extensions].append(
                        os.path.join(dir_name, fname))

    def output_to_page(self, file_paths, file_name, sanity_checks):
        """
        Outputs the sourcelinks and heading to the rst page
        and performs some sanity checks
        """
        valid_ext_list = []
        self.add_rst(self.make_header("Source"))
        for extension, filepath in iteritems(file_paths):
                self.output_path_to_page(filepath, extension)
                valid_ext_list.append(extension)
        # do some sanity checks - unless suppressed
        if sanity_checks > 0:
            suggested_path = "os_agnostic_path_to_file_from_Code/Mantid"
            if not valid_ext_list:
                raise SourceLinkError("No file possibilities for " + file_name + " have been found\n" +
                                      "Please specify a better one using the :filename: opiton or use the " +
                                      str(list(self.file_types.keys())) + " options\n" +
                                      "e.g. \n" +
                                      ".. sourcelink:\n" +
                                      "      :" + list(self.file_types.keys())[0] + ": " + suggested_path + "\n "+
                                      "or \n" +
                                      ".. sourcelink:\n" +
                                      "      :filename: " + file_name)

            # if the have a cpp we should also have a h
            if ("cpp" in valid_ext_list) ^ ("h" in valid_ext_list):
                raise SourceLinkError("Only one of .h and .cpp found for " + file_name + "\n" +
                                      "valid files found for " + str(valid_ext_list) + "\n" +
                                      "Please specify the missing one using an " +
                                      str(list(self.file_types.keys())) + " option\n" +
                                      "e.g. \n" +
                                      ".. sourcelink:\n" +
                                      "      :" + list(self.file_types.keys())[0] + ": " + suggested_path)
    def output_path_to_page(self, filepath, extension):
        """
        Outputs the source link for a file to the rst page
        """
        _, f_name = os.path.split(filepath[0])
        self.add_rst("{}: `{} <{}>`_ *(last modified: {})*\n\n".format(
            self.file_types[extension],
            f_name,
            self.convert_path_to_github_url(filepath[0]),
            filepath[1]
        ))
    def convert_path_to_github_url(self, file_path):
        """
        Converts a file path to the github url for that same file
        example path C:\Mantid\Code\Mantid/Framework/Algorithms/inc/MantidAlgorithms/MergeRuns.h
        example url  https://github.com/mantidproject/mantid/blob/master/Code/Mantid/Framework/Algorithms/inc/MantidAlgorithms/MergeRuns.h
        """
        # remove the directory path
        url = url.replace(self.source_root, "")
        # harmonize slashes
        url = url.replace("\\", "/")
        # prepend the github part
        if not url.startswith("/"):
        url = "https://github.com/mantidproject/mantid/blob/" + mantid.kernel.revision_full() + url
def setup(app):
    """
    Setup the directives when the extension is activated

    Args:
      app: The main Sphinx application object
    """
    app.add_directive('sourcelink', SourceLinkDirective)