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..d1d7ecf
--- /dev/null
+++ b/examples/pure-eval/find_interesting_expressions/code.py
@@ -0,0 +1,69 @@
+# ---------------------------------------------------------------------
+# 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.
+# ---------------------------------------------------------------------
+import ast
+from pure_eval import Evaluator, group_expressions
+
+
+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 = ["
| Expression | Value | Occurrences |
|---|---|---|
{snippet} | "
+ f"{value_repr} | "
+ f"{count} |
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 = ["{snippet} = {value!r}{text}
"), append=True) + 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..5e16a54 --- /dev/null +++ b/examples/pure-eval/safe_evaluation/code.py @@ -0,0 +1,76 @@ +""" +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 +import ast +from pure_eval import Evaluator, CannotEval + + + +# 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..d0bd836
--- /dev/null
+++ b/examples/pure-eval/safe_evaluation/setup.py
@@ -0,0 +1,39 @@
+"""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.version_info = (9, 0, 2, '')
+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) +