KiBot/kibot/mcpy/core.py

167 lines
5.0 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2015 Salvador de la Puente González
# Copyright (c) 2020 Salvador E. Tropea
# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial
# License: MIT
# Project: MCPY https://github.com/delapuente/mcpy
""" Provide the functionality to find and expand macros. """
import sys
from ast import Name, copy_location, Import, alias, ImportFrom
from .visitors import BaseMacroExpander
class _MacroExpander(BaseMacroExpander):
def visit_With(self, withstmt):
"""
Check for a with macro as
with macroname:
# with's body is the argument
It replaces the with node with the result of the macro.
"""
with_item = withstmt.items[0]
candidate = with_item.context_expr
if isinstance(candidate, Name) and self._ismacro(candidate.id):
macro = candidate.id
tree = withstmt.body
kw = {'optional_vars': with_item.optional_vars}
new_tree = self._expand('block', withstmt, macro, tree, kw)
else:
new_tree = self.generic_visit(withstmt)
return new_tree
def visit_Subscript(self, subscript):
"""
Check for a expression macro as
macroname['index expression is the argument']
It replaces the expression node with the result of the macro.
"""
candidate = subscript.value
if isinstance(candidate, Name) and self._ismacro(candidate.id):
macro = candidate.id
tree = subscript.slice.value
new_tree = self._expand('expr', subscript, macro, tree)
else:
new_tree = self.generic_visit(subscript)
return new_tree
def visit_ClassDef(self, classdef):
return self._visit_Decorated(classdef)
def visit_FunctionDef(self, functiondef):
return self._visit_Decorated(functiondef)
def _visit_Decorated(self, decorated):
"""
Check for a expression macro as
@macroname
def f():
# The whole function is the target of the macro
Or
@macroname
class C():
# The whole class is the target of the macro
It replaces the whole decorated node with the result of the macro.
"""
macros, decorators = self._filter_out_macros(decorated.decorator_list)
decorated.decorator_list = decorators
if macros:
for macro in reversed(macros):
new_tree = self._expand('decorator', decorated, macro, decorated)
else:
new_tree = self.generic_visit(decorated)
return new_tree
def _filter_out_macros(self, decorators):
"""
Identify macro names inside a decorator list, and return a pair with
macro names and the decorators not identified as macros.
"""
macros, remaining = [], []
for d in decorators:
if isinstance(d, Name) and self._ismacro(d.id):
macros.append(d.id)
else:
remaining.append(d)
return macros, remaining
def expand_macros(tree, bindings):
"""
Return an expanded version of tree with macros applied.
"""
expansion = _MacroExpander(bindings).visit(tree)
return expansion
def find_macros(tree, name):
"""
Looks for `from ... import macros, ...` statements in the module body and
returns a dict with names and implementations for found macros or an empty
dict if no macros are used.
"""
bindings = {}
for index, statement in enumerate(tree.body):
if _is_macro_import(statement):
bindings.update(_get_macros(statement, name))
# Remove all names to prevent macro names to be used
module = statement.module
tree.body[index] = copy_location(
Import(names=[alias(name=module, asname=None)]),
statement
)
return bindings
def _is_macro_import(statement):
"""
A 'macro import' is a statement with the form of
from ... import macros, ...
"""
is_macro_import = False
if isinstance(statement, ImportFrom):
firstimport = statement.names[0]
if firstimport.name == 'macros' and firstimport.asname is None:
is_macro_import = True
return is_macro_import
def _fix_level(modulename, name, level):
""" Translates the relative import to an absolute one """
path = name.split('.')
index = -2
while level:
modulename = path[index]+'.'+modulename
level -= 1
index -= 1
return modulename
def _get_macros(macroimport, name):
"""
Returns a map with names and macros from the macro import statement.
"""
if macroimport.level:
macroimport.module = _fix_level(macroimport.module, name, macroimport.level)
macroimport.level = 0
modulename = macroimport.module
__import__(modulename)
module = sys.modules[modulename]
return {name.asname or name.name: getattr(module, name.name) for name in macroimport.names[1:]}