Skip to content

[WIP] Add a fixer to get signature data from a command #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
43 changes: 26 additions & 17 deletions pyannotate_tools/annotations/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@

from pyannotate_tools.annotations.main import generate_annotations_json_string, unify_type_comments
from pyannotate_tools.fixes.fix_annotate_json import FixAnnotateJson
from pyannotate_tools.fixes.fix_annotate_command import FixAnnotateCommand

parser = argparse.ArgumentParser()
parser.add_argument('--type-info', default='type_info.json', metavar="FILE",
help="JSON input file (default type_info.json)")
parser.add_argument('--command', '-c', metavar="COMMAND",
help="Command to call to generate JSON info for a call site")
parser.add_argument('--uses-signature', action='store_true',
help="JSON input uses a signature format")
parser.add_argument('-p', '--print-function', action='store_true',
Expand Down Expand Up @@ -111,24 +114,30 @@ def main(args_override=None):
if args.auto_any:
fixers = ['pyannotate_tools.fixes.fix_annotate']
else:
# Produce nice error message if type_info.json not found.
try:
with open(args.type_info) as f:
contents = f.read()
except IOError as err:
sys.exit("Can't open type info file: %s" % err)

# Run pass 2 with output into a variable.
if args.uses_signature:
data = json.loads(contents) # type: List[Any]
else:
data = generate_annotations_json_string(
args.type_info,
only_simple=args.only_simple)
fixers = []
if args.type_info:
# Produce nice error message if type_info.json not found.
try:
with open(args.type_info) as f:
contents = f.read()
except IOError as err:
sys.exit("Can't open type info file: %s" % err)

# Run pass 2 with output into a variable.
if args.uses_signature:
data = json.loads(contents) # type: List[Any]
else:
data = generate_annotations_json_string(
args.type_info,
only_simple=args.only_simple)

# Run pass 3 with input from that variable.
FixAnnotateJson.init_stub_json_from_data(data, args.files[0])
fixers.append('pyannotate_tools.fixes.fix_annotate_json')

# Run pass 3 with input from that variable.
FixAnnotateJson.init_stub_json_from_data(data, args.files[0])
fixers = ['pyannotate_tools.fixes.fix_annotate_json']
if args.command:
fixers.append('pyannotate_tools.fixes.fix_annotate_command')
FixAnnotateCommand.set_command(args.command)

flags = {'print_function': args.print_function,
'annotation_style': annotation_style}
Expand Down
138 changes: 72 additions & 66 deletions pyannotate_tools/fixes/fix_annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def foo(self, bar : Any, baz : int = 12) -> Any:
from lib2to3.pytree import Leaf, Node


class FixAnnotate(BaseFix):
class BaseFixAnnotate(BaseFix):

# This fixer is compatible with the bottom matcher.
BM_compatible = True
Expand All @@ -59,8 +59,8 @@ class FixAnnotate(BaseFix):
counter = None if not _maxfixes else int(_maxfixes)

def transform(self, node, results):
if FixAnnotate.counter is not None:
if FixAnnotate.counter <= 0:
if BaseFixAnnotate.counter is not None:
if BaseFixAnnotate.counter <= 0:
return

# Check if there's already a long-form annotation for some argument.
Expand Down Expand Up @@ -181,8 +181,8 @@ def transform(self, node, results):
self.add_py2_annot(argtypes, restype, node, results)

# Common to py2 and py3 style annotations:
if FixAnnotate.counter is not None:
FixAnnotate.counter -= 1
if BaseFixAnnotate.counter is not None:
BaseFixAnnotate.counter -= 1

# Also add 'from typing import Any' at the top if needed.
self.patch_imports(argtypes + [restype], node)
Expand Down Expand Up @@ -348,67 +348,7 @@ def patch_imports(self, types, node):
break

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 argtypes, restype
raise NotImplementedError

# The parse tree has a different shape when there is a single
# decorator vs. when there are multiple decorators.
Expand Down Expand Up @@ -479,3 +419,69 @@ def is_generator(self, node):
if self.is_generator(child):
return True
return False


class FixAnnotate(BaseFixAnnotate):

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 argtypes, restype
34 changes: 34 additions & 0 deletions pyannotate_tools/fixes/fix_annotate_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import absolute_import, print_function

import json
import shlex
import subprocess

from .fix_annotate_json import BaseFixAnnotateFromSignature, FixAnnotateJson as _FixAnnotateJson

class FixAnnotateCommand(BaseFixAnnotateFromSignature):
# run after FixAnnotateJson
run_order = _FixAnnotateJson.run_order + 1

command = None

@classmethod
def set_command(cls, command):
cls.command = command

def get_command(self, filename, lineno):
return shlex.split(self.command.format(filename=filename, lineno=lineno))

def get_types(self, node, results, funcname):
cmd = self.get_command(self.filename, node.get_lineno())
try:
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
self.log_message("Line %d: Failed calling `%s`: %s" %
(node.get_lineno(), self.command,
err.output.rstrip()))
return None

data = json.loads(out)
signature = data[0]['signature']
return signature['arg_types'], signature['return_type']
Loading