Example #1
0
def compare_frozen(app, exception):
    """
    Compare backed up frozen RST files from ``backup_frozen()`` with 
    newly frozen RST. If they don't match within accepted tolerances, 
    raise an exception to fail the current CI job. This implies that 
    the OCIO API changed without building with CMake option 
    ``-DOCIO_BUILD_FROZEN_DOCS=ON``; needed to update frozen RST in the 
    source tree.
    """
    # Raise Sphinx exceptions for debugging
    if exception:
        raise ExtensionError(str(exception))

    if not app.config.frozendoc_compare:
        return

    logger.info("Comparing frozen RST: {src} <-> {dst}\n".format(
        src=PYTHON_FROZEN_DIR, dst=PYTHON_BACKUP_DIR))

    frozen_files = set(os.listdir(PYTHON_FROZEN_DIR))
    backup_files = set(os.listdir(PYTHON_BACKUP_DIR))

    # Find any files which are different, or only present in one of the two
    # directories.
    match, mismatch, errors = filecmp.cmpfiles(PYTHON_FROZEN_DIR,
                                               PYTHON_BACKUP_DIR,
                                               list(frozen_files
                                                    | backup_files),
                                               shallow=False)

    # Different OSs or compilers may result in slightly different signatures
    # or types. Test each mismatched file for the ratio of how different
    # their contents are. If they have a difference ratio at or above 0.6 (the
    # recommended ratio to determine close matches), assume the differences are
    # ok. This is certainly not a water tight assumption, but deemed (at the
    # moment) preferrable to failing on every mior difference. Keep track of
    # these ignored differences and report them for transparency in CI logs.
    ignored = []

    for i in reversed(range(len(mismatch))):
        filename = mismatch[i]
        logger.info("Difference found in: {}".format(filename))

        frozen_path = os.path.join(PYTHON_FROZEN_DIR, filename)
        with open(frozen_path, "r") as frozen_file:
            frozen_data = frozen_file.read()

        backup_path = os.path.join(PYTHON_BACKUP_DIR, filename)
        with open(backup_path, "r") as backup_file:
            backup_data = backup_file.read()

        for line in difflib.unified_diff(frozen_data.splitlines(),
                                         backup_data.splitlines(),
                                         fromfile=frozen_path,
                                         tofile=backup_path):
            logger.info(line)

        # Based on difflib's caution of argument order playing a role in the
        # results of ratio(), check the ratio in both directions and use the
        # better of the two as our heuristic.
        match_ab = difflib.SequenceMatcher(None, frozen_data, backup_data)
        match_ba = difflib.SequenceMatcher(None, backup_data, frozen_data)
        max_ratio = max(match_ab.ratio(), match_ba.ratio())

        if max_ratio >= 0.6:
            ignored.append(mismatch.pop(i))
            logger.info(
                "Difference ratio {} is within error tolerances\n".format(
                    max_ratio))
        else:
            logger.error(
                "Difference ratio {} exceeds error tolerances\n".format(
                    max_ratio))

    if os.path.exists(PYTHON_BACKUP_DIR):
        shutil.rmtree(PYTHON_BACKUP_DIR)

    if mismatch or errors:
        raise ExtensionError(
            "Frozen RST is out of date! Build OpenColorIO with CMake option "
            "'-DOCIO_BUILD_FROZEN_DOCS=ON' to update required frozen "
            "documentation source files in: {dir}\n\n"
            "    Changed files: {changed}\n\n"
            "      Added files: {added}\n\n"
            "    Removed files: {removed}\n\n"
            "See log for changed file differences.\n".format(
                dir=PYTHON_FROZEN_DIR,
                changed=", ".join(mismatch),
                added=", ".join(f for f in errors if f in frozen_files),
                removed=", ".join(f for f in errors if f in backup_files),
            ))

    # Report ignored differences
    if ignored:
        logger.warning(
            "Some differences were found and ignored when comparing frozen "
            "RST to current documentation source in: {dir}\n\n"
            "    Changed files: {changed}\n\n"
            "See log for changed file differences.\n".format(
                dir=PYTHON_FROZEN_DIR,
                changed=", ".join(ignored),
            ))

    logger.info("Frozen RST matches within error tolerances.")
