Source code for macro_polo.macros.decorator

"""Decorator-style macros."""

from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
import tokenize

from .. import MacroError
from .._utils import SliceView
from ..match import MacroMatch
from ..parse import parse_macro_matcher, parse_macro_transcriber
from ..tokens import Token, stringify
from .types import ParameterizedMacro, PartialMatchMacro


def _stringify_invocation(name: str, parameters: Sequence[Token]) -> str:
    return f'@![{name}({stringify(parameters)})]'


[docs] class DecoratorMacroError(MacroError): """Error during invocation of decorator-style macros.""" def __init__(self, name: str, parameters: Sequence[Token], msg: str): """Create a new DecoratorStyleMacroError.""" super().__init__(f'invoking {_stringify_invocation(name, parameters)!r}: {msg}')
[docs] @dataclass(frozen=True, slots=True) class DecoratorMacroInvokerMacro(PartialMatchMacro): """A macro that processes decorator-style macro invocations. The syntax for invoking a decorator-style macro is :samp:`@![{name}({parameters})]` or :samp:`@![{name}]` (equivalent to :samp:`@![{name}()]`). Decorator-style macro invocations must immediately precede a "block", defined as either a single newline-terminated line, or a line followed by an indented block. When invoked, the registered macro is called with two arguments: 1. :samp:`{parameters}` (as a token sequence) 2. the block immediately following the invocation (as a token sequence). When multiple decorator-style macros are stacked, they are invoked from bottom to top. Macros are defined by :attr:`macros` (which can be updated after this class is instantiated). """ macros: Mapping[str, ParameterizedMacro] = field(default_factory=dict) """A mapping of names to decorator macros. When a decorator macro is invoked, its name is looked up here. This mapping may be shared with other macros, such as a :class:`~macro_polo.macros.importer.ImporterMacro`. """ _invocation_matcher = parse_macro_matcher( '@![$name:name $( ($($parameters:tt)*) )?] $^' ) _block_matcher = parse_macro_matcher( # token sequence terminated by newline, indented block, or EOF '$($[!$^] $[!$> $($_:tt)* $<] $line:tt)* $[($^)|($> $($_:tt)* $<)|($[!$_:tt])]' ) _parameters_transcriber = parse_macro_transcriber('$($($parameters)*)*') def __call__(self, tokens: Sequence[Token]) -> tuple[Sequence[Token], int]: """Transform the beginning of a token sequence.""" tokens = SliceView(tokens) invocations: list[tuple[str, Sequence[Token]]] = [] invocations_size = 0 while True: match self._invocation_matcher.match(tokens): case MacroMatch( size=match_size, captures={'name': Token(string=name)} as captures, ): parameters = tuple( self._parameters_transcriber.transcribe(captures) ) invocations.append((name, parameters)) invocations_size += match_size tokens = tokens[match_size:] case _: break if not invocations: return (), 0 block_match = self._block_matcher.match(tokens) if block_match is None: name, parameters = invocations[-1] raise DecoratorMacroError(name, parameters, 'expected block') initial_block_size = block_match.size tokens = list(tokens[:initial_block_size]) while invocations: name, parameters = invocations.pop() block_match = self._block_matcher.match(tokens) if block_match is None: raise DecoratorMacroError(name, parameters, 'expected block') block = tokens[: block_match.size] macro = self.macros.get(name) if macro is None: raise DecoratorMacroError( name, parameters, f'cannot find macro named {name!r}' ) result = macro(parameters, block) if result is None: raise DecoratorMacroError( name, parameters, "block didn't match expected pattern" ) if len(result) < 1 and invocations: next_name, next_parameters = invocations[-1] raise DecoratorMacroError( next_name, next_parameters, 'expected block, but previous macro ' f'{_stringify_invocation(name, parameters)!r} produced empty output.', ) if len(result) > 0 and result[-1].type not in ( tokenize.NEWLINE, tokenize.DEDENT, ): result = (*result, Token(tokenize.NEWLINE, '\n')) tokens[: block_match.size] = result return tokens, invocations_size + initial_block_size