Blame Tools/jedityper.py

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()