Skip to content

Commit 72345bc

Browse files
committed
typing(feat[typeddict]): Replace dict[str, Any] with TypedDicts, object for argparse fields
why: Eliminate imprecise Any annotations where the structure is actually known. TypedDicts catch key typos at type-check time; object is semantically correct for argparse values (any Python object, not "opts out of type checking"). Also revealed real bug: exemplar.setup() was missing parallel_write_safe key. what: - directive.py: option_spec value Any→Callable[[str], Any] (matches Sphinx's OptionSpec) - exemplar.py: setup() return dict[str, Any]→SetupDict; fix missing parallel_write_safe - test_compat.py: create_parser/get_parser/make_parser/parser return Any→ArgumentParser - test_renderer.py: post_process override list[Any]→list[nodes.Node] + isinstance narrowing - test_sphinx_fonts.py: config_values middle tuple element Any→object
1 parent b39dbd2 commit 72345bc

File tree

5 files changed

+28
-18
lines changed

5 files changed

+28
-18
lines changed

packages/sphinx-argparse-neo/src/sphinx_argparse_neo/directive.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
if t.TYPE_CHECKING:
2020
import argparse
21+
from collections.abc import Callable
2122

2223

2324
class ArgparseDirective(SphinxDirective):
@@ -66,7 +67,7 @@ class ArgparseDirective(SphinxDirective):
6667
required_arguments = 0
6768
optional_arguments = 0
6869

69-
option_spec: t.ClassVar[dict[str, t.Any]] = {
70+
option_spec: t.ClassVar[dict[str, Callable[[str], t.Any]]] = {
7071
"module": directives.unchanged_required,
7172
"func": directives.unchanged_required,
7273
"prog": directives.unchanged,

packages/sphinx-argparse-neo/src/sphinx_argparse_neo/exemplar.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ def setup(app):
117117
from sphinx.application import Sphinx
118118

119119

120+
class SetupDict(t.TypedDict):
121+
"""Return type for Sphinx extension setup()."""
122+
123+
version: str
124+
parallel_read_safe: bool
125+
parallel_write_safe: bool
126+
127+
120128
@dataclasses.dataclass
121129
class ExemplarConfig:
122130
"""Configuration for argparse_exemplar transformation.
@@ -1219,7 +1227,7 @@ def run(self) -> list[nodes.Node]:
12191227
return _reorder_nodes(flattened, config=config)
12201228

12211229

1222-
def setup(app: Sphinx) -> dict[str, t.Any]:
1230+
def setup(app: Sphinx) -> SetupDict:
12231231
"""Register the clean argparse directive, lexers, and CLI roles.
12241232
12251233
Configuration Options
@@ -1303,4 +1311,4 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
13031311

13041312
register_roles()
13051313

1306-
return {"version": "4.0", "parallel_read_safe": True}
1314+
return {"version": "4.0", "parallel_read_safe": True, "parallel_write_safe": True}

tests/ext/argparse_neo/test_compat.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
from __future__ import annotations
44

5+
import argparse
56
import sys
6-
import typing as t
77

88
import pytest
99

@@ -177,7 +177,7 @@ def test_get_parser_from_module_argparse() -> None:
177177

178178
test_module = types.ModuleType("test_parser_module")
179179

180-
def create_parser() -> t.Any:
180+
def create_parser() -> argparse.ArgumentParser:
181181
import argparse
182182

183183
return argparse.ArgumentParser(prog="test")
@@ -198,7 +198,7 @@ def test_get_parser_from_module_with_mock() -> None:
198198

199199
test_module = types.ModuleType("test_mock_parser")
200200

201-
def create_parser() -> t.Any:
201+
def create_parser() -> argparse.ArgumentParser:
202202
import argparse
203203

204204
return argparse.ArgumentParser(prog="mocked")
@@ -225,7 +225,7 @@ def test_get_parser_from_module_dotted_path() -> None:
225225

226226
class CLI:
227227
@staticmethod
228-
def create_parser() -> t.Any:
228+
def create_parser() -> argparse.ArgumentParser:
229229
import argparse
230230

231231
return argparse.ArgumentParser(prog="from_class")
@@ -261,7 +261,7 @@ def test_get_parser_from_entry_point_valid() -> None:
261261

262262
test_module = types.ModuleType("test_entry_point")
263263

264-
def get_parser() -> t.Any:
264+
def get_parser() -> argparse.ArgumentParser:
265265
import argparse
266266

267267
return argparse.ArgumentParser(prog="entry")
@@ -292,7 +292,7 @@ def test_get_parser_from_entry_point_with_class() -> None:
292292

293293
class Factory:
294294
@staticmethod
295-
def parser() -> t.Any:
295+
def parser() -> argparse.ArgumentParser:
296296
import argparse
297297

298298
return argparse.ArgumentParser(prog="factory")
@@ -313,7 +313,7 @@ def test_get_parser_from_entry_point_with_mock() -> None:
313313

314314
test_module = types.ModuleType("test_entry_mock")
315315

316-
def make_parser() -> t.Any:
316+
def make_parser() -> argparse.ArgumentParser:
317317
import argparse
318318

319319
return argparse.ArgumentParser(prog="with_mock")

tests/ext/argparse_neo/test_renderer.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import argparse
6-
import typing as t
76

87
from docutils import nodes
98

@@ -482,18 +481,20 @@ def test_post_process_custom() -> None:
482481
"""Test custom post_process implementation."""
483482

484483
class CustomRenderer(ArgparseRenderer):
485-
def post_process(self, result_nodes: list[t.Any]) -> list[t.Any]:
486-
# Add a marker to each node
484+
def post_process(self, result_nodes: list[nodes.Node]) -> list[nodes.Node]:
485+
# Add a marker to each node; Element subclass supports item assignment
487486
for node in result_nodes:
488-
node["custom_marker"] = True
487+
if isinstance(node, nodes.Element):
488+
node["custom_marker"] = True
489489
return result_nodes
490490

491491
renderer = CustomRenderer()
492492

493493
from docutils import nodes as dn
494494

495-
input_nodes = [dn.paragraph(text="test")]
495+
input_nodes: list[nodes.Node] = [dn.paragraph(text="test")]
496496

497497
result = renderer.post_process(input_nodes)
498498

499+
assert isinstance(result[0], nodes.Element)
499500
assert result[0].get("custom_marker") is True

tests/ext/test_sphinx_fonts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ def test_on_html_page_context_without_attrs() -> None:
489489

490490
def test_setup_return_value() -> None:
491491
"""Verify setup() returns correct metadata dict."""
492-
config_values: list[tuple[str, t.Any, str]] = []
492+
config_values: list[tuple[str, object, str]] = []
493493
connections: list[tuple[str, t.Any]] = []
494494

495495
app = t.cast(
@@ -513,7 +513,7 @@ def test_setup_return_value() -> None:
513513

514514
def test_setup_config_values() -> None:
515515
"""Verify setup() registers all expected config values."""
516-
config_values: list[tuple[str, t.Any, str]] = []
516+
config_values: list[tuple[str, object, str]] = []
517517
connections: list[tuple[str, t.Any]] = []
518518

519519
app = t.cast(
@@ -538,7 +538,7 @@ def test_setup_config_values() -> None:
538538

539539
def test_setup_event_connections() -> None:
540540
"""Verify setup() connects to builder-inited and html-page-context events."""
541-
config_values: list[tuple[str, t.Any, str]] = []
541+
config_values: list[tuple[str, object, str]] = []
542542
connections: list[tuple[str, t.Any]] = []
543543

544544
app = t.cast(

0 commit comments

Comments
 (0)