Source code for default_profile.sphinxext.make

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# {{{
import argparse
import codecs
import locale
import os
import pprint
import shutil
import subprocess
import sys
from pathlib import Path
from typing import TYPE_CHECKING, AnyStr, Dict, List, Optional, Union

# import jinja2
import sphinx
from docutils import nodes
from docutils.core import publish_doctree
from jinja2.environment import Environment
from jinja2.exceptions import TemplateError
from jinja2.ext import autoescape, do, with_

# from jinja2.lexer import get_lexer

# 'FileSystemBytecodeCache': < class 'jinja2.bccache.FileSystemBytecodeCache' >,
from jinja2.loaders import FileSystemLoader

# from jinja2.nodes import EvalContext
# from jinja2.runtime import Context
# from pygments.lexers.markup import MarkdownLexer, RstLexer
from pygments.lexers.python import (
    NumPyLexer,
    PythonConsoleLexer,
    PythonLexer,
    PythonTracebackLexer,
)

# from pygments.lexers.shell import BashLexer
# from pygments.lexers.textedit import VimLexer

# side effect: registers roles and directives
from sphinx import directives  # noqa
from sphinx import roles  # noqa
from sphinx import package_dir
from sphinx.application import Sphinx

# theme_factory = HTMLThemeFactory(self.app)
# Then make this
# from sphinx.builders.html import StandaloneHTMLBuilder

# from sphinx.cmd.build import build_main
# from sphinx.cmd.build import handle_exception
from sphinx.cmd.make_mode import Make
from sphinx.config import Config

# from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment
# from sphinx.environment.adapters.asset import ImageAdapter

# This could be super useful
# from sphinx.environment.adapters.toctree import TocTree
from sphinx.errors import ApplicationError, ConfigError

# , ExtensionError
# from sphinx.events import EventManager, core_events
from sphinx.io import SphinxStandaloneReader # , read_doc
# from sphinx.jinja2glue import BuiltinTemplateLoader, SphinxFileSystemLoader
# from sphinx.locale import __
from sphinx.parsers import RSTParser

# from sphinx.util import import_object, progress_message, rst, status_iterator
# from sphinx.util.build_phase import BuildPhase
# from sphinx.util.console import bold  # type: ignore
# from sphinx.util.docfields import GroupedField

# from sphinx.util.console import (  # type: ignore
#   colorize, color_terminal
# )
from sphinx.util.docutils import docutils_namespace, patch_docutils, sphinx_domains

# from sphinx.util.i18n import CatalogInfo, CatalogRepository, docname_to_domain

# 'CatalogInfo': < class 'sphinx.util.i18n.CatalogInfo' >,
# 'CatalogRepository': < class 'sphinx.util.i18n.CatalogRepository' >,
# this is good to be aware of
# from sphinx.util.inspect import getdoc
from sphinx.util.logging import getLogger

# from sphinx.util.osutil import SEP, ensuredir, relative_uri, relpath

# this is used alongside multiprocessing.connection.Connection
# from sphinx.util.parallel import ParallelTasks
from sphinx.util.parallel import (
    ParallelTasks,
    SerialTasks,
    make_chunks,
    parallel_available,
)
from sphinx.util.tags import Tags

# from sphinx.util.template import ReSTRenderer

try:
    from importlib import metadata
except ImportError:
    import importlib_metadata as metadata


# from sphinx.registry


if TYPE_CHECKING:
    from jinja2.environment import TRIM_BLOCKS, LSTRIP_BLOCKS  # noqa
    from sphinx.domains.rst import ReSTDomain  # noqa
# }}}


logger = getLogger(name=__name__)


def _parse_arguments() -> argparse.ArgumentParser:
    """Parse user arguments.

    Returns
    -------
    user_args : :class:`argparse.NameSpace`
        Argumemts as they've been interpreted by :mod:`argparse`.

    See Also
    --------
    :mod:`docutils.core`
        Shows a few good methods on how to programatically publish docs.

    """
    cmds = [method for method in dir(Runner) if not method.startswith("_")]

    parser = argparse.ArgumentParser(
        prog="Pure Python Makefile",
        description="Dynamic IPython doc builder.",
        epilog="Commands: {}".format(", ".join(cmds)),
    )

    parser.add_argument(
        "builder",
        nargs="?",
        # ugh this shouldn't be independent of DocBuilder.kinds
        choices=[cmds],
        # choices=["html", "singlehtml", "text", "linkcheck", "doctest"],
        metavar="builder",
        help="command to run: {}".format(",\t ".join(cmds)),
    )

    parser.add_argument(
        "-s", "--sourcedir", default=Path.cwd(), help="Sourcedir to pass to Sphinx",
    )

    parser.add_argument(
        "-d", "--destdir", default=None, help="Sphinx Build Dir",
    )

    parser.add_argument(
        "-j",
        "--num-jobs",
        metavar="num-jobs",
        dest="jobs",
        type=int,
        default=os.cpu_count(),
        help="Number of parallel jobs used by `sphinx-build`.",
    )

    parser.add_argument(
        "-b",
        "--open_browser",
        metavar="browser",
        type=bool,
        default=False,
        dest="open_browser",
        help="Toggle opening the docs in the default"
        " browser after a successful build. (boolean)",
    )

    parser.add_argument(
        "-l",
        "--log",
        action="store_false",
        help="Where to write log records to. Defaults to stdout.",
    )

    parser.add_argument(
        "-ll",
        "--log-level",
        dest="log_level",
        default="INFO",
        choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
        help="Log level. If logging is specified, defaults to INFO.",
    )
    # reasonably should mention what the purpose of some of these are.
    # they primarily seem like toggles since they don't provide much else
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_false",
        help="Enable verbose logging and increase level to `debug`.",
    )

    dist = metadata.Distribution().from_name("dynamic_ipython")
    __version__ = dist.version
    parser.add_argument("--version", action="version", version=__version__)

    if len(sys.argv[1:]) == 0:
        parser.print_help()
        # This is actually annoying
        # raise argparse.ArgumentError(None, "Args not provided.")
        sys.exit()

    return parser


