{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Reasonable way to place macros in text.\n", "\n", "Some strings contain delimiters that identify them with a specific syntax.\n", "\n", "* Markdown has ticks\n", "* Yaml has dashes\n", "* Graphviz has graph/digraph\n", "* Latex has Dollar signs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# The Node Transformer\n", "\n", "Node transformers do what . ....\n", "IPython accepts node transformers\n", "\n", "https://greentreesnakes.readthedocs.io/" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ " import ast, abc, doctest, types, unittest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The NodeTransformer will takes two attributes:\n", "\n", "* A condition that must be satisfied to trigger the replacement.\n", "* A formatable string that can be replaced with the desired source.\n", "\n", "This class is to be reused as a base class.\n", "\n", " >>> class NewTransformer(StrTokenTransformerMeta): ..." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ " class StrTokenTransformerMeta(ast.NodeTransformer, metaclass=abc.ABCMeta):\n", " @abc.abstractstaticmethod\n", " def condition(self, callable: str) -> bool:\n", " \"\"\"A callable that tests a string condition.\"\"\"\n", " raise NotImplemented()\n", " \n", " @abc.abstractproperty\n", " def replacement(self, str) -> str:\n", " \"\"\"A block string to replace a condition with.\"\"\"\n", " raise NotImplemented()\n", " \n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> `abstractproperty` is not required, but it doesn't hurt. If this is confusing, just know we can't create a new `StrTokenTransformerMeta` class without the `condition` or `replacement` attributes existing and being `staticmethod` and `abstractproperty`, respectively." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`StrTokenTransformer` defines import `NodeTransformer` attributes." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ " class StrTokenTransformer(StrTokenTransformerMeta):\n", " def generic_visit(self, node): return node\n", " \n", " def visit_Expr(self, node):\n", " if isinstance(node.value, ast.Str):\n", " str = node.value.s\n", " if self.condition(str):\n", " return ast.parse(self.replace(str)).body[0]\n", " return node\n", " \n", " def visit_Module(self, node): return super().generic_visit(node)\n", " \n", " def replace(self, str):\n", " \"\"\"Validate the source, before continuing.\"\"\"\n", " self.validate()\n", " quotes = '\"\"\"'\n", " if quotes in str: quotes = \"'''\"\n", " return self.replacement.format(\n", " quotes + '{}' + quotes).format(str)\n", " \n", " def validate(self):\n", " \"\"\"Validate that the replacement string is Python.\"\"\"\n", " try: ast.parse(self.replacement)\n", " except: raise StrTokenTransformerException(self.replacement)\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is a good practice to define our own exceptions." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ " class StrTokenTransformerException(BaseException): ..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A graphviz replacement\n", "\n", "When a string starts with __graph__ or __digraph__ show a [__graphviz__]() repr." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ " class GraphViz(StrTokenTransformer):\n", " replacement = \"\"\"__import__('graphviz').Source({})\"\"\"\n", " condition = staticmethod(lambda str: str.startswith('graph') or str.startswith('digraph'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> The graphviz syntax looks extra boss with [Fira-Code]()." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A yaml replacement" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ " class YamlDefinition(StrTokenTransformer):\n", " replacement = \"\"\"globals().update(\n", " __import__('collections').ChainMap(*reversed(list(\n", " __import__('yaml').safe_load_all(\n", " __import__('io').StringIO({}))))))\"\"\"\n", " condition = staticmethod(lambda str: str.startswith('---\\n'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## An Iframe replacement." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ " class IframeDisplay(StrTokenTransformer):\n", " replacement = \"\"\"__import__('IPython').display.display(\n", " __import__('IPython').display.IFrame(\n", " {}, 600, 400))\"\"\"\n", " condition = staticmethod(lambda str: str.startswith('http:') or str.startswith('https:'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A tester" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [], "source": [ " class DoctestString(StrTokenTransformer):\n", " replacement = \"\"\"__import__('doctest').testmod(\n", " __import__('types').ModuleType('test', {}), globs=vars(__import__(__name__)))\"\"\"\n", " condition = staticmethod(lambda str: any(line.lstrip().startswith('>>> ') for line in str.splitlines()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## An IPython extension" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ " def load_ipython_extension(ip=None):\n", " ip = ip or __import__('IPython').get_ipython()\n", " ip.ast_transformers = [GraphViz(), YamlDefinition(), IframeDisplay(), DoctestString()]\n", " \n", " load = load_ipython_extension" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Usage\n", "\n", " >> __import__('__String_Node_Transformer').load()" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [], "source": [ " Ø = __name__ == '__main__'\n", " Ø and load_ipython_extension()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### GraphViz Example" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "C->A\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 70, "metadata": {}, "output_type": "execute_result" } ], "source": [ " \"\"\"digraph {rankdir=\"LR\" A->B->C->A}\"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Yaml Example" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [], "source": [ " \"\"\"---\n", " foo: 42\n", " ---\n", " foo: 100\"\"\"\n", " if Ø: assert foo is 100, \"\"\"The transformer is likely not loaded.\"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Doctesting" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "TestResults(failed=0, attempted=1)" ] }, "execution_count": 72, "metadata": {}, "output_type": "execute_result" } ], "source": [ " \"\"\">>> 1\n", " 1\"\"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# What other replacements could you imagine?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "p6", "language": "python", "name": "other-env" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.3" } }, "nbformat": 4, "nbformat_minor": 2 }