Example #2
0
 def add_role_to_domain(self, domain, name, role):
     self.debug('[app] adding role to domain: %r', (domain, name, role))
     if domain not in self.domains:
         raise ExtensionError('domain %s not yet registered' % domain)
     self.domains[domain].roles[name] = role
Example #3
0
 def _validate_event(self, event):
     event = intern(event)
     if event not in self._events:
         raise ExtensionError('Unknown event name: %s' % event)
Example #4
0
 def add(self, name, default, rebuild, types):
     # type: (unicode, Any, Union[bool, unicode], Any) -> None
     if name in self.values:
         raise ExtensionError(__('Config value %r already present') % name)
     else:
         self.values[name] = (default, rebuild, types)
Example #5
0
 def add(self, name: str) -> None:
     """Register a custom Sphinx event."""
     if name in self.events:
         raise ExtensionError(__('Event %r already present') % name)
     self.events[name] = ''
Example #6
0
 def _validate_event(self, event):
     # type: (unicode) -> None
     if event not in self._events:
         raise ExtensionError('Unknown event name: %s' % event)
Example #7
0
def notation_to_string(notation):
    """Parse notation and format it as a string with ellipses."""
    try:
        return stringify_with_ellipses(notation)
    except ParseError as e:
        raise ExtensionError(PARSE_ERROR.format(notation, e.msg)) from e
Example #8
0
            mod.setup(self)
        self._extensions[extension] = mod

    def import_object(self, objname, source=None):
        """Import an object from a 'module.name' string."""
        try:
            module, name = objname.rsplit('.', 1)
        except ValueError, err:
            raise ExtensionError(
                'Invalid full object name %s' % objname +
                (source and ' (needed for %s)' % source or ''), err)
        try:
            return getattr(__import__(module, None, None, [name]), name)
        except ImportError, err:
            raise ExtensionError(
                'Could not import %s' % module +
                (source and ' (needed for %s)' % source or ''), err)
        except AttributeError, err:
            raise ExtensionError(
                'Could not find %s' % objname +
                (source and ' (needed for %s)' % source or ''), err)

    # event interface

    def _validate_event(self, event):
        event = intern(event)
        if event not in self._events:
            raise ExtensionError('Unknown event name: %s' % event)

    def connect(self, event, callback):
        self._validate_event(event)
Example #9
0
 def add_config_value(self, name, default, rebuild):
     if name in self.config.values:
         raise ExtensionError('Config value %r already present' % name)
     if rebuild in (False, True):
         rebuild = rebuild and 'env' or ''
     self.config.values[name] = (default, rebuild)
