From ab7617c4d0dd29357e1e82bc6ffb2a9372fc0f1c Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:12 +0100 Subject: [PATCH 1/3] Add PyScript examples for pure-eval Generated by apply_llm_response.py from prompts/pure-eval/response.toml. Examples included: - safe_evaluation: Safe expression evaluation - find_interesting_expressions: Finding interesting expressions Generated-By: apply_llm_response.py --- examples/pure-eval/README.md | 18 +++++ .../find_interesting_expressions/code.py | 66 +++++++++++++++++ .../find_interesting_expressions/config.toml | 1 + .../find_interesting_expressions/setup.py | 23 ++++++ examples/pure-eval/order.json | 4 + examples/pure-eval/safe_evaluation/code.py | 73 +++++++++++++++++++ .../pure-eval/safe_evaluation/config.toml | 1 + examples/pure-eval/safe_evaluation/setup.py | 41 +++++++++++ 8 files changed, 227 insertions(+) create mode 100644 examples/pure-eval/README.md create mode 100644 examples/pure-eval/find_interesting_expressions/code.py create mode 100644 examples/pure-eval/find_interesting_expressions/config.toml create mode 100644 examples/pure-eval/find_interesting_expressions/setup.py create mode 100644 examples/pure-eval/order.json create mode 100644 examples/pure-eval/safe_evaluation/code.py create mode 100644 examples/pure-eval/safe_evaluation/config.toml create mode 100644 examples/pure-eval/safe_evaluation/setup.py diff --git a/examples/pure-eval/README.md b/examples/pure-eval/README.md new file mode 100644 index 0000000..a3b73d8 --- /dev/null +++ b/examples/pure-eval/README.md @@ -0,0 +1,18 @@ +# pure-eval 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/pure-eval/find_interesting_expressions/code.py b/examples/pure-eval/find_interesting_expressions/code.py new file mode 100644 index 0000000..aa2cf8e --- /dev/null +++ b/examples/pure-eval/find_interesting_expressions/code.py @@ -0,0 +1,66 @@ +# --------------------------------------------------------------------- +# Walk an entire AST and collect every sub-expression we can safely +# evaluate. This is the bread and butter of tools that annotate source +# code with live values, such as debuggers and tracebacks. +# --------------------------------------------------------------------- + +heading("Scanning a script for evaluatable expressions") +note( + "We exec a small script to populate a namespace, then ask " + "pure_eval to find every expression in the script " + "whose value it can determine without running side-effecting " + "code." +) + +source = """ +inventory = {"apples": 12, "pears": 7, "plums": 3} +total_fruit = sum(inventory.values()) +favourite = "pears" +favourite_count = inventory[favourite] +""" + +# Run the script so the names exist with real values. +namespace = {} +exec(source, namespace) + +tree = ast.parse(source) +evaluator = Evaluator(namespace) + +# find_expressions yields every (node, value) pair that pure_eval can +# safely resolve. The same expression may appear in several places, +# so we group equivalent nodes together for a tidier report. +rows = [] +for nodes, value in group_expressions(evaluator.find_expressions(tree)): + snippet = ast.unparse(nodes[0]) + rows.append((snippet, repr(value), len(nodes))) + +# Render as a small HTML table so it's easy to read. +table = [""] +table.append( + "" +) +for snippet, value_repr, count in rows: + table.append( + f"" + f"" + f"" + ) +table.append("
ExpressionValueOccurrences
{snippet}{value_repr}{count}
") +display(HTML("".join(table)), append=True) + +heading("Filtering down to the interesting ones") +note( + "interesting_expressions_grouped drops obvious " + "things like literals and bare references whose name matches " + "the value's __name__. What's left is the stuff a " + "human reader would actually want to see annotated." +) + +interesting = evaluator.interesting_expressions_grouped(tree) + +lines = ["") +display(HTML("".join(lines)), append=True) diff --git a/examples/pure-eval/find_interesting_expressions/config.toml b/examples/pure-eval/find_interesting_expressions/config.toml new file mode 100644 index 0000000..4b7f67a --- /dev/null +++ b/examples/pure-eval/find_interesting_expressions/config.toml @@ -0,0 +1 @@ +packages = ["pure-eval"] diff --git a/examples/pure-eval/find_interesting_expressions/setup.py b/examples/pure-eval/find_interesting_expressions/setup.py new file mode 100644 index 0000000..f6f06b1 --- /dev/null +++ b/examples/pure-eval/find_interesting_expressions/setup.py @@ -0,0 +1,23 @@ +"""Lighter setup: re-establish the names cell 1 introduced.""" +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 +from pure_eval import Evaluator, group_expressions diff --git a/examples/pure-eval/order.json b/examples/pure-eval/order.json new file mode 100644 index 0000000..4f261b6 --- /dev/null +++ b/examples/pure-eval/order.json @@ -0,0 +1,4 @@ +[ + "safe_evaluation", + "find_interesting_expressions" +] diff --git a/examples/pure-eval/safe_evaluation/code.py b/examples/pure-eval/safe_evaluation/code.py new file mode 100644 index 0000000..c0497bc --- /dev/null +++ b/examples/pure-eval/safe_evaluation/code.py @@ -0,0 +1,73 @@ +""" +A first look at pure_eval: evaluating AST nodes without side effects. + +Imagine you're writing a debugger or REPL helper that wants to peek +at the values of expressions on a line of source code. Calling eval() +is dangerous because it can run arbitrary code (network calls, +property side effects, mutations). pure_eval refuses to do anything +that might have a side effect. + +See: https://github.com/alexmojaki/pure_eval +""" +from IPython.core.display import display, HTML + + +# A class with a property that has an observable side effect: it +# prints whenever it's accessed. A naive eval() would trigger this. +class Rectangle: + def __init__(self, width, height): + self.width = width + self.height = height + + @property + def area(self): + # Pretend this is an expensive database call or HTTP request. + print("Calculating area... (side effect!)") + return self.width * self.height + + +rect = Rectangle(3, 5) + +heading("1. Parsing source into an AST") +note( + "We have a tuple expression referencing three attributes of " + "rect. Two are plain data; one is a property that " + "runs code when accessed." +) + +source = "(rect.width, rect.height, rect.area)" +tree = ast.parse(source, mode="eval") +the_tuple = tree.body + +display(HTML(f"
source = {source}
"), append=True) + +heading("2. Evaluating safe nodes") +note( + "We build an Evaluator from a dict of known names " + "and ask it for the value of each AST node. Plain attribute " + "lookups on the data succeed." +) + +evaluator = Evaluator({"rect": rect}) + +for node in the_tuple.elts[:2]: + label = ast.unparse(node) + value = evaluator[node] + note(f"{label}{value}") + +heading("3. Refusing to trigger side effects") +note( + "Asking for rect.area would invoke the property " + "and print a message. pure_eval raises " + "CannotEval instead." +) + +area_node = the_tuple.elts[2] +try: + evaluator[area_node] +except CannotEval: + note( + "Caught CannotEval for " + f"{ast.unparse(area_node)}. No side effect was " + "triggered." + ) diff --git a/examples/pure-eval/safe_evaluation/config.toml b/examples/pure-eval/safe_evaluation/config.toml new file mode 100644 index 0000000..4b7f67a --- /dev/null +++ b/examples/pure-eval/safe_evaluation/config.toml @@ -0,0 +1 @@ +packages = ["pure-eval"] diff --git a/examples/pure-eval/safe_evaluation/setup.py b/examples/pure-eval/safe_evaluation/setup.py new file mode 100644 index 0000000..62fdd98 --- /dev/null +++ b/examples/pure-eval/safe_evaluation/setup.py @@ -0,0 +1,41 @@ +"""Shim IPython's display API onto PyScript and import pure_eval.""" +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) + + +import ast +from pure_eval import Evaluator, CannotEval From 010062ef493eeda2f554d537e14fbdea900665b5 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 11 Jun 2026 12:48:49 +0100 Subject: [PATCH 2/3] Fix imports --- examples/pure-eval/find_interesting_expressions/code.py | 3 +++ examples/pure-eval/find_interesting_expressions/setup.py | 3 --- examples/pure-eval/safe_evaluation/code.py | 3 +++ examples/pure-eval/safe_evaluation/setup.py | 3 --- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/pure-eval/find_interesting_expressions/code.py b/examples/pure-eval/find_interesting_expressions/code.py index aa2cf8e..d1d7ecf 100644 --- a/examples/pure-eval/find_interesting_expressions/code.py +++ b/examples/pure-eval/find_interesting_expressions/code.py @@ -3,6 +3,9 @@ # evaluate. This is the bread and butter of tools that annotate source # code with live values, such as debuggers and tracebacks. # --------------------------------------------------------------------- +import ast +from pure_eval import Evaluator, group_expressions + heading("Scanning a script for evaluatable expressions") note( diff --git a/examples/pure-eval/find_interesting_expressions/setup.py b/examples/pure-eval/find_interesting_expressions/setup.py index f6f06b1..c6a7faf 100644 --- a/examples/pure-eval/find_interesting_expressions/setup.py +++ b/examples/pure-eval/find_interesting_expressions/setup.py @@ -18,6 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import ast -from pure_eval import Evaluator, group_expressions diff --git a/examples/pure-eval/safe_evaluation/code.py b/examples/pure-eval/safe_evaluation/code.py index c0497bc..5e16a54 100644 --- a/examples/pure-eval/safe_evaluation/code.py +++ b/examples/pure-eval/safe_evaluation/code.py @@ -10,6 +10,9 @@ See: https://github.com/alexmojaki/pure_eval """ from IPython.core.display import display, HTML +import ast +from pure_eval import Evaluator, CannotEval + # A class with a property that has an observable side effect: it diff --git a/examples/pure-eval/safe_evaluation/setup.py b/examples/pure-eval/safe_evaluation/setup.py index 62fdd98..270c80c 100644 --- a/examples/pure-eval/safe_evaluation/setup.py +++ b/examples/pure-eval/safe_evaluation/setup.py @@ -36,6 +36,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import ast -from pure_eval import Evaluator, CannotEval From 0359915e4fc4eb75fb53252295b5689c2f9c946e Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 26 Jun 2026 10:50:13 +0100 Subject: [PATCH 3/3] Add version_info to IPython shim for PyScript update. --- examples/pure-eval/safe_evaluation/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pure-eval/safe_evaluation/setup.py b/examples/pure-eval/safe_evaluation/setup.py index 270c80c..d0bd836 100644 --- a/examples/pure-eval/safe_evaluation/setup.py +++ b/examples/pure-eval/safe_evaluation/setup.py @@ -21,6 +21,7 @@ def display(*args, **kwargs): core_display.HTML = HTML ipython.core = core core.display = core_display +ipython.version_info = (9, 0, 2, '') ipython.get_ipython = lambda: None ipython.display = core_display sys.modules["IPython"] = ipython