diff --git a/examples/executing/README.md b/examples/executing/README.md new file mode 100644 index 0000000..faf6016 --- /dev/null +++ b/examples/executing/README.md @@ -0,0 +1,18 @@ +# executing Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/executing/find_calling_node/code.py b/examples/executing/find_calling_node/code.py new file mode 100644 index 0000000..3974fc4 --- /dev/null +++ b/examples/executing/find_calling_node/code.py @@ -0,0 +1,52 @@ +""" +A first taste of `executing`: discover, at runtime, exactly which +piece of source code triggered the current function call. + +`executing` looks at the calling frame's bytecode and matches it back +to a node in the parsed AST of the source file. This is the magic +behind libraries like `icecream`, `snoop`, and `stack_data`. + +See: https://github.com/alexmojaki/executing +""" +from IPython.core.display import display, HTML +import ast +import inspect +import executing + + + +def whoami(): + """Report the AST node and source text of our caller.""" + # The caller's frame is one level up the stack. + caller_frame = inspect.currentframe().f_back + node = executing.Source.executing(caller_frame).node + if node is None: + return "(could not identify the calling node)" + # ast.dump shows the structural shape; ast.unparse recovers source. + return f"{type(node).__name__}: {ast.unparse(node)!r}" + + +heading("What called me?") +note( + "Each line below calls whoami() in a different " + "syntactic context. executing tells us which AST " + "node corresponds to each call." +) + +# A bare call expression. +result_simple = whoami() + +# A call inside a binary operation. +result_in_binop = "prefix: " + whoami() + +# A call used as a subscript index. +labels = {"a": "alpha", "b": "beta"} +result_as_key = labels["a"], whoami() + +display(HTML( + "" +), append=True) diff --git a/examples/executing/find_calling_node/config.toml b/examples/executing/find_calling_node/config.toml new file mode 100644 index 0000000..5e50b6e --- /dev/null +++ b/examples/executing/find_calling_node/config.toml @@ -0,0 +1 @@ +packages = ["executing"] diff --git a/examples/executing/find_calling_node/setup.py b/examples/executing/find_calling_node/setup.py new file mode 100644 index 0000000..07879f9 --- /dev/null +++ b/examples/executing/find_calling_node/setup.py @@ -0,0 +1,42 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/executing/order.json b/examples/executing/order.json new file mode 100644 index 0000000..b44c1bf --- /dev/null +++ b/examples/executing/order.json @@ -0,0 +1,5 @@ +[ + "find_calling_node", + "show_argument_source", + "qualname_and_source" +] diff --git a/examples/executing/qualname_and_source/code.py b/examples/executing/qualname_and_source/code.py new file mode 100644 index 0000000..a31d441 --- /dev/null +++ b/examples/executing/qualname_and_source/code.py @@ -0,0 +1,74 @@ +# --------------------------------------------------------------------- +# Beyond the AST node: ask `executing` for the qualified name of the +# currently executing function, and for the line/column range of the +# specific node within the source. +# --------------------------------------------------------------------- + +heading("Qualified names from any frame") +note( + "Source.for_frame(frame).code_qualname(frame.f_code) " + "returns the dotted __qualname__ of the function " + "currently running in that frame — including nested " + "functions and methods." +) + + +class Telescope: + def observe(self, target): + return self._record(target) + + def _record(self, target): + frame = inspect.currentframe() + source = executing.Source.for_frame(frame) + return source.code_qualname(frame.f_code) + + +def outer(): + def inner(): + frame = inspect.currentframe() + source = executing.Source.for_frame(frame) + return source.code_qualname(frame.f_code) + return inner() + + +display(HTML( + "" +), append=True) + + +# --------------------------------------------------------------------- +# Locate the calling expression's exact position in the source. +# --------------------------------------------------------------------- + +heading("Where in the source did this call happen?") +note( + "AST nodes carry line and column information. We can combine " + "executing's node identification with those " + "attributes to point at the precise span of code." +) + + +def locate(): + """Return a description of where the caller invoked us.""" + caller_frame = inspect.currentframe().f_back + executing_info = executing.Source.executing(caller_frame) + node = executing_info.node + if node is None: + return "unknown location" + filename = caller_frame.f_code.co_filename + return ( + f"{type(node).__name__} in {filename} " + f"at line {node.lineno}, cols {node.col_offset}" + f"–{node.end_col_offset}: {ast.unparse(node)}" + ) + + +# Two distinct call sites; each gets its own location report. +report_a = locate() +report_b = locate() + +display(HTML(f"

First call: {report_a}

"), append=True) +display(HTML(f"

Second call: {report_b}

"), append=True) diff --git a/examples/executing/qualname_and_source/config.toml b/examples/executing/qualname_and_source/config.toml new file mode 100644 index 0000000..5e50b6e --- /dev/null +++ b/examples/executing/qualname_and_source/config.toml @@ -0,0 +1 @@ +packages = ["executing"] diff --git a/examples/executing/qualname_and_source/setup.py b/examples/executing/qualname_and_source/setup.py new file mode 100644 index 0000000..8d68d3c --- /dev/null +++ b/examples/executing/qualname_and_source/setup.py @@ -0,0 +1,22 @@ +"""Same lightweight setup as the previous cell.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import ast +import inspect +import executing diff --git a/examples/executing/show_argument_source/code.py b/examples/executing/show_argument_source/code.py new file mode 100644 index 0000000..21376dc --- /dev/null +++ b/examples/executing/show_argument_source/code.py @@ -0,0 +1,50 @@ +# --------------------------------------------------------------------- +# Build a tiny `icecream`-style debug helper. +# +# When you call `show(some_expr, other_expr)`, it prints both the +# *source text* of each argument and its runtime value. This is the +# core trick behind libraries like `icecream` and `snoop`. +# --------------------------------------------------------------------- + +heading("A mini print-debugger powered by executing") +note( + "show(...) identifies its own Call " + "node in the caller's source, then walks the argument AST nodes " + "to recover the original text of each expression." +) + + +def show(*values): + """Display each argument as `source = value`.""" + caller_frame = inspect.currentframe().f_back + call_node = executing.Source.executing(caller_frame).node + + rows = [] + if isinstance(call_node, ast.Call): + # Pair each AST argument node with its evaluated value. + for arg_node, value in zip(call_node.args, values): + source_text = ast.unparse(arg_node) + rows.append((source_text, repr(value))) + else: + # Fallback if we couldn't identify the call (e.g. inside an + # expression the parser handles unusually). + for value in values: + rows.append(("?", repr(value))) + + body = "".join( + f"{src}" + f"{val}" + for src, val in rows + ) + display(HTML( + "" + f"{body}
expressionvalue
" + ), append=True) + + +# A small story: a basket of fruit and a few derived quantities. +basket = {"apples": 4, "pears": 2, "plums": 7} +total = sum(basket.values()) +heaviest = max(basket, key=basket.get) + +show(basket, total, heaviest, total * 1.5, basket["apples"] + basket["pears"]) diff --git a/examples/executing/show_argument_source/config.toml b/examples/executing/show_argument_source/config.toml new file mode 100644 index 0000000..5e50b6e --- /dev/null +++ b/examples/executing/show_argument_source/config.toml @@ -0,0 +1 @@ +packages = ["executing"] diff --git a/examples/executing/show_argument_source/setup.py b/examples/executing/show_argument_source/setup.py new file mode 100644 index 0000000..c959c37 --- /dev/null +++ b/examples/executing/show_argument_source/setup.py @@ -0,0 +1,25 @@ +""" +Lighter setup: re-establish the names from the first cell without +re-running the IPython shim. +""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import ast +import inspect +import executing