Skip to content

Commit 622b562

Browse files
authored
✨ feat(overloads): add opt-out control for overload rendering (#645)
PR #625 introduced automatic overload signature rendering, which works well for many cases but can produce noisy output when overloads are better described in prose. Users need a way to selectively disable this behavior without losing it entirely across their project. This adds two complementary opt-out mechanisms: a `typehints_document_overloads` Sphinx config option (default `True`) for project-wide control, and a `:no-overloads:` docstring directive for per-function suppression. The directive is automatically stripped from the rendered output. Both approaches follow existing patterns — the config mirrors `typehints_document_rtype`, and the directive works like other RST field-list markers. The overload injection logic was also refactored from a single monolithic function into three focused helpers to stay within linting complexity thresholds while accommodating the new checks. Closes #642
1 parent 3c0ccfa commit 622b562

File tree

5 files changed

+164
-29
lines changed

5 files changed

+164
-29
lines changed

README.md

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ features above. See [Avoid duplicate types with built-in Sphinx](#avoid-duplicat
4040
- [Control return type display](#control-return-type-display)
4141
- [Change how union types look](#change-how-union-types-look)
4242
- [Show default parameter values](#show-default-parameter-values)
43+
- [Control overload signature display](#control-overload-signature-display)
4344
- [Keep type hints in function signatures](#keep-type-hints-in-function-signatures)
4445
- [Handle circular imports](#handle-circular-imports)
4546
- [Resolve types from `TYPE_CHECKING` blocks](#resolve-types-from-type_checking-blocks)
@@ -191,6 +192,31 @@ typehints_defaults = "braces"
191192
typehints_defaults = "braces-after"
192193
```
193194

195+
### Control overload signature display
196+
197+
When a function has [`@overload`](https://docs.python.org/3/library/typing.html#typing.overload) signatures, they are
198+
rendered automatically in the docstring. To disable this globally:
199+
200+
```python
201+
typehints_document_overloads = False
202+
```
203+
204+
To disable overloads for a single function while keeping them everywhere else, add `:no-overloads:` to the docstring:
205+
206+
```python
207+
@overload
208+
def f(x: int) -> str: ...
209+
@overload
210+
def f(x: str) -> bool: ...
211+
def f(x):
212+
""":no-overloads:
213+
214+
f accepts int or str, see docs for details.
215+
"""
216+
```
217+
218+
The `:no-overloads:` directive is stripped from the rendered output.
219+
194220
### Keep type hints in function signatures
195221

196222
By default, type hints are removed from function signatures and shown in the parameter list below. To keep them visible
@@ -326,20 +352,21 @@ To suppress only specific warning types, see [Warning categories](#warning-categ
326352

327353
### Configuration options
328354

329-
| Option | Default | Description |
330-
| -------------------------------- | ------- | --------------------------------------------------------------------------------------------- |
331-
| `typehints_document_rtype` | `True` | Show the return type in docs. |
332-
| `typehints_document_rtype_none` | `True` | Show return type when it's `None`. |
333-
| `typehints_use_rtype` | `True` | Show return type as a separate block. When `False`, it's inlined with the return description. |
334-
| `always_use_bars_union` | `False` | Use `X \| Y` instead of `Union[X, Y]`. Always on for Python 3.14+. |
335-
| `simplify_optional_unions` | `True` | Flatten `Optional[Union[A, B]]` to `Union[A, B, None]`. |
336-
| `typehints_defaults` | `None` | Show default values: `"comma"`, `"braces"`, or `"braces-after"`. |
337-
| `typehints_use_signature` | `False` | Keep parameter types in the function signature. |
338-
| `typehints_use_signature_return` | `False` | Keep the return type in the function signature. |
339-
| `typehints_fully_qualified` | `False` | Show full module path for types (e.g., `module.Class` not `Class`). |
340-
| `always_document_param_types` | `False` | Add types even for parameters that don't have a `:param:` entry in the docstring. |
341-
| `typehints_formatter` | `None` | A function `(annotation, Config) -> str \| None` for custom type rendering. |
342-
| `typehints_fixup_module_name` | `None` | A function `(str) -> str` to rewrite module paths before generating cross-reference links. |
355+
| Option | Default | Description |
356+
| -------------------------------- | ------- | -------------------------------------------------------------------------------------------------- |
357+
| `typehints_document_rtype` | `True` | Show the return type in docs. |
358+
| `typehints_document_rtype_none` | `True` | Show return type when it's `None`. |
359+
| `typehints_document_overloads` | `True` | Show `@overload` signatures in docs. Use `:no-overloads:` in a docstring for per-function control. |
360+
| `typehints_use_rtype` | `True` | Show return type as a separate block. When `False`, it's inlined with the return description. |
361+
| `always_use_bars_union` | `False` | Use `X \| Y` instead of `Union[X, Y]`. Always on for Python 3.14+. |
362+
| `simplify_optional_unions` | `True` | Flatten `Optional[Union[A, B]]` to `Union[A, B, None]`. |
363+
| `typehints_defaults` | `None` | Show default values: `"comma"`, `"braces"`, or `"braces-after"`. |
364+
| `typehints_use_signature` | `False` | Keep parameter types in the function signature. |
365+
| `typehints_use_signature_return` | `False` | Keep the return type in the function signature. |
366+
| `typehints_fully_qualified` | `False` | Show full module path for types (e.g., `module.Class` not `Class`). |
367+
| `always_document_param_types` | `False` | Add types even for parameters that don't have a `:param:` entry in the docstring. |
368+
| `typehints_formatter` | `None` | A function `(annotation, Config) -> str \| None` for custom type rendering. |
369+
| `typehints_fixup_module_name` | `None` | A function `(str) -> str` to rewrite module paths before generating cross-reference links. |
343370

344371
### Warning categories
345372

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,23 +191,38 @@ def _inject_overload_signatures(
191191
obj: Any,
192192
lines: list[str],
193193
) -> bool:
194-
if what not in {"function", "method"}:
194+
if what not in {"function", "method"} or not app.config.typehints_document_overloads:
195195
return False
196+
if _strip_no_overloads_directive(lines):
197+
return False
198+
if (overloads := _resolve_overloads(obj)) is None:
199+
return False
200+
for line in reversed(_format_overload_lines(overloads, app)):
201+
lines.insert(0, line)
202+
return True
196203

204+
205+
def _strip_no_overloads_directive(lines: list[str]) -> bool:
206+
for idx, line in enumerate(lines):
207+
if line.strip() == ":no-overloads:":
208+
del lines[idx]
209+
return True
210+
return False
211+
212+
213+
def _resolve_overloads(obj: Any) -> list[inspect.Signature] | None:
197214
module_name = getattr(obj, "__module__", None)
198215
if not module_name or module_name not in _OVERLOADS_CACHE:
199-
return False
200-
216+
return None
201217
qualname = getattr(obj, "__qualname__", None)
202218
if not qualname:
203-
return False
219+
return None
220+
return _OVERLOADS_CACHE[module_name].get(qualname) or None
204221

205-
overloads = _OVERLOADS_CACHE[module_name].get(qualname)
206-
if not overloads:
207-
return False
208222

223+
def _format_overload_lines(overloads: list[inspect.Signature], app: Sphinx) -> list[str]:
209224
short_literals = app.config.python_display_short_literal_types
210-
overload_lines = [":Overloads:"]
225+
result = [":Overloads:"]
211226
for overload_sig in overloads:
212227
params = []
213228
for param_name, param in overload_sig.parameters.items():
@@ -226,13 +241,9 @@ def _inject_overload_signatures(
226241
formatted_return = add_type_css_class(formatted_return)
227242
return_annotation = f" \u2192 {formatted_return}"
228243

229-
sig_line = f" * {', '.join(params)}{return_annotation}"
230-
overload_lines.append(sig_line)
231-
232-
overload_lines.append("")
233-
for line in reversed(overload_lines):
234-
lines.insert(0, line)
235-
return True
244+
result.append(f" * {', '.join(params)}{return_annotation}")
245+
result.append("")
246+
return result
236247

237248

238249
def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: # noqa: FBT001
@@ -413,6 +424,7 @@ def setup(app: Sphinx) -> dict[str, bool]:
413424
app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003
414425
app.add_config_value("always_use_bars_union", False, "env") # noqa: FBT003
415426
app.add_config_value("typehints_formatter", None, "env")
427+
app.add_config_value("typehints_document_overloads", True, "env") # noqa: FBT003
416428
app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003
417429
app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003
418430
app.add_config_value("typehints_fixup_module_name", None, "env")

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def make_docstring_app(**overrides: object) -> Sphinx:
6868
"typehints_defaults": None,
6969
"always_document_param_types": False,
7070
"python_display_short_literal_types": False,
71+
"typehints_document_overloads": True,
7172
}
7273
defaults.update(overrides)
7374
config = create_autospec(Config, **defaults) # ty: ignore[invalid-argument-type]

tests/test_init.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,64 @@ def fake_method(self: object, x: int) -> str: ...
130130
assert kwargs["location"] == get_obj_location(fake_method)
131131

132132

133+
def test_inject_overload_global_disable() -> None:
134+
obj = MagicMock()
135+
obj.__module__ = "test_mod"
136+
obj.__qualname__ = "func"
137+
138+
sig = inspect.Signature(
139+
parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int)],
140+
return_annotation=str,
141+
)
142+
_OVERLOADS_CACHE["test_mod"] = {"func": [sig]}
143+
try:
144+
app = make_docstring_app(typehints_document_overloads=False)
145+
lines: list[str] = []
146+
assert _inject_overload_signatures(app, "function", "name", obj, lines) is False
147+
assert lines == []
148+
finally:
149+
_OVERLOADS_CACHE.pop("test_mod", None)
150+
151+
152+
def test_inject_overload_local_no_overloads_directive() -> None:
153+
obj = MagicMock()
154+
obj.__module__ = "test_mod"
155+
obj.__qualname__ = "func"
156+
157+
sig = inspect.Signature(
158+
parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int)],
159+
return_annotation=str,
160+
)
161+
_OVERLOADS_CACHE["test_mod"] = {"func": [sig]}
162+
try:
163+
app = make_docstring_app()
164+
lines = [":no-overloads:", "", "Some docstring."]
165+
assert _inject_overload_signatures(app, "function", "name", obj, lines) is False
166+
assert ":no-overloads:" not in lines
167+
assert lines == ["", "Some docstring."]
168+
finally:
169+
_OVERLOADS_CACHE.pop("test_mod", None)
170+
171+
172+
def test_inject_overload_local_directive_with_global_enabled() -> None:
173+
obj = MagicMock()
174+
obj.__module__ = "test_mod"
175+
obj.__qualname__ = "func"
176+
177+
sig = inspect.Signature(
178+
parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int)],
179+
return_annotation=str,
180+
)
181+
_OVERLOADS_CACHE["test_mod"] = {"func": [sig]}
182+
try:
183+
app = make_docstring_app(typehints_document_overloads=True)
184+
lines = [":no-overloads:", "", "Docs here."]
185+
assert _inject_overload_signatures(app, "function", "name", obj, lines) is False
186+
assert ":no-overloads:" not in lines
187+
finally:
188+
_OVERLOADS_CACHE.pop("test_mod", None)
189+
190+
133191
def test_inject_types_no_signature() -> None:
134192
"""Branch 261->263: signature is None skips _inject_signature."""
135193

tests/test_integration.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,43 @@ def overload_with_complex_types(x: list[int] | dict[str, int]) -> dict[str, int]
708708
"""
709709

710710

711+
@overload
712+
def func_with_overload_no_overloads(a: int, b: int) -> None: ...
713+
714+
715+
@overload
716+
def func_with_overload_no_overloads(a: str, b: str) -> None: ...
717+
718+
719+
@expected(
720+
"""\
721+
mod.func_with_overload_no_overloads(a, b)
722+
723+
Accepts int or str pairs, see docs for details.
724+
725+
Parameters:
726+
* **a** ("int" | "str") -- The first thing
727+
728+
* **b** ("int" | "str") -- The second thing
729+
730+
Return type:
731+
"None"
732+
""",
733+
)
734+
def func_with_overload_no_overloads(a: Union[int, str], b: Union[int, str]) -> None:
735+
""":no-overloads:
736+
737+
Accepts int or str pairs, see docs for details.
738+
739+
Parameters
740+
----------
741+
a:
742+
The first thing
743+
b:
744+
The second thing
745+
"""
746+
747+
711748
@expected(
712749
"""\
713750
mod.func_literals_long_format(a, b)

0 commit comments

Comments
 (0)