コード例 #1
0
ファイル: plugin.py プロジェクト: Holt59/mkdocstrings
    def on_config(self, config: Config, **kwargs) -> Config:
        """
        Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config).

        In this hook, we instantiate our [`MkdocstringsExtension`][mkdocstrings.extension.MkdocstringsExtension]
        and add it to the list of Markdown extensions used by `mkdocs`.

        We pass this plugin's configuration dictionary to the extension when instantiating it (it will need it
        later when processing markdown to get handlers and their global configurations).
        """
        if not config["site_url"]:
            log.error(
                "mkdocstrings.plugin: configuration item 'site_url' is required for cross-references"
            )

        log.debug("mkdocstrings.plugin: Adding extension to the list")

        extension_config = dict(
            theme_name=config["theme"].name,
            mdx=config["markdown_extensions"],
            mdx_configs=config["mdx_configs"],
            mkdocstrings=self.config,
        )

        self.mkdocstrings_extension = MkdocstringsExtension(
            config=extension_config)
        config["markdown_extensions"].append(self.mkdocstrings_extension)
        return config
コード例 #2
0
    def on_post_build(self, config: Config, **kwargs) -> None:
        """
        Hook for the [`on_post_build` event](https://www.mkdocs.org/user-guide/plugins/#on_post_build).

        This hook is used to teardown all the handlers that were instantiated and cached during documentation buildup.

        For example, the [Python handler's collector][mkdocstrings.handlers.python.PythonCollector] opens a subprocess
        in the background and keeps it open to feed it the "autodoc" instructions and get back JSON data. Therefore,
        it must close it at some point, and it does it in its
        [`teardown()` method][mkdocstrings.handlers.python.PythonCollector.teardown] which is indirectly called by
        this hook.
        """
        log.debug("mkdocstrings.plugin: Tearing handlers down")
        teardown()
コード例 #3
0
    def on_page_content(self, html: str, page: Page, config: Config,
                        files: Files, **kwargs) -> str:
        """
        Hook for the [`on_page_contents` event](https://www.mkdocs.org/user-guide/plugins/#on_page_contents).

        In this hook, we map the IDs of every anchor found in the table of contents to the anchors absolute URLs.
        This mapping will be used later to fix unresolved reference of the form `[title][identifier]` or
        `[identifier][]`.
        """
        log.debug(
            f"mkdocstrings.plugin: Mapping identifiers to URLs for page {page.file.src_path}"
        )
        for item in page.toc.items:
            self.map_urls(page.canonical_url, item)
        return html
コード例 #4
0
    def on_serve(self, server: Server, config: Config, **kwargs) -> Server:
        """
        Hook for the [`on_serve` event](https://www.mkdocs.org/user-guide/plugins/#on_serve).

        In this hook, we add the directories specified in the plugin's configuration to the list of directories
        watched by `mkdocs`. Whenever a change occurs in one of these directories, the documentation is built again
        and the site reloaded.

        Note:
            The implementation is a hack. We are retrieving the watch function from a protected attribute.
            See issue [mkdocs/mkdocs#1952](https://github.com/mkdocs/mkdocs/issues/1952) for more information.
        """
        builder = list(server.watcher._tasks.values())[0]["func"]
        for element in self.config["watch"]:
            log.debug(
                f"mkdocstrings.plugin: Adding directory '{element}' to watcher"
            )
            server.watch(element, builder)
        return server
コード例 #5
0
ファイル: plugin.py プロジェクト: Holt59/mkdocstrings
    def on_page_content(self, html: str, page: Page, config: Config,
                        files: Files, **kwargs) -> str:
        """
        Hook for the [`on_page_contents` event](https://www.mkdocs.org/user-guide/plugins/#on_page_contents).

        In this hook, we map the IDs of every anchor found in the table of contents to the anchors absolute URLs.
        This mapping will be used later to fix unresolved reference of the form `[title][identifier]` or
        `[identifier][]`.
        """
        log.debug(
            f"mkdocstrings.plugin: Mapping identifiers to URLs for page {page.file.src_path}"
        )
        try:
            for item in page.toc.items:
                self.map_urls(page.canonical_url, item)
            return html
        except TypeError:
            # page.canonical_url is None, fail silently.
            # An error already has been logged in the on_config hook,
            # and warnings will be logged later, in the on_post_page hook.
            pass
