Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 7 additions & 24 deletions src/docstub/_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,30 +677,11 @@ def attributes(self):
-------
attributes : dict[str, Annotation]
A dictionary mapping attribute names to their annotations.
Attributes without annotations fall back to :class:`_typeshed.Incomplete`.
Attributes without annotations fall back to
:class:`FallbackAnnotation` which corresponds to
:class:`_typeshed.Incomplete`.
"""
annotations = {}
for attribute in self.np_docstring["Attributes"]:
self._handle_missing_whitespace(attribute)
if not attribute.type:
continue

ds_line = 0
for i, line in enumerate(self.docstring.split("\n")):
if attribute.name in line and attribute.type in line:
ds_line = i
break

if attribute.name in annotations:
self.reporter.message(
"duplicate attribute name in docstring",
details=self.reporter.underline(attribute.name),
)
continue

annotation = self._doctype_to_annotation(attribute.type, ds_line=ds_line)
annotations[attribute.name.strip()] = annotation

annotations = self._section_annotations("Attributes")
return annotations

@cached_property
Expand All @@ -711,7 +692,9 @@ def parameters(self):
-------
parameters : dict[str, Annotation]
A dictionary mapping parameters names to their annotations.
Parameters without annotations fall back to :class:`_typeshed.Incomplete`.
Parameters without annotations fall back to
:class:`FallbackAnnotation` which corresponds to
:class:`_typeshed.Incomplete`.
"""
param_section = self._section_annotations("Parameters")
other_section = self._section_annotations("Other Parameters")
Expand Down
4 changes: 2 additions & 2 deletions src/docstub/_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import libcst.matchers as cstm

from ._analysis import PyImport
from ._docstrings import DocstringAnnotations, DoctypeTransformer
from ._docstrings import DocstringAnnotations, DoctypeTransformer, FallbackAnnotation
from ._utils import ErrorReporter, module_name_from_path

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -701,7 +701,7 @@ def leave_AnnAssign(self, original_node, updated_node):
updated_node.annotation, annotation=expr
)

else:
elif pytype != FallbackAnnotation:
# Notify about ignored docstring annotation
# TODO: either remove message or print only in verbose mode
position = self.get_metadata(
Expand Down
104 changes: 92 additions & 12 deletions tests/test_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,13 @@ def test_keep_assign_param(self):
result = transformer.python_to_stub(source)
assert expected == result

def test_keep_inline_assign_with_doctype(self, capsys):
def test_module_assign_conflict(self, capsys):
source = dedent(
'''
"""
Attributes
----------
a : Sized
a : int
"""
a: str
'''
Expand All @@ -334,9 +334,43 @@ def test_keep_inline_assign_with_doctype(self, capsys):
assert expected == result

captured = capsys.readouterr()
assert "Keeping existing inline annotation for assignment" in captured.out
assert captured.out == (
"Keeping existing inline annotation for assignment\n"
" str\n"
" ^^^ ignoring docstring: int\n"
"\n"
)

def test_keep_class_assign_param(self):
def test_module_assign_no_conflict(self, capsys):
source = dedent(
'''
"""
Attributes
----------
a :
No type info here; it's already inlined.
b : float
"""
a: int
b = 3
'''
)
expected = dedent(
"""
a: int
b: float
"""
)
transformer = Py2StubTransformer()
result = transformer.python_to_stub(source)
assert expected == result

# No warning should have been raised, since there is no conflict
# between docstring and inline annotation
output = capsys.readouterr()
assert output.out == ""

def test_class_assign_keep_inline_annotation(self):
source = dedent(
"""
class Foo:
Expand All @@ -353,7 +387,7 @@ class Foo:
result = transformer.python_to_stub(source)
assert expected == result

def test_keep_inline_class_assign_with_doctype(self, capsys):
def test_class_assign_conflict(self, capsys):
source = dedent(
'''
class Foo:
Expand All @@ -376,9 +410,45 @@ class Foo:
assert expected == result

captured = capsys.readouterr()
assert "Keeping existing inline annotation for assignment" in captured.out
assert captured.out == (
"Keeping existing inline annotation for assignment\n"
" str\n"
" ^^^ ignoring docstring: Sized\n"
"\n"
)

def test_keep_inline_param(self):
def test_class_assign_no_conflict(self, capsys):
source = dedent(
'''
class Foo:
"""
Attributes
----------
a :
No type info here; it's already inlined.
b : float
"""
a: int
b = 3
'''
)
expected = dedent(
"""
class Foo:
a: int
b: float
"""
)
transformer = Py2StubTransformer()
result = transformer.python_to_stub(source)
assert expected == result

# No warning should have been raised, since there is no conflict
# between docstring and inline annotation
output = capsys.readouterr()
assert output.out == ""

def test_param_keep_inline_annotation(self):
source = dedent(
"""
def foo(a: str) -> None:
Expand All @@ -394,7 +464,7 @@ def foo(a: str) -> None: ...
result = transformer.python_to_stub(source)
assert expected == result

def test_keep_inline_param_with_doctype(self, capsys):
def test_param_conflict(self, capsys):
source = dedent(
'''
def foo(a: int) -> None:
Expand All @@ -416,9 +486,14 @@ def foo(a: int) -> None: ...
assert expected == result

captured = capsys.readouterr()
assert "Keeping existing inline parameter annotation" in captured.out
assert captured.out == (
"Keeping existing inline parameter annotation\n"
" int\n"
" ^^^ ignoring docstring: Sized\n"
"\n"
)

def test_keep_inline_return(self):
def test_return_keep_inline_annotation(self):
source = dedent(
"""
def foo() -> str:
Expand All @@ -434,7 +509,7 @@ def foo() -> str: ...
result = transformer.python_to_stub(source)
assert expected == result

def test_keep_inline_return_with_doctype(self, capsys):
def test_return_conflict(self, capsys):
source = dedent(
'''
def foo() -> int:
Expand All @@ -456,7 +531,12 @@ def foo() -> int: ...
assert expected == result

captured = capsys.readouterr()
assert "Keeping existing inline return annotation" in captured.out
assert captured.out == (
"Keeping existing inline return annotation\n"
" int\n"
" ^^^ ignoring docstring: Sized\n"
"\n"
)

def test_preserved_type_comment(self):
source = dedent(
Expand Down