|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
Inject Cython type declarations into a .py file using the Jedi static analysis tool.
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
from __future__ import absolute_import
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
from io import open
|
|
Packit |
562c7a |
from collections import defaultdict
|
|
Packit |
562c7a |
from itertools import chain
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
import jedi
|
|
Packit |
562c7a |
from jedi.parser.tree import Module, ImportName
|
|
Packit |
562c7a |
from jedi.evaluate.representation import Function, Instance, Class
|
|
Packit |
562c7a |
from jedi.evaluate.iterable import ArrayMixin, GeneratorComprehension
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
from Cython.Utils import open_source_file
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
default_type_map = {
|
|
Packit |
562c7a |
'float': 'double',
|
|
Packit |
562c7a |
'int': 'long',
|
|
Packit |
562c7a |
}
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
def analyse(source_path=None, code=None):
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
Analyse a Python source code file with Jedi.
|
|
Packit |
562c7a |
Returns a mapping from (scope-name, (line, column)) pairs to a name-types mapping.
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
if not source_path and code is None:
|
|
Packit |
562c7a |
raise ValueError("Either 'source_path' or 'code' is required.")
|
|
Packit |
562c7a |
scoped_names = {}
|
|
Packit |
562c7a |
statement_iter = jedi.names(source=code, path=source_path, all_scopes=True)
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
for statement in statement_iter:
|
|
Packit |
562c7a |
parent = statement.parent()
|
|
Packit |
562c7a |
scope = parent._definition
|
|
Packit |
562c7a |
evaluator = statement._evaluator
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
# skip function/generator definitions, class definitions, and module imports
|
|
Packit |
562c7a |
if any(isinstance(statement._definition, t) for t in [Function, Class, ImportName]):
|
|
Packit |
562c7a |
continue
|
|
Packit |
562c7a |
key = (None if isinstance(scope, Module) else str(parent.name), scope.start_pos)
|
|
Packit |
562c7a |
try:
|
|
Packit |
562c7a |
names = scoped_names[key]
|
|
Packit |
562c7a |
except KeyError:
|
|
Packit |
562c7a |
names = scoped_names[key] = defaultdict(set)
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
position = statement.start_pos if statement.name in names else None
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
for name_type in evaluator.find_types(scope, statement.name, position=position ,search_global=True):
|
|
Packit |
562c7a |
if isinstance(name_type, Instance):
|
|
Packit |
562c7a |
if isinstance(name_type.base, Class):
|
|
Packit |
562c7a |
type_name = 'object'
|
|
Packit |
562c7a |
else:
|
|
Packit |
562c7a |
type_name = name_type.base.obj.__name__
|
|
Packit |
562c7a |
elif isinstance(name_type, ArrayMixin):
|
|
Packit |
562c7a |
type_name = name_type.type
|
|
Packit |
562c7a |
elif isinstance(name_type, GeneratorComprehension):
|
|
Packit |
562c7a |
type_name = None
|
|
Packit |
562c7a |
else:
|
|
Packit |
562c7a |
try:
|
|
Packit |
562c7a |
type_name = type(name_type.obj).__name__
|
|
Packit |
562c7a |
except AttributeError as error:
|
|
Packit |
562c7a |
type_name = None
|
|
Packit |
562c7a |
if type_name is not None:
|
|
Packit |
562c7a |
names[str(statement.name)].add(type_name)
|
|
Packit |
562c7a |
return scoped_names
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
def inject_types(source_path, types, type_map=default_type_map, mode='python'):
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
Hack type declarations into source code file.
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
@param mode is currently 'python', which means that the generated type declarations use pure Python syntax.
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
col_and_types_by_line = dict(
|
|
Packit |
562c7a |
# {line: (column, scope_name or None, [(name, type)])}
|
|
Packit |
562c7a |
(k[-1][0], (k[-1][1], k[0], [(n, next(iter(t))) for (n, t) in v.items() if len(t) == 1]))
|
|
Packit |
562c7a |
for (k, v) in types.items())
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
lines = [u'import cython\n']
|
|
Packit |
562c7a |
with open_source_file(source_path) as f:
|
|
Packit |
562c7a |
for line_no, line in enumerate(f, 1):
|
|
Packit |
562c7a |
if line_no in col_and_types_by_line:
|
|
Packit |
562c7a |
col, scope, types = col_and_types_by_line[line_no]
|
|
Packit |
562c7a |
if types:
|
|
Packit |
562c7a |
types = ', '.join("%s='%s'" % (name, type_map.get(type_name, type_name))
|
|
Packit |
562c7a |
for name, type_name in types)
|
|
Packit |
562c7a |
if scope is None:
|
|
Packit |
562c7a |
type_decl = u'{indent}cython.declare({types})\n'
|
|
Packit |
562c7a |
else:
|
|
Packit |
562c7a |
type_decl = u'{indent}@cython.locals({types})\n'
|
|
Packit |
562c7a |
lines.append(type_decl.format(indent=' '*col, types=types))
|
|
Packit |
562c7a |
lines.append(line)
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
return lines
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
def main(file_paths=None, overwrite=False):
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
Main entry point to process a list of .py files and inject type inferred declarations.
|
|
Packit |
562c7a |
"""
|
|
Packit |
562c7a |
if file_paths is None:
|
|
Packit |
562c7a |
import sys
|
|
Packit |
562c7a |
file_paths = sys.argv[1:]
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
for source_path in file_paths:
|
|
Packit |
562c7a |
types = analyse(source_path)
|
|
Packit |
562c7a |
lines = inject_types(source_path, types)
|
|
Packit |
562c7a |
target_path = source_path + ('' if overwrite else '_typed.py')
|
|
Packit |
562c7a |
with open(target_path, 'w', encoding='utf8') as f:
|
|
Packit |
562c7a |
for line in lines:
|
|
Packit |
562c7a |
f.write(line)
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
|
|
Packit |
562c7a |
if __name__ == '__main__':
|
|
Packit |
562c7a |
main()
|