コード例 #6
0
    def __init__(self) -> None:
        """
        Initialization method.

        When instantiating a Python collector, we open a subprocess in the background with `subprocess.Popen`.
        It will allow us to feed input to and read output from this subprocess, keeping it alive during
        the whole documentation generation. Spawning a new Python subprocess for each "autodoc" instruction would be
        too resource intensive, and would slow down `mkdocstrings` a lot.
        """
        log.debug(
            "mkdocstrings.handlers.python: Opening 'pytkdocs' subprocess")
        env = os.environ.copy()
        env["PYTHONUNBUFFERED"] = "1"
        self.process = Popen(  # nosec: there's no way to give the full path to the executable, is there?
            ["pytkdocs", "--line-by-line"],
            universal_newlines=True,
            stderr=PIPE,
            stdout=PIPE,
            stdin=PIPE,
            bufsize=-1,
            env=env,
        )
コード例 #7
0
    def on_post_page(self, output: str, page: Page, config: Config,
                     **kwargs) -> str:
        """
        Hook for the [`on_post_page` event](https://www.mkdocs.org/user-guide/plugins/#on_post_page).

        In this hook, we try to fix unresolved references of the form `[title][identifier]` or `[identifier][]`.
        Doing that allows the user of `mkdocstrings` to cross-reference objects in their documentation strings.
        It uses the native Markdown syntax so it's easy to remember and use.

        We log a warning for each reference that we couldn't map to an URL, but try to be smart and ignore identifiers
        that do not look legitimate (sometimes documentation can contain strings matching
        our [`AUTO_REF`][mkdocstrings.plugin.AUTO_REF] regular expression that did not intend to reference anything).
        We currently ignore references when their identifier contains a space or a slash.
        """
        log.debug(
            f"mkdocstrings.plugin: Fixing references in page {page.file.src_path}"
        )

        placeholder = Placeholder()
        while re.search(placeholder.seed, output) or any(
                placeholder.seed in url for url in self.url_map.values()):
            placeholder.set_seed()

        unmapped, unintended = [], []
        soup = BeautifulSoup(output, "html.parser")
        placeholder.replace_code_tags(soup)
        fixed_soup = AUTO_REF.sub(self.fix_ref(unmapped, unintended),
                                  str(soup))

        if unmapped or unintended:
            # We do nothing with unintended refs
            if unmapped and log.isEnabledFor(logging.WARNING):
                for ref in unmapped:
                    log.warning(
                        f"mkdocstrings.plugin: {page.file.src_path}: Could not fix ref '[{ref}]'.\n    "
                        f"The referenced object was not both collected and rendered."
                    )

        return placeholder.restore_code_tags(fixed_soup)
コード例 #8
0
 def teardown(self) -> None:
     """Terminate the opened subprocess, set it to None."""
     log.debug("mkdocstrings.handlers.python: Tearing process down")
     self.process.terminate()
     self.process = None
