diff --git a/pyproject.toml b/pyproject.toml index ef6b6804857..9bfed6cfb50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,35 @@ dependencies = [ ] dynamic = ["version"] +[project.optional-dependencies] +docs = [ + "sphinxcontrib-websupport", +] +lint = [ + "ruff==0.12.7", + "mypy==1.17.0", + "sphinx-lint>=0.9", + "types-colorama==0.4.15.20240311", + "types-defusedxml==0.7.0.20250708", + "types-docutils==0.22.0.20250814", + "types-Pillow==10.2.0.20240822", + "types-Pygments==2.19.0.20250715", + "types-requests==2.32.4.20250611", # align with requests + "types-urllib3==1.26.25.14", + "pyright==1.1.400", + "pytest>=8.0", + "pypi-attestations==0.0.27", + "betterproto==2.0.0b6", +] +test = [ + "pytest>=8.0", + "pytest-xdist[psutil]>=3.4", + "defusedxml>=0.7.1", # for secure XML/HTML parsing + "cython>=3.0", + "setuptools>=70.0", # for Cython compilation + "typing_extensions>=4.9", # for typing_extensions.Unpack +] + [[project.authors]] name = "Adam Turner" email = "aa-turner@users.noreply.github.com" @@ -129,16 +158,16 @@ translations = [ "Jinja2>=3.1", ] types = [ - "mypy==1.17.1", + "mypy==1.17.0", "pyrefly", "pyright==1.1.400", { include-group = "type-stubs" }, ] type-stubs = [ # align with versions used elsewhere - "types-colorama==0.4.15.20250801", + "types-colorama==0.4.15.20240311", "types-defusedxml==0.7.0.20250708", - "types-docutils==0.21.0.20250525", + "types-docutils==0.22.0.20250814", "types-Pillow==10.2.0.20240822", "types-Pygments==2.19.0.20250715", "types-requests==2.32.4.20250611", @@ -218,6 +247,7 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = [ # tests/ + "tests.test_project", "tests.test_versioning", # tests/test_builders "tests.test_builders.test_build", @@ -227,6 +257,8 @@ module = [ # tests/test_directives "tests.test_directives.test_directive_code", "tests.test_directives.test_directives_no_typesetting", + # tests/test_environment + "tests.test_environment.test_environment", # tests/test_extensions "tests.test_extensions.test_ext_autodoc_autoclass", "tests.test_extensions.test_ext_autosummary_imports", @@ -235,6 +267,7 @@ module = [ "tests.test_extensions.test_ext_napoleon", # tests/test_markup "tests.test_markup.test_markup", + "tests.test_markup.test_parser", # tests/test_theming "tests.test_theming.test_templating", "tests.test_theming.test_theming", @@ -293,6 +326,7 @@ check_untyped_defs = false disable_error_code = [ "annotation-unchecked", ] +disallow_incomplete_defs = false disallow_untyped_calls = false disallow_untyped_defs = false diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 759a084cd00..f1e09399e05 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -528,7 +528,7 @@ def run(self, **kwargs: Any) -> None: citations += node if len(citations) > 0: - self.document += citations # type: ignore[attr-defined] + self.document += citations class CitationReferenceTransform(SphinxPostTransform): diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 090e58a4cf0..8c96b143dd6 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -405,7 +405,6 @@ def _insert_input(include_lines: list[str], source: str) -> None: # Only enable this patch if there are listeners for 'include-read'. if self.env.events.listeners.get('include-read'): - # See https://github.com/python/mypy/issues/2427 for details on the mypy issue self.state_machine.insert_input = _insert_input if self.arguments[0].startswith('<') and self.arguments[0].endswith('>'): diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 0a7419ed563..05e9fa41b66 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -19,6 +19,7 @@ from sphinx.util.osutil import SEP, relpath if TYPE_CHECKING: + from collections.abc import Sequence from typing import ClassVar from docutils.nodes import Node @@ -30,12 +31,12 @@ logger = logging.getLogger(__name__) -class Figure(images.Figure): # type: ignore[misc] +class Figure(images.Figure): """The figure directive which applies `:name:` option to the figure node instead of the image node. """ - def run(self) -> list[Node]: + def run(self) -> Sequence[Node]: name = self.options.pop('name', None) result = super().run() if len(result) == 2 or isinstance(result[0], nodes.system_message): @@ -56,12 +57,12 @@ def run(self) -> list[Node]: return [figure_node] -class CSVTable(tables.CSVTable): # type: ignore[misc] +class CSVTable(tables.CSVTable): """The csv-table directive which searches a CSV file from Sphinx project's source directory when an absolute path is given via :file: option. """ - def run(self) -> list[Node]: + def run(self) -> Sequence[nodes.table | nodes.system_message]: if 'file' in self.options and self.options['file'].startswith((SEP, os.sep)): env = self.state.document.settings.env filename = Path(self.options['file']) diff --git a/sphinx/environment/collectors/title.py b/sphinx/environment/collectors/title.py index 50dfa2bdc54..59a77ef3e7e 100644 --- a/sphinx/environment/collectors/title.py +++ b/sphinx/environment/collectors/title.py @@ -50,7 +50,7 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: for node in doctree.findall(nodes.section): visitor = SphinxContentsFilter(doctree) node[0].walkabout(visitor) - titlenode += visitor.get_entry_text() # type: ignore[no-untyped-call] + titlenode += visitor.get_entry_text() break else: # document has no title diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index 5c3d5c97f8c..571d4d964a5 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -84,7 +84,7 @@ def build_toc( # and unnecessary stuff visitor = SphinxContentsFilter(doctree) title.walkabout(visitor) - nodetext = visitor.get_entry_text() # type: ignore[no-untyped-call] + nodetext = visitor.get_entry_text() anchorname = _make_anchor_name(sectionnode['ids'], numentries) # make these nodes: # list_item -> compact_paragraph -> reference diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 0f27fd7df3a..c14ba1665fa 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -105,9 +105,7 @@ def run(self) -> list[Node]: reporter = self.state.document.reporter try: - source, lineno = reporter.get_source_and_line( # type: ignore[attr-defined] - self.lineno - ) + source, lineno = reporter.get_source_and_line(self.lineno) except AttributeError: source, lineno = (None, None) logger.debug('[autodoc] %s:%s: input:\n%s', source, lineno, self.block_text) diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 83a6d4b7b01..f44bbb860ab 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -429,6 +429,7 @@ def run(self) -> list[Node]: include_subclasses='include-subclasses' in self.options, ) except InheritanceException as err: + assert node.document is not None return [node.document.reporter.warning(err, line=self.lineno)] # Create xref nodes for each target of the graph's image map and diff --git a/sphinx/io.py b/sphinx/io.py index 1df5ac454ce..5b11b9e4918 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -32,7 +32,7 @@ warnings.warn('sphinx.io is deprecated', RemovedInSphinx10Warning, stacklevel=2) -class SphinxBaseReader(standalone.Reader): # type: ignore[misc] +class SphinxBaseReader(standalone.Reader): """A base class of readers for Sphinx. This replaces reporter by Sphinx's on generating document. @@ -92,15 +92,17 @@ def _setup_transforms(self, transforms: list[type[Transform]], /) -> None: def read(self, source: Input, parser: Parser, settings: Values) -> nodes.document: # type: ignore[type-arg] self.source = source - if not self.parser: # type: ignore[has-type] + if not self.parser: self.parser = parser self.settings = settings self.input = self.read_source(settings.env) self.parse() + assert self.document is not None return self.document def read_source(self, env: BuildEnvironment) -> str: """Read content from source and do post-process.""" + assert self.source is not None content = self.source.read() # emit "source-read" event diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 3728093f093..7cdef224fcb 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -22,10 +22,10 @@ from sphinx.util.nodes import apply_source_workaround, is_smartquotable if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Generator, Iterable from typing import Any, Literal, TypeAlias - from docutils.nodes import Node, Text + from docutils.nodes import Node from typing_extensions import TypeIs from sphinx.application import Sphinx @@ -92,7 +92,7 @@ def apply_transforms(self) -> None: if not hasattr(self.document.settings, 'env') and self.env: self.document.settings.env = self.env - super().apply_transforms() # type: ignore[misc] + super().apply_transforms() else: # wrap the target node by document node during transforming try: @@ -281,6 +281,7 @@ def is_translatable_node(node: Node) -> TypeIs[nodes.Element]: return isinstance(node, target_nodes) for node in self.document.findall(is_translatable_node): + assert isinstance(node, nodes.Element) node['translatable'] = True @@ -395,7 +396,14 @@ def is_available(self) -> bool: language = self.env.settings['language_code'] return any(tag in smartchars.quotes for tag in normalize_language_tag(language)) - def get_tokens(self, txtnodes: list[Text]) -> Iterator[tuple[str, str]]: + def get_tokens( + self, + txtnodes: Iterable[Node], + ) -> Generator[ + tuple[Literal['plain', 'literal'], str], + None, + None, + ]: # A generator that yields ``(texttype, nodetext)`` tuples for a list # of "Text" nodes (interface to ``smartquotes.educate_tokens()``). for txtnode in txtnodes: diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 2673a3fc77f..4f0786d434b 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -678,7 +678,7 @@ def get_source_info(self, lineno: int | None = None) -> tuple[str, int]: # .. versionadded:: 3.0 if lineno is None: lineno = self.lineno - return self.inliner.reporter.get_source_and_line(lineno) # type: ignore[attr-defined] + return self.inliner.reporter.get_source_and_line(lineno) def set_source_info(self, node: Node, lineno: int | None = None) -> None: # .. versionadded:: 2.0 @@ -911,4 +911,4 @@ def _get_settings( defaults=defaults, read_config_files=read_config_files, ) - return option_parser.get_default_values() # type: ignore[return-value] + return option_parser.get_default_values() diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 5b6bd429a8e..be0495e00a8 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -683,7 +683,7 @@ def set_source_info(directive: Directive, node: Node) -> None: def set_role_source_info(inliner: Inliner, lineno: int, node: Node) -> None: - gsal = inliner.reporter.get_source_and_line # type: ignore[attr-defined] + gsal = inliner.reporter.get_source_and_line node.source, node.line = gsal(lineno) diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 04f9af122a4..cb273cbdbd3 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -20,7 +20,7 @@ # https://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html -class HTMLWriter(html4css1.Writer): # type: ignore[misc] +class HTMLWriter(html4css1.Writer): # override embed-stylesheet default value to False. settings_default_overrides = {'embed_stylesheet': False} @@ -33,6 +33,7 @@ def translate(self) -> None: # sadly, this is mostly copied from parent class visitor = self.builder.create_translator(self.document, self.builder) self.visitor = cast('HTML5Translator', visitor) + assert self.document is not None self.document.walkabout(visitor) self.output = self.visitor.astext() for attr in ( diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index bbcd247e33c..7d15623989e 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -41,7 +41,7 @@ def multiply_length(length: str, scale: int) -> str: return f'{int(result)}{unit}' -class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc] +class HTML5Translator(SphinxTranslator, BaseTranslator): """Our custom HTML translator.""" builder: StandaloneHTMLBuilder diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index 282cd0ed14c..fd655d08ea6 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -25,12 +25,13 @@ logger = logging.getLogger(__name__) -class ManualPageWriter(manpage.Writer): # type: ignore[misc] +class ManualPageWriter(manpage.Writer): def __init__(self, builder: Builder) -> None: super().__init__() self.builder = builder def translate(self) -> None: + assert self.document is not None transform = NestedInlineTransform(self.document) transform.apply() visitor = self.builder.create_translator(self.document, self.builder) diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py index e9877825de6..bf5bd17a8a5 100644 --- a/sphinx/writers/xml.py +++ b/sphinx/writers/xml.py @@ -12,7 +12,7 @@ from sphinx.builders import Builder -class XMLWriter(docutils_xml.Writer): # type: ignore[misc] +class XMLWriter(docutils_xml.Writer): output: str def __init__(self, builder: Builder) -> None: @@ -21,6 +21,7 @@ def __init__(self, builder: Builder) -> None: self._config = builder.config def translate(self, *args: Any, **kwargs: Any) -> None: + assert self.document is not None self.document.settings.newlines = self.document.settings.indents = ( self._config.xml_pretty ) @@ -34,7 +35,7 @@ def translate(self, *args: Any, **kwargs: Any) -> None: self.output = ''.join(visitor.output) # type: ignore[attr-defined] -class PseudoXMLWriter(docutils_xml.Writer): # type: ignore[misc] +class PseudoXMLWriter(docutils_xml.Writer): supported = ('pprint', 'pformat', 'pseudoxml') """Formats this writer supports.""" @@ -49,6 +50,7 @@ def __init__(self, builder: Builder) -> None: self.builder = builder def translate(self) -> None: + assert self.document is not None self.output = self.document.pformat() def supports(self, format: str) -> bool: diff --git a/tests/test_util/test_util_docutils_sphinx_directive.py b/tests/test_util/test_util_docutils_sphinx_directive.py index eb1e4aea16a..e91a4d2ae23 100644 --- a/tests/test_util/test_util_docutils_sphinx_directive.py +++ b/tests/test_util/test_util_docutils_sphinx_directive.py @@ -11,6 +11,7 @@ state_classes, ) from docutils.statemachine import StringList +from docutils.utils import Reporter from sphinx.util.docutils import SphinxDirective, new_document @@ -26,7 +27,7 @@ def make_directive_and_state( *, env: SimpleNamespace, input_lines: StringList | None = None ) -> tuple[RSTState, SphinxDirective]: sm = RSTStateMachine(state_classes, initial_state='Body') - sm.reporter = object() + sm.reporter = Reporter(source='', report_level=0, halt_level=0) if input_lines is not None: sm.input_lines = input_lines state = RSTState(sm)