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..6da2098 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,101 @@ 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_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( @@ -515,6 +614,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(