Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/pure-eval/README.md
Original file line number Diff line number Diff line change
@@ -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.
69 changes: 69 additions & 0 deletions examples/pure-eval/find_interesting_expressions/code.py
Original file line number Diff line number Diff line change
@@ -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 "
"<code>pure_eval</code> 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 border='1' cellpadding='4' style='border-collapse:collapse'>"]
table.append(
"<tr><th>Expression</th><th>Value</th><th>Occurrences</th></tr>"
)
for snippet, value_repr, count in rows:
table.append(
f"<tr><td><code>{snippet}</code></td>"
f"<td><code>{value_repr}</code></td>"
f"<td>{count}</td></tr>"
)
table.append("</table>")
display(HTML("".join(table)), append=True)

heading("Filtering down to the interesting ones")
note(
"<code>interesting_expressions_grouped</code> drops obvious "
"things like literals and bare references whose name matches "
"the value's <code>__name__</code>. What's left is the stuff a "
"human reader would actually want to see annotated."
)

interesting = evaluator.interesting_expressions_grouped(tree)

lines = ["<ul>"]
for nodes, value in interesting:
snippet = ast.unparse(nodes[0])
lines.append(f"<li><code>{snippet}</code> = <code>{value!r}</code></li>")
lines.append("</ul>")
display(HTML("".join(lines)), append=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pure-eval"]
20 changes: 20 additions & 0 deletions examples/pure-eval/find_interesting_expressions/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

4 changes: 4 additions & 0 deletions examples/pure-eval/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"safe_evaluation",
"find_interesting_expressions"
]
76 changes: 76 additions & 0 deletions examples/pure-eval/safe_evaluation/code.py
Original file line number Diff line number Diff line change
@@ -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 "
"<code>rect</code>. 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"<pre>source = {source}</pre>"), append=True)

heading("2. Evaluating safe nodes")
note(
"We build an <code>Evaluator</code> 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"<code>{label}</code> &rarr; <strong>{value}</strong>")

heading("3. Refusing to trigger side effects")
note(
"Asking for <code>rect.area</code> would invoke the property "
"and print a message. <code>pure_eval</code> raises "
"<code>CannotEval</code> instead."
)

area_node = the_tuple.elts[2]
try:
evaluator[area_node]
except CannotEval:
note(
"Caught <code>CannotEval</code> for "
f"<code>{ast.unparse(area_node)}</code>. No side effect was "
"triggered."
)
1 change: 1 addition & 0 deletions examples/pure-eval/safe_evaluation/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pure-eval"]
39 changes: 39 additions & 0 deletions examples/pure-eval/safe_evaluation/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)