From 8492ebc3b438da72ce006a0f6d6f47adf67b7916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 31 Oct 2025 23:50:40 +0100 Subject: [PATCH 1/2] Fix missing warning if only one parameter is missing Previously, the test cases would not raise a warning. --- src/docstub/_stubs.py | 7 ++++-- tests/test_stubs.py | 50 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/docstub/_stubs.py b/src/docstub/_stubs.py index 7947c04..dc50d65 100644 --- a/src/docstub/_stubs.py +++ b/src/docstub/_stubs.py @@ -572,8 +572,11 @@ def leave_Param(self, original_node, updated_node): details=details, ) - # Potentially use "Incomplete" except for first param in (class)methods - elif not is_self_or_cls and updated_node.annotation is None: + has_missing_annotation = ( + "annotation" not in node_changes and updated_node.annotation is None + ) + # Fallback to "Incomplete" except for first param in (class)methods + if has_missing_annotation and not is_self_or_cls: node_changes["annotation"] = self._Annotation_Incomplete import_ = PyImport.typeshed_Incomplete() self._required_imports.add(import_) diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 83876c3..624f5c1 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -1,3 +1,4 @@ +import logging import re from textwrap import dedent @@ -278,6 +279,7 @@ def __init__(self, a): ) expected = dedent( """ + from _typeshed import Incomplete from typing import ClassVar class Foo: a: int @@ -286,7 +288,7 @@ class Foo: c: list d: ClassVar[bool] - def __init__(self, a) -> None: ... + def __init__(self, a: Incomplete) -> None: ... """ ) transformer = Py2StubTransformer() @@ -335,6 +337,7 @@ def test_module_assign_conflict(self, caplog): assert caplog.messages == ["Keeping existing inline annotation for assignment"] assert "ignoring docstring: int" in caplog.records[0].details + assert caplog.records[0].levelno == logging.WARNING def test_module_assign_no_conflict(self, capsys): source = dedent( @@ -405,6 +408,7 @@ class Foo: assert expected == result assert caplog.messages == ["Keeping existing inline annotation for assignment"] + assert caplog.records[0].levelno == logging.WARNING def test_class_assign_no_conflict(self, caplog): source = dedent( @@ -475,6 +479,49 @@ def foo(a: int) -> None: ... assert caplog.messages == ["Keeping existing inline parameter annotation"] assert "ignoring docstring: Sized" in caplog.records[0].details + assert caplog.records[0].levelno == logging.WARNING + + def test_missing_param(self, caplog): + source = dedent( + ''' + def foo(a, b) -> None: + """ + Parameters + ---------- + a : int + """ + ''' + ) + expected = dedent( + """ + from _typeshed import Incomplete + def foo(a: int, b: Incomplete) -> None: ... + """ + ) + transformer = Py2StubTransformer() + result = transformer.python_to_stub(source) + assert expected == result + assert caplog.messages == ["Missing annotation for parameter 'b'"] + assert caplog.records[0].levelno == logging.WARNING + + def test_missing_param_inline(self, caplog): + source = dedent( + """ + def foo(a: int, b) -> None: + pass + """ + ) + expected = dedent( + """ + from _typeshed import Incomplete + def foo(a: int, b: Incomplete) -> None: ... + """ + ) + transformer = Py2StubTransformer() + result = transformer.python_to_stub(source) + assert expected == result + assert caplog.messages == ["Missing annotation for parameter 'b'"] + assert caplog.records[0].levelno == logging.WARNING def test_return_keep_inline_annotation(self): source = dedent( @@ -515,6 +562,7 @@ def foo() -> int: ... assert caplog.messages == ["Keeping existing inline return annotation"] assert "ignoring docstring: Sized" in caplog.records[0].details + assert caplog.records[0].levelno == logging.WARNING def test_preserved_type_comment(self): source = dedent( From 0f0e93ef6aaf69ea9373ee428369543169ceaab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 31 Oct 2025 23:59:57 +0100 Subject: [PATCH 2/2] Add test for missing attributes too --- tests/test_stubs.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 624f5c1..6da2098 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -523,6 +523,58 @@ def foo(a: int, b: Incomplete) -> None: ... assert caplog.messages == ["Missing annotation for parameter 'b'"] assert caplog.records[0].levelno == logging.WARNING + def test_missing_attr(self, caplog): + source = dedent( + ''' + class Foo: + """ + Attributes + ---------- + a : ClassVar[int] + """ + a = 3 + b = True + ''' + ) + expected = dedent( + """ + from _typeshed import Incomplete + from typing import ClassVar + class Foo: + a: ClassVar[int] + b: Incomplete + """ + ) + transformer = Py2StubTransformer() + result = transformer.python_to_stub(source) + assert expected == result + assert caplog.messages == ["Missing annotation for assignment 'b'"] + assert caplog.records[0].levelno == logging.WARNING + + def test_missing_attr_inline(self, caplog): + source = dedent( + """ + from typing import ClassVar + class Foo: + a: ClassVar[int] = 3 + b = True + """ + ) + expected = dedent( + """ + from _typeshed import Incomplete + from typing import ClassVar + class Foo: + a: ClassVar[int] + b: Incomplete + """ + ) + transformer = Py2StubTransformer() + result = transformer.python_to_stub(source) + assert expected == result + assert caplog.messages == ["Missing annotation for assignment 'b'"] + assert caplog.records[0].levelno == logging.WARNING + def test_return_keep_inline_annotation(self): source = dedent( """