コード例 #9
0
    def collect(self, identifier: str, config: dict) -> dict:
        """
        Collect the documentation tree given an identifier and selection options.

        In this method, we feed one line of JSON to the standard input of the subprocess that was opened
        during instantiation of the collector. Then we read one line of JSON on its standard output.

        We load back the JSON text into a Python dictionary.
        If there is a decoding error, we log it as error and raise a CollectionError.

        If the dictionary contains an `error` key, we log it  as error (with the optional `traceback` value),
        and raise a CollectionError.

        If the dictionary values for keys `loading_errors` and `parsing_errors` are not empty,
        we log them as warnings.

        Then we pick up the only object within the `objects` list (there's always only one, because we collect
        them one by one), rebuild it's categories lists
        (see [`rebuild_category_lists()`][mkdocstrings.handlers.python.rebuild_category_lists]),
        and return it.

        Arguments:
            identifier: The dotted-path of a Python object available in the Python path.
            config: Selection options, used to alter the data collection done by `pytkdocs`.

        Returns:
            The collected object-tree.
        """
        final_config = dict(self.DEFAULT_CONFIG)
        final_config.update(config)

        log.debug("mkdocstrings.handlers.python: Preparing input")
        json_input = json.dumps(
            {"objects": [{
                "path": identifier,
                **final_config
            }]})

        log.debug("mkdocstrings.handlers.python: Writing to process' stdin")
        print(json_input, file=self.process.stdin, flush=True)

        log.debug("mkdocstrings.handlers.python: Reading process' stdout")
        stdout = self.process.stdout.readline()

        log.debug(
            "mkdocstrings.handlers.python: Loading JSON output as Python object"
        )
        try:
            result = json.loads(stdout)
        except json.decoder.JSONDecodeError as error:
            log.error(
                f"mkdocstrings.handlers.python: Error while loading JSON: {stdout}"
            )
            raise CollectionError(str(error))

        if "error" in result:
            message = f"mkdocstrings.handlers.python: Collection failed: {result['error']}"
            if "traceback" in result:
                message += f"\n{result['traceback']}"
            log.error(message)
            raise CollectionError(result["error"])

        if result["loading_errors"]:
            for error in result["loading_errors"]:
                log.warning(f"mkdocstrings.handlers.python: {error}")

        if result["parsing_errors"]:
            for path, errors in result["parsing_errors"].items():
                for error in errors:
                    log.warning(f"mkdocstrings.handlers.python: {error}")

        # We always collect only one object at a time
        result = result["objects"][0]

        log.debug(
            "mkdocstrings.handlers.python: Rebuilding categories and children lists"
        )
        rebuild_category_lists(result)

        return result
コード例 #10
0
ファイル: extension.py プロジェクト: Holt59/mkdocstrings
    def run(self, parent: Element, blocks: Element) -> None:
        block = blocks.pop(0)
        m = self.RE.search(str(block))

        if m:
            # removes the first line
            block = block[m.end():]  # type: ignore

        block, the_rest = self.detab(block)

        if m:
            identifier = m.group(1)
            log.debug(f"mkdocstrings.extension: Matched '::: {identifier}'")
            config = yaml.safe_load(str(block)) or {}

            handler_name = self.get_handler_name(config)
            log.debug(
                f"mkdocstrings.extension: Using handler '{handler_name}'")
            handler = get_handler(
                handler_name, self._config["theme_name"],
                self._config["mkdocstrings"]["custom_templates"])

            selection, rendering = self.get_item_configs(handler_name, config)

            log.debug("mkdocstrings.extension: Collecting data")
            try:
                data = handler.collector.collect(identifier, selection)
            except CollectionError:
                log.error(
                    f"mkdocstrings.extension: Could not collect '{identifier}'"
                )
                return

            log.debug("mkdocstrings.extension: Updating renderer's env")
            handler.renderer.update_env(self.md, self._config)

            log.debug("mkdocstrings.extension: Rendering templates")
            try:
                rendered = handler.renderer.render(data, rendering)
            except TemplateNotFound as error:
                theme_name = self._config["theme_name"]
                log.error(
                    f"mkdocstrings.extension: Template '{error.name}' not found "
                    f"for '{handler_name}' handler and theme '{theme_name}'.")
                return

            log.debug(
                "mkdocstrings.extension: Loading HTML back into XML tree")
            try:
                as_xml = XML(rendered)
            except ParseError as error:
                message = f"mkdocstrings.extension: {error}"
                if "mismatched tag" in str(error):
                    line, column = str(error).split(":")[-1].split(", ")

                    lineno = int(line.split(" ")[-1])
                    columnno = int(column.split(" ")[-1])

                    line = rendered.split("\n")[lineno - 1]
                    character = line[columnno]
                    message += (
                        f" (character {character}):\n{line}\n"
                        f"If your Markdown contains angle brackets < >, try to wrap them between backticks `< >`, "
                        f"or replace them with &lt; and &gt;")
                log.error(message)
                return

            as_xml = atomic_brute_cast(as_xml)  # type: ignore
            parent.append(as_xml)

        if the_rest:
            # This block contained unindented line(s) after the first indented
            # line. Insert these lines as the first block of the master blocks
            # list for future processing.
            blocks.insert(0, the_rest)