Skip to content

Commit e0c39be

Browse files
committed
Support annotating class attributes
1 parent 3dbbb43 commit e0c39be

2 files changed

Lines changed: 131 additions & 5 deletions

File tree

src/docstub/_stubs.py

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,22 @@ def current_source(self, value):
337337
if self.types_db is not None:
338338
self.types_db.current_source = value
339339

340+
@property
341+
def is_inside_function_def(self):
342+
"""Check whether the current scope is within a function.
343+
344+
Returns
345+
-------
346+
out : bool
347+
"""
348+
inside_function_def = self._scope_stack[-1].type in (
349+
ScopeType.FUNC,
350+
ScopeType.METHOD,
351+
ScopeType.CLASSMETHOD,
352+
ScopeType.STATICMETHOD,
353+
)
354+
return inside_function_def
355+
340356
def python_to_stub(self, source, *, module_path=None):
341357
"""Convert Python source code to stub-file ready code.
342358
@@ -386,7 +402,10 @@ def visit_ClassDef(self, node):
386402
return True
387403

388404
def leave_ClassDef(self, original_node, updated_node):
389-
"""Drop class scope from the stack.
405+
"""Finalize class definition.
406+
407+
If the docstring documents attributes, make sure to insert them as (instance)
408+
attributes for the class. Also drop class scope from the stack.
390409
391410
Parameters
392411
----------
@@ -397,6 +416,11 @@ def leave_ClassDef(self, original_node, updated_node):
397416
-------
398417
updated_node : cst.ClassDef
399418
"""
419+
pytypes = self._pytypes_stack[-1]
420+
if pytypes and pytypes.attributes:
421+
updated_node = self._insert_instance_attributes(
422+
updated_node, pytypes.attributes
423+
)
400424
self._scope_stack.pop()
401425
self._pytypes_stack.pop()
402426
return updated_node
@@ -418,6 +442,32 @@ def visit_FunctionDef(self, node):
418442
self._pytypes_stack.append(pytypes)
419443
return True
420444

445+
def visit_IndentedBlock(self, node):
446+
"""Skip function body.
447+
448+
Parameters
449+
----------
450+
node : cst.IndentedBlock
451+
452+
Returns
453+
-------
454+
out : bool
455+
"""
456+
return not self.is_inside_function_def
457+
458+
def visit_SimpleStatementSuite(self, node):
459+
"""Skip statement suites inside functions.
460+
461+
Parameters
462+
----------
463+
node : cst.SimpleStatementSuite
464+
465+
Returns
466+
-------
467+
out : bool
468+
"""
469+
return not self.is_inside_function_def
470+
421471
def leave_FunctionDef(self, original_node, updated_node):
422472
"""Add type annotation for return to function.
423473
@@ -671,15 +721,12 @@ def leave_Module(self, original_node, updated_node):
671721
if self.current_source:
672722
current_module = module_name_from_path(self.current_source)
673723
required_imports = [
674-
imp
675-
for imp in self._required_imports
676-
if imp.import_path != current_module
724+
imp for imp in required_imports if imp.import_path != current_module
677725
]
678726
import_nodes = self._parse_imports(
679727
required_imports, current_module=current_module
680728
)
681729
updated_node = updated_node.with_changes(
682-
header=[self._docstub_generated_comment],
683730
body=import_nodes + updated_node.body,
684731
)
685732
self._scope_stack.pop()
@@ -744,6 +791,7 @@ def _parse_imports(imports, *, current_module=None):
744791
import_nodes : tuple[cst.SimpleStatementLine, ...]
745792
"""
746793
lines = {imp.format_import(relative_to=current_module) for imp in imports}
794+
lines = sorted(lines)
747795
import_nodes = tuple(cst.parse_statement(line) for line in lines)
748796
return import_nodes
749797

@@ -840,3 +888,39 @@ def _create_annotated_assign(self, *, name, trailing_semicolon=False):
840888
semicolon=semicolon,
841889
)
842890
return node
891+
892+
def _insert_instance_attributes(self, updated_node, attributes):
893+
"""Insert instance attributes into ClassDef node.
894+
895+
Instance attributes of classes are usually initialized inside the ``__init__``
896+
or other methods, whose body this transformer doesn't visit. Instead, we rely
897+
on the "Attributes" section in the docstring to make those available. If
898+
attributes are found in the docstring, we need to make sure that they are
899+
inserted into the class scope / definition.
900+
901+
Parameters
902+
----------
903+
updated_node : cst.ClassDef
904+
attributes : dict[str, ~.Annotation]
905+
906+
Returns
907+
-------
908+
updated_node : cst.ClassDef
909+
"""
910+
to_insert = []
911+
for name in attributes:
912+
attribute_exists = any(
913+
cstm.findall(updated_node, cstm.AnnAssign(target=cstm.Name(name)))
914+
)
915+
if attribute_exists:
916+
continue
917+
918+
assign = self._create_annotated_assign(name=name)
919+
stmnt_line = cst.SimpleStatementLine(body=[assign])
920+
to_insert.append(stmnt_line)
921+
922+
updated_node = updated_node.with_deep_changes(
923+
updated_node.body, body=tuple(to_insert) + updated_node.body.body
924+
)
925+
926+
return updated_node

tests/test_stubs.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,48 @@ def test_attributes_with_doctype(self, assign, doctype, expected, scope):
226226
assert "from _typeshed import Incomplete" in result
227227
# fmt: on
228228

229+
def test_class_init_attributes(self):
230+
src = dedent(
231+
"""
232+
class Foo:
233+
'''
234+
Attributes
235+
----------
236+
a : int
237+
b : float
238+
c : tuple
239+
d : ClassVar[bool]
240+
'''
241+
242+
c: list
243+
d = True
244+
245+
def __init__(self, a):
246+
self.a = a
247+
self.e = None
248+
"""
249+
)
250+
expected = dedent(
251+
"""
252+
from _typeshed import Incomplete as ClassVar
253+
from _typeshed import Incomplete as bool
254+
from _typeshed import Incomplete as float
255+
from _typeshed import Incomplete as int
256+
from _typeshed import Incomplete as tuple
257+
class Foo:
258+
a: int
259+
b: float
260+
261+
c: tuple
262+
d: ClassVar[bool]
263+
264+
def __init__(self, a) -> None: ...
265+
"""
266+
)
267+
transformer = Py2StubTransformer()
268+
result = transformer.python_to_stub(src)
269+
assert result == expected
270+
229271
def test_undocumented_objects(self):
230272
# TODO test undocumented objects
231273
# https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#undocumented-objects

0 commit comments

Comments
 (0)