Example #10
0
def save_thumbnail(image_path_template, src_file, script_vars, file_conf,
                   gallery_conf):
    """Generate and Save the thumbnail image

    Parameters
    ----------
    image_path_template : str
        holds the template where to save and how to name the image
    src_file : str
        path to source python file
    script_vars : dict
        Configuration and run time variables
    file_conf : dict
        File-specific settings given in source file comments as:
        ``# sphinx_gallery_<name> = <value>``
    gallery_conf : dict
        Sphinx-Gallery configuration dictionary
    """

    thumb_dir = os.path.join(os.path.dirname(image_path_template), 'thumb')
    if not os.path.exists(thumb_dir):
        os.makedirs(thumb_dir)

    # read specification of the figure to display as thumbnail from main text
    thumbnail_number = file_conf.get('thumbnail_number', None)
    thumbnail_path = file_conf.get('thumbnail_path', None)
    # thumbnail_number has priority.
    if thumbnail_number is None and thumbnail_path is None:
        # If no number AND no path, set to default thumbnail_number
        thumbnail_number = 1
    if thumbnail_number is None:
        image_path = os.path.join(gallery_conf['src_dir'], thumbnail_path)
    else:
        if not isinstance(thumbnail_number, int):
            raise ExtensionError(
                'sphinx_gallery_thumbnail_number setting is not a number, '
                'got %r' % (thumbnail_number,))
        # negative index means counting from the last one
        if thumbnail_number < 0:
            thumbnail_number += len(script_vars["image_path_iterator"]) + 1
        image_path = image_path_template.format(thumbnail_number)
    del thumbnail_number, thumbnail_path, image_path_template
    thumbnail_image_path, ext = _find_image_ext(image_path)

    base_image_name = os.path.splitext(os.path.basename(src_file))[0]
    thumb_file = os.path.join(thumb_dir,
                              'sphx_glr_%s_thumb.%s' % (base_image_name, ext))

    if src_file in gallery_conf['failing_examples']:
        img = os.path.join(glr_path_static(), 'broken_example.png')
    elif os.path.exists(thumbnail_image_path):
        img = thumbnail_image_path
    elif not os.path.exists(thumb_file):
        # create something to replace the thumbnail
        img = gallery_conf.get("default_thumb_file")
        if img is None:
            img = os.path.join(glr_path_static(), 'no_image.png')
    else:
        return
    if ext in ('svg', 'gif'):
        copyfile(img, thumb_file)
    else:
        scale_image(img, thumb_file, *gallery_conf["thumbnail_size"])
        if 'thumbnails' in gallery_conf['compress_images']:
            optipng(thumb_file, gallery_conf['compress_images_args'])
Example #11
0
def _check_input(prompt=None):
    raise ExtensionError(
        'Cannot use input() builtin function in Sphinx-Gallery examples')
Example #12
0
def check_config(app):
    if not app.config.googleanalytics_id:
        raise ExtensionError(
            "'googleanalytics_id' config value must be set for ga statistics to function properly."
        )
Example #13
0
    def add_node(self, node, **kwds):
        # type: (nodes.Node, Any) -> None
        """Register a Docutils node class.

        This is necessary for Docutils internals.  It may also be used in the
        future to validate nodes in the parsed documents.

        Node visitor functions for the Sphinx HTML, LaTeX, text and manpage
        writers can be given as keyword arguments: the keyword should be one or
        more of ``'html'``, ``'latex'``, ``'text'``, ``'man'``, ``'texinfo'``
        or any other supported translators, the value a 2-tuple of ``(visit,
        depart)`` methods.  ``depart`` can be ``None`` if the ``visit``
        function raises :exc:`docutils.nodes.SkipNode`.  Example:

        .. code-block:: python

           class math(docutils.nodes.Element): pass

           def visit_math_html(self, node):
               self.body.append(self.starttag(node, 'math'))
           def depart_math_html(self, node):
               self.body.append('</math>')

           app.add_node(math, html=(visit_math_html, depart_math_html))

        Obviously, translators for which you don't specify visitor methods will
        choke on the node when encountered in a document to translate.

        .. versionchanged:: 0.5
           Added the support for keyword arguments giving visit functions.
        """
        logger.debug('[app] adding node: %r', (node, kwds))
        if not kwds.pop('override', False) and \
           hasattr(nodes.GenericNodeVisitor, 'visit_' + node.__name__):
            logger.warning(__('while setting up extension %s: node class %r is '
                              'already registered, its visitors will be overridden'),
                           self._setting_up_extension, node.__name__,
                           type='app', subtype='add_node')
        nodes._add_node_class_names([node.__name__])
        for key, val in iteritems(kwds):
            try:
                visit, depart = val
            except ValueError:
                raise ExtensionError(__('Value for key %r must be a '
                                        '(visit, depart) function tuple') % key)
            translator = self.registry.translators.get(key)
            translators = []
            if translator is not None:
                translators.append(translator)
            elif key == 'html':
                from sphinx.writers.html import HTMLTranslator
                translators.append(HTMLTranslator)
                if is_html5_writer_available():
                    from sphinx.writers.html5 import HTML5Translator
                    translators.append(HTML5Translator)
            elif key == 'latex':
                from sphinx.writers.latex import LaTeXTranslator
                translators.append(LaTeXTranslator)
            elif key == 'text':
                from sphinx.writers.text import TextTranslator
                translators.append(TextTranslator)
            elif key == 'man':
                from sphinx.writers.manpage import ManualPageTranslator
                translators.append(ManualPageTranslator)
            elif key == 'texinfo':
                from sphinx.writers.texinfo import TexinfoTranslator
                translators.append(TexinfoTranslator)

            for translator in translators:
                setattr(translator, 'visit_' + node.__name__, visit)
                if depart:
                    setattr(translator, 'depart_' + node.__name__, depart)
