Skip to content

Commit 62064e0

Browse files
committed
[docutils] Improve and complete stubs
1 parent 9093590 commit 62064e0

112 files changed

Lines changed: 3153 additions & 1619 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,61 @@
1-
docutils.nodes.Element.__iter__ # doesn't exist at runtime, but the class is iterable due to __getitem__
2-
docutils.nodes.Element.tagname # class variable is overridden in __init__ method
3-
docutils.nodes.NodeVisitor.depart_\w+ # Methods are discovered dynamically on commonly-used subclasses
4-
docutils.nodes.NodeVisitor.visit_\w+ # Methods are discovered dynamically on commonly-used subclasses
5-
docutils.nodes.NodeVisitor.__init__ # Argument "document" should be positional-only, but subclasses are not
1+
# This class variable is overridden in __init__.
2+
docutils.nodes.Element.tagname
3+
# The `document` parameter should be positional-only, but there are subclasses where it is not.
4+
docutils.nodes.NodeVisitor.__init__
65

7-
# these methods take a rawsource parameter that has been deprecated and is completely ignored, so we omit it from the stub
6+
# This constructor technically still takes a `rawsource` parameter;
7+
# however, it is deprecated on Text nodes. Passing 'None' prints a warning, and passing
8+
# anything else will raise.
9+
# It is omitted for simplicity (e.g. not requiring matching overloads in child classes).
810
docutils.nodes.Text.__new__
9-
docutils.parsers.rst.directives.admonitions.BaseAdmonition.node_class # must be overridden by base classes (pseudo-abstract)
10-
docutils.statemachine.State.nested_sm # is initialised in __init__
11-
docutils.statemachine.State.nested_sm_kwargs # is initialised in __init__
12-
docutils.statemachine.ViewList.__iter__ # doesn't exist at runtime, but the class is iterable due to __getitem__
13-
docutils.transforms.Transform.apply # method apply is not implemented
14-
docutils.transforms.Transform.__getattr__
15-
docutils.TransformSpec.unknown_reference_resolvers
11+
12+
# This attribute is required to be overridden by base classes (pseudo-abstractmethod).
13+
docutils.parsers.rst.directives.admonitions.BaseAdmonition.node_class
14+
15+
# These attributes are initialized to None in the classs-body, then initialized in __init__.
16+
# It is never None in practice.
17+
docutils.statemachine.State.nested_sm
18+
docutils.statemachine.State.nested_sm_kwargs
19+
20+
# These attribute names contain spaces and are set using setattr from docutils.sty.
1621
docutils.writers.latex2e.PreambleCmds... contents
17-
docutils.writers.latex2e.PreambleCmds.inline role \w+ # attribute names with spaces, set with setattr() from docutils.sty
22+
docutils.writers.latex2e.PreambleCmds.inline role \w+
1823

19-
# Files that don't exist at runtime of stubtests, raises ImportError:
24+
# These modules have optional dependencies that likely are not available at runtime.
2025
docutils.parsers.commonmark_wrapper
2126
docutils.parsers.recommonmark_wrapper
22-
docutils.writers.odf_odt.pygmentsformatter # import `pygments` third-party library
2327

