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
48 changes: 46 additions & 2 deletions libcst/codemod/commands/convert_type_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing_extensions import TypeAlias

import libcst as cst
import libcst.matchers as m
from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand


Expand Down Expand Up @@ -285,6 +286,7 @@ def is_empty(self) -> bool:
def from_cst(
cls,
node_cst: cst.FunctionDef,
is_method: bool,
) -> "FunctionTypeInfo":
"""
Using the `ast` type comment extraction logic, get type information
Expand Down Expand Up @@ -350,6 +352,18 @@ def from_cst(
},
returns=returns,
)
elif is_method and len(argtypes) == len(args) - 1:
# Merge as above, but skip merging the initial `self` or `cls` arg.
return cls(
arguments={
args[0].arg: args[0].type_comment,
**{
arg.arg: arg.type_comment or ast.unparse(from_func_type)
for arg, from_func_type in zip(args[1:], argtypes)
},
},
returns=returns,
)
else:
# On arity mismatches, ignore the type information
return cls({}, None)
Expand Down Expand Up @@ -623,10 +637,16 @@ def leave_With(
# (B) we also manually reach down to the first statement inside of the
# funciton body and aggressively strip type comments from leading
# whitespaces
#
# PEP 484 underspecifies how to apply type comments to (non-static)
# methods - it would be possible to provide a type for `self`, or to omit
# it. So we accept either approach when interpreting type comments on
# non-static methods: the first argument an have a type provided or not.

def visit_FunctionDef(
def _visit_FunctionDef(
self,
node: cst.FunctionDef,
is_method: bool,
) -> None:
"""
Set up the data we need to handle function definitions:
Expand All @@ -636,11 +656,35 @@ def visit_FunctionDef(
- Set that we are aggressively stripping type comments, which will
remain true until we visit the body.
"""
function_type_info = FunctionTypeInfo.from_cst(node)
function_type_info = FunctionTypeInfo.from_cst(node, is_method=is_method)
self.aggressively_strip_type_comments = not function_type_info.is_empty()
self.function_type_info_stack.append(function_type_info)
self.function_body_stack.append(node.body)

@m.call_if_not_inside(m.ClassDef())
@m.visit(m.FunctionDef())
def visit_method(
self,
node: cst.FunctionDef,
) -> None:
return self._visit_FunctionDef(
node=node,
is_method=False,
)

@m.call_if_inside(m.ClassDef())
@m.visit(m.FunctionDef())
def visit_function(
self,
node: cst.FunctionDef,
) -> None:
return self._visit_FunctionDef(
node=node,
is_method=not any(
m.matches(d.decorator, m.Name("staticmethod")) for d in node.decorators
),
)

def leave_TrailingWhitespace(
self,
original_node: cst.TrailingWhitespace,
Expand Down
67 changes: 67 additions & 0 deletions libcst/codemod/commands/tests/test_convert_type_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,73 @@ def f(
"""
self.assertCodemod39Plus(before, after)

def test_method_transforms(self) -> None:
before = """
class A:

def __init__(self, thing): # type: (str) -> None
self.thing = thing

@classmethod
def make(cls): # type: () -> A
return cls("thing")

@staticmethod
def f(x, y): # type: (object, object) -> None
pass

def method0(
self,
other_thing,
): # type: (str) -> bool
return self.thing == other_thing

def method1(
self, # type: A
other_thing, # type: str
): # type: (int) -> bool
return self.thing == other_thing

def method2(
self,
other_thing,
): # type: (A, str) -> bool
return self.thing == other_thing
"""
after = """
class A:

def __init__(self, thing: str) -> None:
self.thing = thing

@classmethod
def make(cls) -> "A":
return cls("thing")

@staticmethod
def f(x: object, y: object) -> None:
pass

def method0(
self,
other_thing: str,
) -> bool:
return self.thing == other_thing

def method1(
self: "A",
other_thing: str,
) -> bool:
return self.thing == other_thing

def method2(
self: "A",
other_thing: str,
) -> bool:
return self.thing == other_thing
"""
self.assertCodemod39Plus(before, after)

def test_no_change_if_function_type_comments_unused(self) -> None:
before = """
# arity error in arguments
Expand Down