Skip to content

Support using beartype and MyST-Parser for creating a custom role which requires an Inliner #1017

Open
@adamtheturtle

Description

@adamtheturtle

Describe the feature you'd like to request

Goals

  • Create a custom role which extends docutils.parsers.rst.roles.code_role.
  • Pass static type checking (mypy)
  • Pass runtime type checking (beartype)

Below is a minimized example of my work towards supporting MyST for substitution-code within https://github.com/adamtheturtle/sphinx-substitution-extensions.

Requirements

  • beartype==0.19.0
  • myst-parser==4.0.0
  • types-docutils==0.21.0.20241128

Files

Layout as described in
https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-extensions.

<project directory>/
├── conf.py
└── sphinxext/
    └── extname.py
# conf.py

import sys
from pathlib import Path

# Path configured as per
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-extensions
sys.path.append(str(Path('sphinxext').resolve()))

extensions = [
    "myst_parser",
    "extname",
]
# sphinxext/extname.py

from typing import Any

from beartype import beartype
from docutils.nodes import Node, system_message
from docutils.parsers.rst.roles import code_role
from docutils.parsers.rst.states import Inliner


# Typed version of the example from https://docutils.sourceforge.io/docs/howto/rst-roles.html#define-the-role-function
@beartype
def role_fn(
    name: str,
    rawtext: str,
    text: str,
    lineno: int,
    # This is an `Inliner`, so it can be passed through directly to `code_role`.
    inliner: Inliner,
    options: dict[Any, Any] = {},
    content: list[str] = [],
) -> tuple[list[Node], list[system_message]]:
    print("Custom logic")
    # Type stubs for `code_role` are at
    # https://github.com/python/typeshed/blob/57d7c4334b64856fda6f6e8f992b101ddafe2f57/stubs/docutils/docutils/parsers/rst/roles.pyi#L103
    #
    # Importantly, they require that `inliner` is an `Inliner`.
    return code_role(   
        role=name,
        rawtext=rawtext,
        text=text,
        lineno=lineno,
        inliner=inliner,
        options=options,
        content=content,
    )

@beartype
def setup(app):
    app.add_role(name="my-role", role=role_fn)

Command

sphinx-build -M html . build/

Error

beartype.roar.BeartypeCallHintParamViolation: Function extname.role_fn() parameter inliner=<myst_parser.mocking.MockInliner object at 0x10826e120> violates type hint <class 'docutils.parsers.rst.states.Inliner'>, as <class "myst_parser.mocking.MockInliner"> <myst_parser.mocking.MockInliner object at 0x10826e120> not instance of <class "docutils.parsers.rst.states.Inliner">.

Describe the solution you'd like

Modify MockInliner as per https://beartype.readthedocs.io/en/latest/faq/#mock-types to define a new __class__ property which returns Inliner.

Describe alternatives you've considered

My workaround is to have role_fn hint inliner as inliner: Inliner | MockInliner, and then to create a new Inliner object as needed for code_role:

if isinstance(inliner, MockInliner):
    new_inliner = Inliner()
    new_inliner.document = inliner_document
    inliner = new_inliner

Another alternative is to contribute to types-docutils to have code_role take a Protocol of just what is needed from Inliner, so it supports MockInliner, too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions