Skip to content

Commit a13ad28

Browse files
authored
Improve handling of inline annotations without conflicts (#66)
Make sure that no warning is raised for inline annotations, if there is no conflicting annotation in a docstring. Otherwise, the inlined annotation takes precedence and a warning is printed. * Don't raise note for inline annotated attributes when the docstring doesn't contain a type description. In which case a `FallbackAnnotation` is provided. Also simplifies the redundant implementation of `DocstringAnnotations.attributes`. * Improve tests for doctype-inline conflicts
1 parent d2a1dfd commit a13ad28

3 files changed

Lines changed: 101 additions & 38 deletions

File tree

src/docstub/_docstrings.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -677,30 +677,11 @@ def attributes(self):
677677
-------
678678
attributes : dict[str, Annotation]
679679
A dictionary mapping attribute names to their annotations.
680-
Attributes without annotations fall back to :class:`_typeshed.Incomplete`.
680+
Attributes without annotations fall back to
681+
:class:`FallbackAnnotation` which corresponds to
682+
:class:`_typeshed.Incomplete`.
681683
"""
682-
annotations = {}
683-
for attribute in self.np_docstring["Attributes"]:
684-
self._handle_missing_whitespace(attribute)
685-
if not attribute.type:
686-
continue
687-
688-
ds_line = 0
689-
for i, line in enumerate(self.docstring.split("\n")):
690-
if attribute.name in line and attribute.type in line:
691-
ds_line = i
692-
break
693-
694-
if attribute.name in annotations:
695-
self.reporter.message(
696-
"duplicate attribute name in docstring",
697-
details=self.reporter.underline(attribute.name),
698-
)
699-
continue
700-
701-
annotation = self._doctype_to_annotation(attribute.type, ds_line=ds_line)
702-
annotations[attribute.name.strip()] = annotation
703-
684+
annotations = self._section_annotations("Attributes")
704685
return annotations
705686

706687
@cached_property
@@ -711,7 +692,9 @@ def parameters(self):
711692
-------
712693
parameters : dict[str, Annotation]
713694
A dictionary mapping parameters names to their annotations.
714-
Parameters without annotations fall back to :class:`_typeshed.Incomplete`.
695+
Parameters without annotations fall back to
696+
:class:`FallbackAnnotation` which corresponds to
697+
:class:`_typeshed.Incomplete`.
715698
"""
716699
param_section = self._section_annotations("Parameters")
717700
other_section = self._section_annotations("Other Parameters")

src/docstub/_stubs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import libcst.matchers as cstm
1616

1717
from ._analysis import PyImport
18-
from ._docstrings import DocstringAnnotations, DoctypeTransformer
18+
from ._docstrings import DocstringAnnotations, DoctypeTransformer, FallbackAnnotation
1919
from ._utils import ErrorReporter, module_name_from_path
2020

2121
logger = logging.getLogger(__name__)
@@ -701,7 +701,7 @@ def leave_AnnAssign(self, original_node, updated_node):
701701
updated_node.annotation, annotation=expr
702702
)
703703

704-
else:
704+
elif pytype != FallbackAnnotation:
705705
# Notify about ignored docstring annotation
706706
# TODO: either remove message or print only in verbose mode
707707
position = self.get_metadata(

tests/test_stubs.py

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,13 @@ def test_keep_assign_param(self):
313313
result = transformer.python_to_stub(source)
314314
assert expected == result
315315

316-
def test_keep_inline_assign_with_doctype(self, capsys):
316+
def test_module_assign_conflict(self, capsys):
317317
source = dedent(
318318
'''
319319
"""
320320
Attributes
321321
----------
322-
a : Sized
322+
a : int
323323
"""
324324
a: str
325325
'''
@@ -334,9 +334,43 @@ def test_keep_inline_assign_with_doctype(self, capsys):
334334
assert expected == result
335335

336336
captured = capsys.readouterr()
337-
assert "Keeping existing inline annotation for assignment" in captured.out
337+
assert captured.out == (
338+
"Keeping existing inline annotation for assignment\n"
339+
" str\n"
340+
" ^^^ ignoring docstring: int\n"
341+
"\n"
342+
)
338343

339-
def test_keep_class_assign_param(self):
344+
def test_module_assign_no_conflict(self, capsys):
345+
source = dedent(
346+
'''
347+
"""
348+
Attributes
349+
----------
350+
a :
351+
No type info here; it's already inlined.
352+
b : float
353+
"""
354+
a: int
355+
b = 3
356+
'''
357+
)
358+
expected = dedent(
359+
"""
360+
a: int
361+
b: float
362+
"""
363+
)
364+
transformer = Py2StubTransformer()
365+
result = transformer.python_to_stub(source)
366+
assert expected == result
367+
368+
# No warning should have been raised, since there is no conflict
369+
# between docstring and inline annotation
370+
output = capsys.readouterr()
371+
assert output.out == ""
372+
373+
def test_class_assign_keep_inline_annotation(self):
340374
source = dedent(
341375
"""
342376
class Foo:
@@ -353,7 +387,7 @@ class Foo:
353387
result = transformer.python_to_stub(source)
354388
assert expected == result
355389

356-
def test_keep_inline_class_assign_with_doctype(self, capsys):
390+
def test_class_assign_conflict(self, capsys):
357391
source = dedent(
358392
'''
359393
class Foo:
@@ -376,9 +410,45 @@ class Foo:
376410
assert expected == result
377411

378412
captured = capsys.readouterr()
379-
assert "Keeping existing inline annotation for assignment" in captured.out
413+
assert captured.out == (
414+
"Keeping existing inline annotation for assignment\n"
415+
" str\n"
416+
" ^^^ ignoring docstring: Sized\n"
417+
"\n"
418+
)
380419

381-
def test_keep_inline_param(self):
420+
def test_class_assign_no_conflict(self, capsys):
421+
source = dedent(
422+
'''
423+
class Foo:
424+
"""
425+
Attributes
426+
----------
427+
a :
428+
No type info here; it's already inlined.
429+
b : float
430+
"""
431+
a: int
432+
b = 3
433+
'''
434+
)
435+
expected = dedent(
436+
"""
437+
class Foo:
438+
a: int
439+
b: float
440+
"""
441+
)
442+
transformer = Py2StubTransformer()
443+
result = transformer.python_to_stub(source)
444+
assert expected == result
445+
446+
# No warning should have been raised, since there is no conflict
447+
# between docstring and inline annotation
448+
output = capsys.readouterr()
449+
assert output.out == ""
450+
451+
def test_param_keep_inline_annotation(self):
382452
source = dedent(
383453
"""
384454
def foo(a: str) -> None:
@@ -394,7 +464,7 @@ def foo(a: str) -> None: ...
394464
result = transformer.python_to_stub(source)
395465
assert expected == result
396466

397-
def test_keep_inline_param_with_doctype(self, capsys):
467+
def test_param_conflict(self, capsys):
398468
source = dedent(
399469
'''
400470
def foo(a: int) -> None:
@@ -416,9 +486,14 @@ def foo(a: int) -> None: ...
416486
assert expected == result
417487

418488
captured = capsys.readouterr()
419-
assert "Keeping existing inline parameter annotation" in captured.out
489+
assert captured.out == (
490+
"Keeping existing inline parameter annotation\n"
491+
" int\n"
492+
" ^^^ ignoring docstring: Sized\n"
493+
"\n"
494+
)
420495

421-
def test_keep_inline_return(self):
496+
def test_return_keep_inline_annotation(self):
422497
source = dedent(
423498
"""
424499
def foo() -> str:
@@ -434,7 +509,7 @@ def foo() -> str: ...
434509
result = transformer.python_to_stub(source)
435510
assert expected == result
436511

437-
def test_keep_inline_return_with_doctype(self, capsys):
512+
def test_return_conflict(self, capsys):
438513
source = dedent(
439514
'''
440515
def foo() -> int:
@@ -456,7 +531,12 @@ def foo() -> int: ...
456531
assert expected == result
457532

458533
captured = capsys.readouterr()
459-
assert "Keeping existing inline return annotation" in captured.out
534+
assert captured.out == (
535+
"Keeping existing inline return annotation\n"
536+
" int\n"
537+
" ^^^ ignoring docstring: Sized\n"
538+
"\n"
539+
)
460540

461541
def test_preserved_type_comment(self):
462542
source = dedent(

0 commit comments

Comments
 (0)