[docs]def parse(app: Sphinx, text: str, docname: str = "index") -> nodes.document: """Parse a string as reStructuredText with Sphinx application.""" try: app.env.temp_data["docname"] = docname reader = SphinxStandaloneReader() reader.setup(app) parser = RSTParser() parser.set_application(app) with sphinx_domains(app.env): return publish_doctree( text, path.join(app.srcdir, docname + ".rst"), reader=reader, parser=parser, settings_overrides={"env": app.env, "gettext_compact": True}, ) finally: app.env.temp_data.pop("docname", None)
[docs]class DocBuilder:
[docs] def __init__( self, kind=None, num_jobs=1, verbosity=0, root: Optional[os.PathLike] = None ): if kind is None: kind = "html" self.kind = kind self.num_jobs = num_jobs self.verbosity = verbosity self.root = root if root is not None else os.getcwd()
def __repr__(self): return "{}\t{}".format(self.__class__.__name__, self.kind) def __call__(self, kind): return self.run(kind)
[docs] @staticmethod def status(output): """Print output in bold. Emits ANSI escape sequences to sys.stdout""" print("\033[1m{0}\033[0m".format(output))
[docs] def cleanup(self): """Clean the working tree.""" try: self.status("Removing previous builds…") shutil.rmtree("build") except OSError as e: logger.error(e)
[docs] def sphinx_build(self, kind, BUILD_PATH: Optional[os.PathLike] = None): """Build docs. Examples -------- >>> DocBuilder(num_jobs=4).sphinx_build('html') """ BUILD_PATH = ( BUILD_PATH if BUILD_PATH is not None else Path.cwd().joinpath("build") ) if kind not in self.kinds: raise ValueError( "kind must be one of: {}".format(str(self.kinds)) + "not {}".format(kind) ) cmd = ["sphinx-build", "-b", kind, ".", "-c", self.root] if self.num_jobs: cmd += ["-j", str(self.num_jobs)] if self.verbosity: cmd.append("-{}".format("v" * self.verbosity)) cmd += [ "-d", os.path.join(BUILD_PATH, "doctrees"), os.path.join(BUILD_PATH, kind), ] logger.debug("Cmd is ", cmd) return cmd
[docs] def run(self, kind): """Run :command:`sphinx-build`. The :attr:`check` argument to :func:`subprocess.run()` is not enabled so there's no need to catch :class:`subprocess.CalledProcessError()`. """ # Shit is that really the only time you gotta catch it? self.status("Running sphinx-build.") return ( codecs.decode( subprocess.run( self.sphinx_build(kind), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ).stdout, "utf-8", ).strip(), )
[docs] def open_browser(self, doc: Optional[os.PathLike] = None): """Open a browser tab to the provided document. :param doc: path to index.html :type: str """ if doc is None: doc = "index.html" url = os.path.join("file://", self.root, "build", "html", doc) self.status("Opening path to: {!s}".format(url)) import webbrowser webbrowser.open(url, new=2)
[docs]class Runner: """Initializes a `DocBuilder` and proxies sphinx-build subcommands."""
[docs] def __init__(self, *args, **kwargs): """Initialized with the same args as `DocBuilder`.""" self._builder = DocBuilder(*args, **kwargs)
@property def __builder(self): return self._builder def __repr__(self): return self.__class__.__name__
[docs] def html(self): return self.__builder.run("html")
[docs] def man(self): return self.__builder.run("man")
[docs] def doctest(self): return self.__builder.run("doctest")
[docs] def texinfo(self): return self.__builder.run("texinfo")
[docs] def singlehtml(self): return self.__builder.run("singlehtml")
[docs] def text(self): return self.__builder.run("text")
[docs] def linkcheck(self): return self.__builder.run("linkcheck")
def _run(self, kind): """Proxy to `DocBuilder.run`. Conventionally, methods beginning with :kbd:`_` are module private. This method is not. It's prefixed with a :kbd:`_` to avoid showing up in the epilog of the `argparse.ArgumentParser`. .. seealso:: `_parse_arguments` """ return self.__builder.run(kind) def __call__(self, kind): return self.run(kind)
[docs]def generate_autosummary(**kwargs): from sphinx.ext.autosummary.generate import generate_autosummary_docs generate_autosummary_docs(**kwargs)
[docs]class UserSphinxAdditions:
[docs] def __init__(self, sphinx: Sphinx): self.sphinx = sphinx
[docs] @classmethod def additions(self, method, **kwargs): if not hasattr(self.sphinx, "method"): raise AttributeError getattr(self.sphinx, "method")(**kwargs)
[docs]def generate_sphinx_app( root: os.PathLike, namespace: Optional[argparse.Namespace] = None ): """Generate the primary Sphinx application object that drives the project. Parameters ---------- root : str or pathlib.Path The root of the repositories docs namespace : arparse.NameSpace User provided arments. Returns ------- app : Sphinx :class:`sphinx.application.Sphinx` instance. """ srcdir = confdir = Path(root).joinpath("source") doctreedir = "build/.doctrees" outdir = "build/html" if hasattr(namespace, "verbosity"): if namespace.verbosity < 20: verbosity = 2 elif namespace.verbosity < 40: verbosity = 1 else: verbosity = 0 else: verbosity = 0 try: with patch_docutils(confdir), docutils_namespace(): app = Sphinx( buildername=namespace.builder if namespace.builder is not None else "html", srcdir=srcdir, outdir=outdir, doctreedir=doctreedir, confdir=confdir, parallel=namespace.jobs, verbosity=verbosity, ) app.build() return app.statuscode except ApplicationError: raise except (Exception, KeyboardInterrupt) as exc: # handle_exception(app, namespace, exc) pprint.pprint(exc) return 2
[docs]def get_jinja_loader(template_path: Union[str, os.PathLike]) -> FileSystemLoader: if template_path is None: template_path = "_templates" elif isinstance(template_path, Path): template_path = [template_path.iterdir()] try: loader = FileSystemLoader(template_path, followlinks=True) except TemplateError: return return loader
[docs]def setup_jinja(path_to_template: Union[str, os.PathLike]) -> Environment: """Use jinja to set up the Sphinx environment.""" TRIM_BLOCKS = True LSTRIP_BLOCKS = True loader = get_jinja_loader(path_to_template) env = Environment( trim_blocks=TRIM_BLOCKS, lstrip_blocks=LSTRIP_BLOCKS, loader=loader, extensions=["jinja2.ext.i18n", autoescape, do, with_], enable_async=True, ) return env
[docs]class Maker(Make):
[docs] def __init__( self, source_dir: os.PathLike, build_dir: os.PathLike, builder: List, *args: List, **kwargs: Dict, ): super().__init__(source_dir, build_dir, builder, *args) self.source_dir = source_dir self.build_dir = build_dir
[docs] def run( self, builder: Union[Optional[List[AnyStr]], Optional[AnyStr]] = None, *args, **kwargs, ): """Run sphinx-build with as many options given reasonable defaults as possible.""" if builder is None: builder = [ "html" ] # list required if no args provided as sphinx concatenates the builder with the other args super().run_generic_build(builder, *args, **kwargs)
def __repr__(self): return self.__class__.__name__
[docs]def create_sphinx_config(confdir: os.PathLike): """Create a sphinx.config.Config object which isn't utilized currently. Simply here as a thorough method to check for syntax errors. """ tags = Tags() try: return Config.read(confdir, tags) except ConfigError: print(*sys.exc_info())
[docs]def main(repo_root: os.PathLike, argv=None): """Create the required objects to simulate the sphinx make-main command. Create a `jinja2.Environment`, a `sphinx.project.Project`, `sphinx.jinja2glue.SphinxFileSystemLoader`. """ if argv is None: argv = sys.argv[1:] user_parser = _parse_arguments() user_args = user_parser.parse_args(argv) doc_root = Path(repo_root).joinpath("docs") # conf_path = doc_root.joinpath("source") sphinx.locale.setlocale(locale.LC_ALL, "") sphinx.locale.init_console(os.path.join(package_dir, "locale"), "sphinx") # config = create_sphinx_config(conf_path) template_path = doc_root.joinpath("source/_templates") env = setup_jinja(template_path) if user_args.log: logger.setLevel(30) logger.info(f"git root was: {repo_root}") logger.info(f"jinja env: {env}") generate_sphinx_app(doc_root, user_args)
[docs]def get_git_root() -> Path: try: almost = codecs.decode( subprocess.check_output(["git", "rev-parse", "--show-toplevel"]), "utf-8" ) return Path(almost.rstrip()) except subprocess.CalledProcessError as e: print(e) return Path.cwd()
if __name__ == "__main__": git_root = get_git_root() sys.exit(main(git_root))