Example #14
0
 def add_enumerable_node(self, node, figtype, title_getter=None, override=False):
     # type: (nodes.Node, unicode, TitleGetter, bool) -> None
     logger.debug('[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter)
     if node in self.enumerable_nodes and not override:
         raise ExtensionError(__('enumerable_node %r already registered') % node)
     self.enumerable_nodes[node] = (figtype, title_getter)
Example #15
0
 def add_source_parser(self, suffix, parser):
     # type: (unicode, Type[Parser]) -> None
     logger.debug('[app] adding search source_parser: %r, %r', suffix, parser)
     if suffix in self.source_parsers:
         raise ExtensionError(__('source_parser for %r is already registered') % suffix)
     self.source_parsers[suffix] = parser
Example #16
0
 def add_event(self, name):
     if name in self._events:
         raise ExtensionError('Event %r already present' % name)
     self._events[name] = ''
Example #17
0
def pymol_scraper(block, block_vars, gallery_conf):
    # Search for comment line containing 'Visualization with PyMOL...'
    _, code, _ = block
    if any([
            line.strip() == "# Visualization with PyMOL..."
            for line in code.splitlines()
    ]):
        pymol_script_path = splitext(block_vars["src_file"])[0] + "_pymol.py"
        # The rendered image will be created in the same directory as
        # the example script
        # -> the image will be included in version control
        # -> Rendering with PyMOL is not necessary for building the docs
        pymol_image_path = splitext(block_vars["src_file"])[0] + ".png"
        if not isfile(pymol_script_path):
            raise ExtensionError(
                f"'{block_vars['src_file']}' has no corresponding "
                f"'{pymol_script_path}' file")

        # If PyMOL image is already created, do not run PyMOL script,
        # as this should not be required for building the documentation
        if not isfile(pymol_image_path):
            # Create a shallow copy,
            # to avoid ading new variables to example script
            script_globals = copy.copy(block_vars["example_globals"])
            script_globals["__image_destination__"] = pymol_image_path

            try:
                import pymol
            except ImportError:
                raise ExtensionError("PyMOL is not installed")
            try:
                import ammolite
            except ImportError:
                raise ExtensionError("Ammolite is not installed")
            with open(pymol_script_path, "r") as script:
                # Prevent PyMOL from writing stuff (splash screen, etc.)
                # to STDOUT or STDERR
                # -> Save original STDOUT/STDERR and point them
                # temporarily to DEVNULL
                dev_null = open(os.devnull, 'w')
                orig_stdout = sys.stdout
                orig_stderr = sys.stderr
                sys.stdout = dev_null
                sys.stderr = dev_null
                try:
                    exec(script.read(), script_globals)
                except Exception as e:
                    raise ExtensionError(
                        f"PyMOL script raised a {type(e).__name__}: {str(e)}")
                finally:
                    # Restore STDOUT/STDERR
                    sys.stdout = orig_stdout
                    sys.stderr = orig_stderr
                    dev_null.close()
            if not isfile(pymol_image_path):
                raise ExtensionError("PyMOL script did not create an image "
                                     "(at expected location)")

        # Copy the images into the 'gallery' directory under a canonical
        # sphinx-gallery name
        image_path_iterator = block_vars['image_path_iterator']
        image_destination = image_path_iterator.next()
        shutil.copy(pymol_image_path, image_destination)
        return figure_rst([image_destination], gallery_conf['src_dir'])

    else:
        return figure_rst([], gallery_conf['src_dir'])
Example #18
0
 def add_domain(self, domain: "Type[Domain]", override: bool = False) -> None:
     logger.debug('[app] adding domain: %r', domain)
     if domain.name in self.domains and not override:
         raise ExtensionError(__('domain %s already registered') % domain.name)
     self.domains[domain.name] = domain
Example #19
0
 def add(self, name: str, default: Any, rebuild: Union[bool, str],
         types: Any) -> None:
     if name in self.values:
         raise ExtensionError(__('Config value %r already present') % name)
     else:
         self.values[name] = (default, rebuild, types)
Example #20
0
 def add_source_suffix(self, suffix: str, filetype: str, override: bool = False) -> None:
     logger.debug('[app] adding source_suffix: %r, %r', suffix, filetype)
     if suffix in self.source_suffix and not override:
         raise ExtensionError(__('source_suffix %r is already registered') % suffix)
     else:
         self.source_suffix[suffix] = filetype
Example #21
0
 def add_source_input(self, filetype, input_class):
     # type: (unicode, Type[Input]) -> None
     if filetype in self.source_inputs:
         raise ExtensionError(
             __('source_input for %r is already registered') % filetype)
     self.source_inputs[filetype] = input_class
Example #22
0
 def add_translator(self, name: str, translator: "Type[nodes.NodeVisitor]",
                    override: bool = False) -> None:
     logger.debug('[app] Change of translator for the %s builder.', name)
     if name in self.translators and not override:
         raise ExtensionError(__('Translator for %r already exists') % name)
     self.translators[name] = translator
Example #23
0
def run_autoapi(app):  # pylint: disable=too-many-branches
    """
    Load AutoAPI data from the filesystem.
    """
    if app.config.autoapi_type not in LANGUAGE_MAPPERS:
        raise ExtensionError(
            "Invalid autoapi_type setting, "
            "following values is allowed: {}".format(", ".join(
                '"{}"'.format(api_type)
                for api_type in sorted(LANGUAGE_MAPPERS))))

    if not app.config.autoapi_dirs:
        raise ExtensionError("You must configure an autoapi_dirs setting")

    if app.config.autoapi_include_summaries is not None:
        warnings.warn(
            "autoapi_include_summaries has been replaced by "
            "the show-module-summary AutoAPI option\n",
            RemovedInAutoAPI2Warning,
        )
        if app.config.autoapi_include_summaries:
            app.config.autoapi_options.append("show-module-summary")

    # Make sure the paths are full
    normalised_dirs = _normalise_autoapi_dirs(app.config.autoapi_dirs,
                                              app.srcdir)
    for _dir in normalised_dirs:
        if not os.path.exists(_dir):
            raise ExtensionError(
                "AutoAPI Directory `{dir}` not found. "
                "Please check your `autoapi_dirs` setting.".format(dir=_dir))

    normalized_root = os.path.normpath(
        os.path.join(app.srcdir, app.config.autoapi_root))
    url_root = os.path.join("/", app.config.autoapi_root)

    if not all(import_name in sys.modules for _, import_name in
               LANGUAGE_REQUIREMENTS[app.config.autoapi_type]):
        raise ExtensionError(
            "AutoAPI of type `{type}` requires following "
            "packages to be installed and included in extensions list: "
            "{packages}".format(
                type=app.config.autoapi_type,
                packages=", ".join(
                    '{import_name} (available as "{pkg_name}" on PyPI)'.format(
                        pkg_name=pkg_name, import_name=import_name)
                    for pkg_name, import_name in LANGUAGE_REQUIREMENTS[
                        app.config.autoapi_type]),
            ))

    sphinx_mapper = LANGUAGE_MAPPERS[app.config.autoapi_type]
    template_dir = app.config.autoapi_template_dir
    if template_dir and not os.path.isabs(template_dir):
        if not os.path.isdir(template_dir):
            template_dir = os.path.join(app.srcdir,
                                        app.config.autoapi_template_dir)
        elif app.srcdir != os.getcwd():
            warnings.warn(
                "autoapi_template_dir will be expected to be "
                "relative to the Sphinx source directory instead of "
                "relative to where sphinx-build is run\n",
                RemovedInAutoAPI2Warning,
            )
    sphinx_mapper_obj = sphinx_mapper(app,
                                      template_dir=template_dir,
                                      url_root=url_root)

    if app.config.autoapi_file_patterns:
        file_patterns = app.config.autoapi_file_patterns
    else:
        file_patterns = DEFAULT_FILE_PATTERNS.get(app.config.autoapi_type, [])

    if app.config.autoapi_ignore:
        ignore_patterns = app.config.autoapi_ignore
    else:
        ignore_patterns = DEFAULT_IGNORE_PATTERNS.get(app.config.autoapi_type,
                                                      [])

    if ".rst" in app.config.source_suffix:
        out_suffix = ".rst"
    elif ".txt" in app.config.source_suffix:
        out_suffix = ".txt"
    else:
        # Fallback to first suffix listed
        out_suffix = next(iter(app.config.source_suffix))

    if sphinx_mapper_obj.load(patterns=file_patterns,
                              dirs=normalised_dirs,
                              ignore=ignore_patterns):
        sphinx_mapper_obj.map(options=app.config.autoapi_options)

        if app.config.autoapi_generate_api_docs:
            sphinx_mapper_obj.output_rst(root=normalized_root,
                                         source_suffix=out_suffix)
Example #24
0
 def add_enumerable_node(self, node: "Type[Node]", figtype: str,
                         title_getter: TitleGetter = None, override: bool = False) -> None:
     logger.debug('[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter)
     if node in self.enumerable_nodes and not override:
         raise ExtensionError(__('enumerable_node %r already registered') % node)
     self.enumerable_nodes[node] = (figtype, title_getter)
Example #25
0
 def add_domain(self, domain):
     self.debug('[app] adding domain: %r', domain)
     if domain.name in self.domains:
         raise ExtensionError('domain %s already registered' % domain.name)
     self.domains[domain.name] = domain
Example #26
0
 def add_translator(self, name, translator, override=False):
     # type: (unicode, Type[nodes.NodeVisitor], bool) -> None
     logger.debug('[app] Change of translator for the %s builder.' % name)
     if name in self.translators and not override:
         raise ExtensionError(__('Translator for %r already exists') % name)
     self.translators[name] = translator
Example #27
0
 def add_index_to_domain(self, domain, index):
     self.debug('[app] adding index to domain: %r', (domain, index))
     if domain not in self.domains:
         raise ExtensionError('domain %s not yet registered' % domain)
     self.domains[domain].indices.append(index)
Example #28
0
 def add_domain(self, domain):
     # type: (Type[Domain]) -> None
     logger.debug('[app] adding domain: %r', domain)
     if domain.name in self.domains:
         raise ExtensionError(__('domain %s already registered') % domain.name)
     self.domains[domain.name] = domain
Example #29
0
 def add_event(self, name):
     self.debug('[app] adding event: %r', name)
     if name in self._events:
         raise ExtensionError('Event %r already present' % name)
     self._events[name] = ''
Example #30
0
 def add_index_to_domain(self, domain, index):
     # type: (unicode, Type[Index]) -> None
     logger.debug('[app] adding index to domain: %r', (domain, index))
     if domain not in self.domains:
         raise ExtensionError(_('domain %s not yet registered') % domain)
     self.domains[domain].indices.append(index)