24-
# `TYPE_CHECKING` variable is for internal use:
28+
# Imports `pygments`; using types-Pygments caused ts_utils to have a recuesion error.
29+
docutils.writers.odf_odt.pygmentsformatter
30+
31+
# Docutils defines `TYPE_CHECKING` instead of importing it.
2532
docutils.*\.TYPE_CHECKING
33+
34+
# stubtest infers `dict[Any, Any]` for the optional keyword arguments.
35+
# That obviously fails to match `_OptionKwargs`.
36+
docutils\..*\.settings_spec
37+
38+
# Not generic at runtime, but it is runtime subscriptable via `__class_getitem__()`.
39+
# Take advantage by treating it like a proper generic.
40+
docutils.languages.LanguageImporter.__class_getitem__
41+
42+
# These are functions defined in the class bodies without `self`.
43+
# They are only meant to be accessed via the class (e.g. in `option_spec`);
44+
# When docutils drops 3.9 support, they should theoretically be real staticmethods.
45+
docutils.parsers.rst.directives.images.Figure.align
46+
docutils.parsers.rst.directives.images.Figure.figwidth_value
47+
docutils.parsers.rst.directives.images.Image.align
48+
docutils.parsers.rst.directives.images.Image.loading
49+
docutils.parsers.rst.directives.parts.Contents.backlinks
50+
51+
# These are aliased to `invalid_input(), with defaulted parameters, at runtime.
52+
# Subclasses must explicitly re-enable. This causes unavoidable signature conflicts.
53+
# The stub keeps the expected transition signatures so that subclasses may override without errors.
54+
docutils.parsers.rst.states.SpecializedBody.(anonymous|bullet|doctest|enumerator|explicit_markup|field_marker|grid_table_top|indent|line|line_block|option_marker|simple_table_top|text)
55+
docutils.parsers.rst.states.SpecializedText.(blank|indent|text|underline)
56+
57+
# This is mutated at runtime. It is `None` at class level, and
58+
# once `Reader.read()` is called, it is no longer None.
59+
# `Reader.document` has no real good reason to be accessed before calling `read`, and
60+
# annotating as "| None" is an unnecessary nuissance.
61+
docutils.readers.standalone.Reader.document
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from typing_extensions import assert_type
4+
5+
from docutils.core import publish_doctree, publish_parts
6+
7+
# `publish_parts()` returns writer-specific parts for the HTML and LaTeX writers.
8+
# It should be possible to deduce key types with this knowledge.
9+
html_parts = publish_parts("Hello", writer="html5")
10+
assert_type(html_parts["body"], str)
11+
assert_type(html_parts["whole"], str)
12+
latex_parts = publish_parts("Hello", writer="latex")
13+
assert_type(latex_parts["titledata"], str)
14+
other_parts = publish_parts("Hello", writer="pseudoxml")
15+
assert_type(other_parts["whole"], "str | bytes")
16+
assert_type(other_parts.get("body"), "str | None")
17+
18+
# `settings_overrides` should accept any copyable mapping.
19+
# This should include `dict`s of narrower value types.
20+
overrides: dict[str, int] = {"report_level": 5, "halt_level": 5}
21+
publish_doctree("x", settings_overrides=overrides)
22+
publish_doctree("x", settings_overrides={"report_level": 5, "input_encoding": "utf-8"})
23+
24+
25+
def not_a_mapping() -> None:
26+
publish_doctree("x", settings_overrides=[("report_level", 5)]) # type: ignore[arg-type]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Callable, Sequence
4+
from typing import ClassVar
5+
6+
from docutils import nodes
7+
from docutils.parsers.rst import Directive, directives
8+
from docutils.parsers.rst.directives.body import Rubric, Topic
9+
from docutils.parsers.rst.directives.images import Figure
10+
from docutils.parsers.rst.directives.tables import CSVTable
11+
12+
13+
# `option_spec` should accept any mapping.
14+
# This should include `dict` subclasses with narrower converters.
15+
class DummyOptionSpec(dict[str, Callable[[str], str]]):
16+
def __bool__(self) -> bool:
17+
return True
18+
19+
20+
class AnyOptions(Directive):
21+
option_spec = DummyOptionSpec()
22+
23+
24+
# Concrete directives have a `dict` option spec that should be extendable.
25+
class Exercise(Topic):
26+
option_spec: ClassVar[dict[str, Callable[[str], object]]] = {**Topic.option_spec, "difficulty": directives.nonnegative_int}
27+
28+
29+
class MyFigure(Figure):
30+
option_spec: ClassVar[dict[str, Callable[[str], object]]] = Figure.option_spec.copy()
31+
option_spec["caption"] = directives.unchanged
32+
33+
# Overrides should be able to return a (narrower) sequence of nodes.
34+
def run(self) -> Sequence[nodes.Node]:
35+
return super().run()
36+
37+
38+
class MyRubric(Rubric):
39+
def run(self) -> list[nodes.rubric | nodes.system_message]:
40+
return []
41+
42+
43+
class MyCSVTable(CSVTable):
44+
def run(self) -> Sequence[nodes.table | nodes.system_message]:
45+
return super().run()
46+
47+
48+
# A directive's own `option_spec` literal should be acceptable.
49+
class MyDirective(Directive):
50+
option_spec = {"class": directives.class_option, "flag": directives.flag, "count": directives.nonnegative_int}
51+
52+
def run(self) -> list[nodes.Node]:
53+
return []
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing_extensions import assert_type
5+
6+
from docutils.languages import LanguageImporter, get_language
7+
from docutils.parsers.rst.languages import get_language as get_rst_language
8+
9+
if TYPE_CHECKING:
10+
from docutils.languages import LanguageModule
11+
from docutils.parsers.rst.languages import RSTLanguageModule
12+
13+
14+
assert_type(get_language("de"), "LanguageModule")
15+
16+
# The rST importer has should have no fallback language.
17+
assert_type(get_rst_language("de"), "RSTLanguageModule | None")
18+
19+
# The module type should default to `LanguageModule`.
20+
assert_type(LanguageImporter()("de"), "LanguageModule")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
from typing_extensions import assert_type
5+
6+
from docutils import nodes
7+
8+
9+
# Well-known attributes with stable types have `Literal`-keyed overloads.
10+
# So, it should be able to deduce the types of certain keys.
11+
def attributes(node: nodes.Element) -> None:
12+
assert_type(node["classes"], list[str])
13+
assert_type(node.get("classes"), list[str])
14+
assert_type(node.get("backrefs"), "list[str] | None")
15+
assert_type(node["refuri"], str)
16+
assert_type(node.get("refuri"), "str | None")
17+
assert_type(node.get("refuri", ""), str)
18+
19+
# Arbitrary attributes are `Any`.
20+
assert_type(node.get("highlight_args", {}), Any)
21+
22+
23+
# Visitor methods should be overridable with any return type.
24+
class Visitor(nodes.SparseNodeVisitor):
25+
def visit_paragraph(self, node: nodes.paragraph) -> None:
26+
raise nodes.SkipNode
27+
28+
def visit_section(self, node: nodes.Element) -> bool:
29+
return True
30+
31+
32+
# A node's parent may be `None`; the root document never has one.
33+
def parents(document: nodes.document, paragraph: nodes.paragraph) -> None:
34+
assert_type(document.parent, None)
35+
assert_type(paragraph.parent, "nodes.Element | None")
36+
if paragraph.parent is not None:
37+
paragraph.parent.remove(paragraph)
38+
39+
40+
# Visit/depart methods do not exist on the base `NodeVisitor` at runtime.
41+
# The methods only exist when a subclass defines them.
42+
class DirectVisitor(nodes.NodeVisitor):
43+
def visit_paragraph(self, node: nodes.paragraph) -> None:
44+
# Pyright correctly flags this as not existing NodeVisitor.
45+
super().visit_paragraph(node) # pyright: ignore[reportGeneralTypeIssues]
46+
47+
48+
class SparseVisitor(nodes.SparseNodeVisitor):
49+
# Pyright correctly allows this because it is concretely defined
50+
# in SparseNodeVisitor.
51+
def visit_paragraph(self, node: nodes.paragraph) -> None:
52+
super().visit_paragraph(node)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping, Sequence
4+
from typing import Any
5+
from typing_extensions import assert_type
6+
7+
from docutils import nodes
8+
from docutils.parsers.rst import roles
9+
from docutils.parsers.rst.states import Inliner
10+
11+
# Docutils calls Role functions with five positional arguments, so the names do not matter.
12+
# `options` and `content` are passed as keyword-only by `CustomRole`, so they must have defaults.
13+
14+
15+
def role_with_defaults(
16+
name: str,
17+
rawtext: str,
18+
text: str,
19+
lineno: int,
20+
inliner: Inliner,
21+
options: Mapping[str, Any] | None = None,
22+
content: Sequence[str] | None = None,
23+
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
24+
return [], []
25+
26+
27+
def role_with_dict_defaults(
28+
typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: dict[str, Any] = {}, content: list[str] = []
29+
) -> tuple[list[nodes.reference], list[nodes.system_message]]:
30+
return [], []
31+
32+
33+
def role_without_defaults(
34+
name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: dict[str, Any], content: list[str]
35+
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
36+
return [], []
37+
38+
39+
roles.register_local_role("a", role_with_defaults)
40+
roles.register_local_role("b", role_with_dict_defaults)
41+
roles.register_local_role("c", role_without_defaults) # type: ignore[arg-type]
42+
roles.register_canonical_role("d", roles.GenericRole("d", nodes.emphasis))
43+
44+
# `normalize_options()` should preserve the value type of the options.
45+
assert_type(roles.normalize_options({"class": ["a"]}), dict[str, list[str]])
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from docutils.transforms import Transform
6+
7+
# `apply()` overrides should be accepted with and without kwargs,
8+
# True keyword arguments (and positional) should error.
9+
10+
11+
class DocutilsStyle(Transform):
12+
def apply(self) -> None: ...
13+
14+
15+
class SphinxStyle(Transform):
16+
def apply(self, **kwargs: Any) -> None: ...
17+
18+
19+
class RequiredKeyword(Transform):
20+
def apply(self, *, level: int) -> None: ... # type: ignore[override]
21+
22+
23+
def apply(transform: Transform) -> None:
24+
transform.apply()
25+
transform.apply(level=1) # type: ignore[call-arg]

0 commit comments

Comments
 (0)