Skip to content

Reference#

CODE_EXTENSION_TO_LANGUAGE = {'yml': 'yaml', 'j2': 'jinja'} module-attribute #

Map of file extensions to code language.

Used when pygments is not available.

includex(filepath, start=1, end=None, lines=0, dedent=True, indent=0, indent_char=' ', indent_first=False, keep_trailing_whitespace=False, start_match='', end_match='', start_offset=0, end_offset=0, include_end_match=False, silence_errors=False, raise_errors=True, raw=False, escape=None, replace=None, add_heading_levels=0, lang=None, escape_notice=True, replace_notice=False, caption=None, alt_code_fences=False, suffix='', code=False) #

Include parts of a file.

Note

Whitespace (spaces, empty lines) will be stripped from the end of file. This prevents all includes of full files to end in a newline.

Parameters:

  • filepath (Path) –

    file to include

  • start (int, default: 1 ) –

    line number to begin include (is overwritten, if start_match matches a line).

  • end (int, default: None ) –

    line number to end include on (is overwritten, if end_match matches a line).

    Use negative numbers to index from end of file (e.g. -3 to skip last 3 lines of file). Must be greater than start, otherwise no content will be returned.

  • lines (int, default: 0 ) –

    number of lines to return (takes precedence over end and end_match)

  • dedent (bool, default: True ) –

    dedent by indentation of first line to be returned

  • dedent (int, default: True ) –

    dedent by that many characters

  • indent (int, default: 0 ) –

    add this many indent_chars to beginning of each line

  • indent_char (str, default: ' ' ) –

    single character to use for indentation (default: " " → space)

  • indent_first (bool, default: False ) –

    whether to also indent the first line

    indent might be used to make the content match the indentation of the document where the content is included. As the includex statement will already be indented, the first line doesn't need to be indented in most cases where an indent would be used.

  • start_match (str, default: '' ) –

    find start by providing text that shall match the first line

  • end_match (str, default: '' ) –

    find end by providing text that shall match the last line.

    Warning

    Cannot be used together with lines

  • start_offset (int, default: 0 ) –

    number of lines to offset line found by start_match provide positive integer to exclude that many lines after matched line provide negative integer to include additional lines before matched line

  • end_offset (int, default: 0 ) –

    number of lines to offset line found by end_match

    • provide positive integer to include additional lines after matched line
    • provide negative integer to exclude lines before matched line
  • include_end_match (bool, default: False ) –

    also include the ending matched line (same as end_offset=1)

  • silence_errors (bool, default: False ) –

    if true, do not return exception messages

  • raise_errors (bool, default: True ) –

    if true, raise exceptions instead of returning error string

  • raw (bool, default: False ) –

    will wrap file content in {% raw %} block.

    this prevents further interpretation of the file content by jinja.

  • escape (list[str], default: None ) –

    characters in list will be escaped using \

  • replace (list[tuple[str]], default: None ) –

    replace arbitrary substrings

  • add_heading_levels (int, default: 0 ) –

    If > 0, append as many "#" to any line starting with "#"

    this is meant to be used with Markdown files, that need to fit into an existing header structure

  • lang (str, default: None ) –

    wrap included file in markdown code fences with this language

    this was added to support escaping an included file (using raw) but not have the raw-tags be part of the code block.

    Deprecated since v0.0.4, use code instead.

  • escape_notice (bool | str, default: True ) –

    include note about escaped characters at the end

  • replace_notice (bool | str, default: False ) –

    include note about replaced strings at the end

  • caption (bool | str, default: None ) –

    include caption for code block

    lang must be given for this option to have any effect

  • alt_code_fences (bool | str, default: False ) –

    when True, ''' is used for code fences so they are not rendered in Markdown documents.

    When a custom string is provided, it will be used as code fence marker instead.

  • suffix (str, default: '' ) –

    add this after the included content

  • code (bool | str, default: False ) –

    render included content as code block

    If True, the language will be inferred from the file extension. If "", code block will be rendered without language. Otherwise, the given string will be used as code language

    Added in v0.0.4

Returns:

  • str

    content of file at filepath, modified by remaining arguments

Source code in includex.py
def includex(
    filepath: pathlib.Path,
    start: int = 1,
    end: int = None,
    lines: int = 0,
    dedent=True,
    indent: int = 0,
    indent_char: str = " ",
    indent_first: bool = False,
    keep_trailing_whitespace: bool = False,
    start_match: str = "",
    end_match: str = "",
    start_offset: int = 0,
    end_offset: int = 0,
    include_end_match: bool = False,
    silence_errors: bool = False,
    raise_errors: bool = True,
    raw: bool = False,
    escape: list[str] = None,
    replace: list[tuple[str]] = None,
    add_heading_levels: int = 0,
    lang: str = None,
    escape_notice: bool | str = True,
    replace_notice: bool | str = False,
    caption: bool | str = None,
    alt_code_fences: bool | str = False,
    suffix: str = "",
    code: bool | str = False,
) -> str:
    r"""Include parts of a file.

    Note:
        Whitespace (spaces, empty lines) will be stripped from the end of file.
        This prevents all includes of full files to end in a newline.

    Args:
        filepath: file to include
        start: line number to begin include (is overwritten, if start_match matches a line).
        end: line number to end include on (is overwritten, if end_match matches a line).

            Use negative numbers to index from end of file (e.g. -3 to skip last 3 lines of file).
            Must be greater than start, otherwise no content will be returned.

        lines: number of lines to return (takes precedence over end and end_match)
        dedent (bool): dedent by indentation of first line to be returned
        dedent (int): dedent by that many characters
        indent: add this many *indent_char*s to beginning of each line
        indent_char: single character to use for indentation (default: " " → *space*)
        indent_first: whether to also indent the first line

            `indent` might be used to make the content match the indentation of the document
            where the content is included. As the `includex` statement will already be
            indented, the first line doesn't need to be indented in most cases where an indent
            would be used.

        start_match: find start by providing text that shall match the first line
        end_match: find end by providing text that shall match the last line.

            !!! warning
                Cannot be used together with *lines*

        start_offset: number of lines to offset line found by *start_match*
            provide positive integer to exclude that many lines after matched line
            provide negative integer to include additional lines before matched line
        end_offset: number of lines to offset line found by *end_match*

            - provide positive integer to include additional lines after matched line
            - provide negative integer to exclude lines before matched line

        include_end_match: also include the ending matched line (same as `end_offset=1`)
        silence_errors: if true, do not return exception messages
        raise_errors: if true, raise exceptions instead of returning error string
        raw: will wrap file content in {% raw %} block.

            this prevents further interpretation of the file content by jinja.

        escape: characters in list will be escaped using `\`
        replace: replace arbitrary substrings
        add_heading_levels: If > 0, append as many "#" to any line starting with "#"

            this is meant to be used with Markdown files, that need to fit into an existing header
            structure

        lang: wrap included file in markdown code fences with this language

            this was added to support escaping an included file (using `raw`) but not have
            the raw-tags be part of the code block.

            Deprecated since v0.0.4, use `code` instead.

        escape_notice: include note about escaped characters at the end
        replace_notice: include note about replaced strings at the end
        caption: include caption for code block

            `lang` must be given for this option to have any effect

        alt_code_fences: when `True`, `'''` is used for code fences so they are not rendered
            in Markdown documents.

            When a custom string is provided, it will be used as code fence marker instead.

        suffix: add this after the included content
        code: render included content as code block

            If `True`, the language will be inferred from the file extension.
            If `""`, code block will be rendered without language.
            Otherwise, the given string will be used as code language

            Added in v0.0.4

    Returns:
        content of file at *filepath*, modified by remaining arguments
    """
    # transform one-based indices into file to zero-based indices into arrays
    start_idx = start - 1 if start > 0 else start
    # end doesn't need to be adjusted here, as it is exclusive in Python but should be
    # inclusive (+1) and also is a one-based index (-1)
    end_idx = end
    # use empty list as default argument safely
    escape = [] if escape is None else escape
    replace = [] if replace is None else replace
    prefix_offset, suffix_offset = 0, 0
    has_escaped_characters, has_replaced_characters = False, False

    try:
        filepath = pathlib.Path(filepath)
        content = filepath.open("r").readlines()
        original_content = content.copy()
        if start_match or end_match:
            first_line_found = not start_match
            for i, line in enumerate(content):
                if not first_line_found and start_match in line:
                    start_idx = i + start_offset
                    first_line_found = True
                    if not lines and end_match:
                        continue
                    else:
                        break
                if first_line_found and end_match and end_match in line:
                    end_idx = i + end_offset + (1 if include_end_match else 0)
                    break
            else:
                raise NoMatchError(
                    f"Couldn't find match for {'end_match' if first_line_found else 'start_match'}="
                    f"'{end_match if first_line_found else start_match}' in {filepath}"
                )
        if lines:
            end_idx = start_idx + lines

        content = content[start_idx:end_idx]

        if not content:
            if raise_errors and not silence_errors:
                raise ValueError("no content to include")
            elif silence_errors:
                return ""
            else:
                return ERROR_NOTICE_TEMPLATE % "no content to include"

        for esc in escape:
            for i, line in enumerate(content):
                if esc in line:
                    content[i] = line.replace(esc, "\\" + esc)
                    has_escaped_characters = True

        for orig, repl in replace:
            for idx, line in enumerate(content):
                if orig in line:
                    content[idx] = line.replace(orig, repl)
                    has_replaced_characters = True

        assert isinstance(dedent, (bool, int))
        if dedent is True and content:
            dedent = len(content[0].rstrip()) - len(content[0].strip())

        if add_heading_levels:
            content = [add_heading_levels * "#" + c if c.startswith("#") else c for c in content]

        if not keep_trailing_whitespace:
            content[-1] = content[-1].rstrip()

        if suffix:
            if not content[-1].endswith("\n"):
                content[-1] += "\n"
            content[-1] += f"{suffix}\n"

        if lang is not None:
            warn(
                "`lang` option is deprecated and will be removed in a future release. "
                "Use `code` instead.",
                DeprecationWarning,
                stacklevel=2,
            )

        if code is True and lang is None:
            lang = _infer_code_language(filepath, "".join(content))
        elif isinstance(code, str):
            lang = code

        if lang is not None:
            code_fence_marker = (
                "```"
                if alt_code_fences is False
                else ("'''" if alt_code_fences is True else alt_code_fences)
            )
            content.insert(0, f"{code_fence_marker}{lang}\n")
            if not content[-1].endswith("\n"):
                content[-1] += "\n"
            content.append(f"{code_fence_marker}")
            prefix_offset += 1
            suffix_offset += 1

        if raw:
            if not content[-1].endswith("\n"):
                content[-1] += "\n"
            content = ["{% raw %}\n", *content, "{% endraw %}"]
            prefix_offset += 1
            suffix_offset += 1

        # only dedent actual content, not prefix and suffix inserted by this macro
        content = "".join(
            [
                ((indent_char * indent) if i > 0 or indent_first else "")
                + line[dedent if prefix_offset <= i < len(content) - suffix_offset else None :]
                for i, line in enumerate(content)
            ]
        )

        if escape_notice and has_escaped_characters:
            if not content.endswith("\n"):
                content += "\n"
            content += (ESCAPE_NOTICE_TEMPLATE if escape_notice is True else escape_notice) % (
                ", ".join(f"` {e} `" for e in escape)
            )
            suffix_offset += 1

        if replace_notice and has_replaced_characters:
            if not content.endswith("\n"):
                content += "\n"
            content += (
                REPLACE_NOTICE_TEMPLATE % ", ".join(f"{orig} --> {repl}" for orig, repl in replace)
                if replace_notice is True
                else replace_notice
            )
            suffix_offset += 1

        if caption and lang is not None:
            if not content.endswith("\n"):
                content += "\n"

            # lines in file start with 1
            start_lineno = start_idx + 1
            end_lineno = end_idx if end_idx is not None else None
            # indices might be negative
            if start_lineno < 0:
                start_lineno = len(original_content) + start_lineno
            if end_lineno is not None and end_lineno < 0:
                end_lineno = len(original_content) + end_lineno

            content += _render_caption(caption, filepath, start_lineno, end_lineno)
            suffix_offset += 1

        return content

    except Exception as e:
        if raise_errors and not silence_errors:
            raise e
        return (
            ""
            if silence_errors
            else ERROR_NOTICE_TEMPLATE % (f"{e.__class__.__name__}" + (f": {e}" if f"{e}" else ""))
        )

caption#

Include a caption with included content, that is rendered as a code block via lang.

The caption will have a .caption css class applied via attr_list. So ensure you have this markdown extension enabled in your mkdocs.yaml:

markdown_extensions:
  - attr_list
  - ...

Then you can style these captions to look just like figcaptions. Here is an example for Material for MkDocs:

/* center captions */
.md-typeset .caption,
.md-typeset figcaption {
    /* same as material's figcaption */
    font-style: normal;
    max-width: 24rem;
    margin: 1em auto 1.5em;
    text-align: center;
    display: block;
    font-size: 0.8rem;
    /* custom */
    line-height: 1.2;
}

lang#

Wrap included content in code-fences with the given language.