Some strings contain delimiters that identify them with a specific syntax.
Node transformers do what . .... IPython accepts node transformers
import ast, abc, doctest, types, unittest
The NodeTransformer will takes two attributes:
This class is to be reused as a base class.
>>> class NewTransformer(StrTokenTransformerMeta): ...
class StrTokenTransformerMeta(ast.NodeTransformer, metaclass=abc.ABCMeta):
@abc.abstractstaticmethod
def condition(self, callable: str) -> bool:
"""A callable that tests a string condition."""
raise NotImplemented()
@abc.abstractproperty
def replacement(self, str) -> str:
"""A block string to replace a condition with."""
raise NotImplemented()
abstractproperty
is not required, but it doesn't hurt. If this is confusing, just know we can't create a newStrTokenTransformerMeta
class without thecondition
orreplacement
attributes existing and beingstaticmethod
andabstractproperty
, respectively.
StrTokenTransformer
defines import NodeTransformer
attributes.
class StrTokenTransformer(StrTokenTransformerMeta):
def generic_visit(self, node): return node
def visit_Expr(self, node):
if isinstance(node.value, ast.Str):
str = node.value.s
if self.condition(str):
return ast.parse(self.replace(str)).body[0]
return node
def visit_Module(self, node): return super().generic_visit(node)
def replace(self, str):
"""Validate the source, before continuing."""
self.validate()
quotes = '"""'
if quotes in str: quotes = "'''"
return self.replacement.format(
quotes + '{}' + quotes).format(str)
def validate(self):
"""Validate that the replacement string is Python."""
try: ast.parse(self.replacement)
except: raise StrTokenTransformerException(self.replacement)
It is a good practice to define our own exceptions.
class StrTokenTransformerException(BaseException): ...
class GraphViz(StrTokenTransformer):
replacement = """__import__('graphviz').Source({})"""
condition = staticmethod(lambda str: str.startswith('graph') or str.startswith('digraph'))
The graphviz syntax looks extra boss with Fira-Code.
class YamlDefinition(StrTokenTransformer):
replacement = """globals().update(
__import__('collections').ChainMap(*reversed(list(
__import__('yaml').safe_load_all(
__import__('io').StringIO({}))))))"""
condition = staticmethod(lambda str: str.startswith('---\n'))
class IframeDisplay(StrTokenTransformer):
replacement = """__import__('IPython').display.display(
__import__('IPython').display.IFrame(
{}, 600, 400))"""
condition = staticmethod(lambda str: str.startswith('http:') or str.startswith('https:'))
class DoctestString(StrTokenTransformer):
replacement = """__import__('doctest').testmod(
__import__('types').ModuleType('test', {}), globs=vars(__import__(__name__)))"""
condition = staticmethod(lambda str: any(line.lstrip().startswith('>>> ') for line in str.splitlines()))
def load_ipython_extension(ip=None):
ip = ip or __import__('IPython').get_ipython()
ip.ast_transformers = [GraphViz(), YamlDefinition(), IframeDisplay(), DoctestString()]
load = load_ipython_extension
>> __import__('__String_Node_Transformer').load()
Ø = __name__ == '__main__'
Ø and load_ipython_extension()
"""digraph {rankdir="LR" A->B->C->A}"""
"""---
foo: 42
---
foo: 100"""
if Ø: assert foo is 100, """The transformer is likely not loaded."""
""">>> 1
1"""
TestResults(failed=0, attempted=1)