KiBot/kibot/mcpyrate/markers.py

90 lines
3.3 KiB
Python

# -*- coding: utf-8; -*-
"""AST markers for internal communication.
*Internal* here means they are to be never passed to Python's `compile`;
macros may use them to work together.
"""
__all__ = ["ASTMarker", "get_markers", "delete_markers", "check_no_markers_remaining"]
import ast
from . import core, utils, walkers
class ASTMarker(ast.AST):
"""Base class for AST markers.
Markers are AST-node-like objects meant for communication between
co-operating, related macros. They are also used by the macro expander
to talk with itself during expansion.
We inherit from `ast.AST`, so that during macro expansion, a marker
behaves like a single AST node.
It is a postcondition of a completed macro expansion that no markers
remain in the AST.
To help fail-fast, if you define your own marker types, use `get_markers`
to check (at an appropriate point) that the expanded AST has no instances
of your own markers remaining. (You'll want a base class for your own markers.)
A typical usage example is in the quasiquote system, where the unquote
operators (some of which expand to markers) may only appear inside a quoted
section. So just before the quote operator exits, it checks that all
quasiquote markers within that section have been compiled away.
"""
# TODO: Silly default `None`, because `copy` and `deepcopy` call `__init__` without arguments,
# TODO: though the docs say they behave like `pickle` (and wouldn't thus need to call __init__ at all!).
def __init__(self, body=None):
"""body: the actual AST that is annotated by this marker"""
self.body = body
self._fields = ["body"] # support ast.iter_fields
def get_markers(tree, cls=ASTMarker):
"""Return a `list` of any `cls` instances found in `tree`. For output validation."""
class ASTMarkerCollector(walkers.ASTVisitor):
def examine(self, tree):
if isinstance(tree, cls):
self.collect(tree)
self.generic_visit(tree)
w = ASTMarkerCollector()
w.visit(tree)
return w.collected
def delete_markers(tree, cls=ASTMarker):
"""Delete any `cls` ASTMarker instances found in `tree`.
The deletion takes place by replacing each marker node with
the actual AST node stored in its `body` attribute.
"""
class ASTMarkerDeleter(walkers.ASTTransformer):
def transform(self, tree):
if isinstance(tree, cls):
return self.visit(tree.body)
return self.generic_visit(tree)
return ASTMarkerDeleter().visit(tree)
def check_no_markers_remaining(tree, *, filename, cls=None):
"""Check that `tree` has no AST markers remaining.
If a class `cls` is provided, only check for markers that `isinstance(cls)`.
If there are any, raise `MacroExpansionError`.
No return value.
`filename` is the full path to the `.py` file, for error reporting.
Convenience function.
"""
cls = cls or ASTMarker
remaining_markers = get_markers(tree, cls)
if remaining_markers:
codes = [utils.format_context(node, n=5) for node in remaining_markers]
locations = [utils.format_location(filename, node, code) for node, code in zip(remaining_markers, codes)]
report = "\n\n".join(locations)
raise core.MacroExpansionError(f"{filename}: AST markers remaining after expansion:\n{report}")