blob: fc8ac27466d5060396efd4e2dfa12070dc3a04ab [file] [log] [blame]
"""Fixer for lib2to3 that inserts mypy annotations into all methods.
The simplest way to run this is to copy it into lib2to3's "fixes"
subdirectory and then run "2to3 -f annotate" over your files.
The fixer transforms e.g.
def foo(self, bar, baz=12):
return bar + baz
into
def foo(self, bar, baz=12):
# type: (Any, int) -> Any
return bar + baz
It does not do type inference but it recognizes some basic default
argument values such as numbers and strings (and assumes their type
implies the argument type).
It also uses some basic heuristics to decide whether to ignore the
first argument:
- always if it's named 'self'
- if there's a @classmethod decorator
Finally, it knows that __init__() is supposed to return None.
"""
from __future__ import annotations
import os
import re
from lib2to3.fixer_base import BaseFix
from lib2to3.fixer_util import syms, token, touch_import
from lib2to3.patcomp import compile_pattern
from lib2to3.pytree import Leaf, Node
class FixAnnotate(BaseFix):
# This fixer is compatible with the bottom matcher.
BM_compatible = True
# This fixer shouldn't run by default.
explicit = True
# The pattern to match.
PATTERN = """
funcdef< 'def' name=any parameters< '(' [args=any] ')' > ':' suite=any+ >
"""
counter = None if not os.getenv("MAXFIXES") else int(os.getenv("MAXFIXES"))
def transform(self, node, results):
if FixAnnotate.counter is not None:
if FixAnnotate.counter <= 0:
return
suite = results["suite"]
children = suite[0].children
# NOTE: I've reverse-engineered the structure of the parse tree.
# It's always a list of nodes, the first of which contains the
# entire suite. Its children seem to be:
#
# [0] NEWLINE
# [1] INDENT
# [2...n-2] statements (the first may be a docstring)
# [n-1] DEDENT
#
# Comments before the suite are part of the INDENT's prefix.
#
# "Compact" functions (e.g. "def foo(x, y): return max(x, y)")
# have a different structure that isn't matched by PATTERN.
#
# print('-'*60)
# print(node)
# for i, ch in enumerate(children):
# print(i, repr(ch.prefix), repr(ch))
#
# Check if there's already an annotation.
for ch in children:
if ch.prefix.lstrip().startswith("# type:"):
return # There's already a # type: comment here; don't change anything.
# Compute the annotation
annot = self.make_annotation(node, results)
# Insert '# type: {annot}' comment.
# For reference, see lib2to3/fixes/fix_tuple_params.py in stdlib.
if len(children) >= 2 and children[1].type == token.INDENT:
children[1].prefix = "{}# type: {}\n{}".format(
children[1].value, annot, children[1].prefix
)
children[1].changed()
if FixAnnotate.counter is not None:
FixAnnotate.counter -= 1
# Also add 'from typing import Any' at the top.
if "Any" in annot:
touch_import("typing", "Any", node)
def make_annotation(self, node, results):
name = results["name"]
assert isinstance(name, Leaf), repr(name)
assert name.type == token.NAME, repr(name)
decorators = self.get_decorators(node)
is_method = self.is_method(node)
if name.value == "__init__" or not self.has_return_exprs(node):
restype = "None"
else:
restype = "Any"
args = results.get("args")
argtypes = []
if isinstance(args, Node):
children = args.children
elif isinstance(args, Leaf):
children = [args]
else:
children = []
# Interpret children according to the following grammar:
# (('*'|'**')? NAME ['=' expr] ','?)*
stars = inferred_type = ""
in_default = False
at_start = True
for child in children:
if isinstance(child, Leaf):
if child.value in ("*", "**"):
stars += child.value
elif child.type == token.NAME and not in_default:
if not is_method or not at_start or "staticmethod" in decorators:
inferred_type = "Any"
else:
# Always skip the first argument if it's named 'self'.
# Always skip the first argument of a class method.
if child.value == "self" or "classmethod" in decorators:
pass
else:
inferred_type = "Any"
elif child.value == "=":
in_default = True
elif in_default and child.value != ",":
if child.type == token.NUMBER:
if re.match(r"\d+[lL]?$", child.value):
inferred_type = "int"
else:
inferred_type = "float" # TODO: complex?
elif child.type == token.STRING:
if child.value.startswith(("u", "U")):
inferred_type = "unicode"
else:
inferred_type = "str"
elif child.type == token.NAME and child.value in ("True", "False"):
inferred_type = "bool"
elif child.value == ",":
if inferred_type:
argtypes.append(stars + inferred_type)
# Reset
stars = inferred_type = ""
in_default = False
at_start = False
if inferred_type:
argtypes.append(stars + inferred_type)
return "(" + ", ".join(argtypes) + ") -> " + restype
# The parse tree has a different shape when there is a single
# decorator vs. when there are multiple decorators.
DECORATED = "decorated< (d=decorator | decorators< dd=decorator+ >) funcdef >"
decorated = compile_pattern(DECORATED)
def get_decorators(self, node):
"""Return a list of decorators found on a function definition.
This is a list of strings; only simple decorators
(e.g. @staticmethod) are returned.
If the function is undecorated or only non-simple decorators
are found, return [].
"""
if node.parent is None:
return []
results = {}
if not self.decorated.match(node.parent, results):
return []
decorators = results.get("dd") or [results["d"]]
decs = []
for d in decorators:
for child in d.children:
if isinstance(child, Leaf) and child.type == token.NAME:
decs.append(child.value)
return decs
def is_method(self, node):
"""Return whether the node occurs (directly) inside a class."""
node = node.parent
while node is not None:
if node.type == syms.classdef:
return True
if node.type == syms.funcdef:
return False
node = node.parent
return False
RETURN_EXPR = "return_stmt< 'return' any >"
return_expr = compile_pattern(RETURN_EXPR)
def has_return_exprs(self, node):
"""Traverse the tree below node looking for 'return expr'.
Return True if at least 'return expr' is found, False if not.
(If both 'return' and 'return expr' are found, return True.)
"""
results = {}
if self.return_expr.match(node, results):
return True
return any(
child.type not in (syms.funcdef, syms.classdef) and self.has_return_exprs(child)
for child in node.children
)