167 lines
5.0 KiB
Python
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:]}
|