From 4e4695aca6a5e3220c47b998716489967d40a748 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:11 +0100 Subject: [PATCH 1/2] Add PyScript examples for parso Generated by apply_llm_response.py from prompts/parso/response.toml. Examples included: - parse_and_walk: Parse and walk a Python module - syntax_errors: Find every syntax error at once - rename_identifier: Refactor by editing leaves in place Generated-By: apply_llm_response.py --- examples/parso/README.md | 18 ++++++ examples/parso/order.json | 5 ++ examples/parso/parse_and_walk/code.py | 60 +++++++++++++++++++ examples/parso/parse_and_walk/config.toml | 1 + examples/parso/parse_and_walk/setup.py | 43 ++++++++++++++ examples/parso/rename_identifier/code.py | 61 ++++++++++++++++++++ examples/parso/rename_identifier/config.toml | 1 + examples/parso/rename_identifier/setup.py | 22 +++++++ examples/parso/syntax_errors/code.py | 58 +++++++++++++++++++ examples/parso/syntax_errors/config.toml | 1 + examples/parso/syntax_errors/setup.py | 22 +++++++ 11 files changed, 292 insertions(+) create mode 100644 examples/parso/README.md create mode 100644 examples/parso/order.json create mode 100644 examples/parso/parse_and_walk/code.py create mode 100644 examples/parso/parse_and_walk/config.toml create mode 100644 examples/parso/parse_and_walk/setup.py create mode 100644 examples/parso/rename_identifier/code.py create mode 100644 examples/parso/rename_identifier/config.toml create mode 100644 examples/parso/rename_identifier/setup.py create mode 100644 examples/parso/syntax_errors/code.py create mode 100644 examples/parso/syntax_errors/config.toml create mode 100644 examples/parso/syntax_errors/setup.py diff --git a/examples/parso/README.md b/examples/parso/README.md new file mode 100644 index 0000000..abd09ad --- /dev/null +++ b/examples/parso/README.md @@ -0,0 +1,18 @@ +# parso 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/parso/order.json b/examples/parso/order.json new file mode 100644 index 0000000..9686437 --- /dev/null +++ b/examples/parso/order.json @@ -0,0 +1,5 @@ +[ + "parse_and_walk", + "syntax_errors", + "rename_identifier" +] diff --git a/examples/parso/parse_and_walk/code.py b/examples/parso/parse_and_walk/code.py new file mode 100644 index 0000000..017c3f1 --- /dev/null +++ b/examples/parso/parse_and_walk/code.py @@ -0,0 +1,60 @@ +""" +A first look at parso: parse Python source into a syntax tree, then +walk the tree to discover what's inside. + +Parso is the parser that powers Jedi. It produces a concrete syntax +tree that preserves every byte of the original source (whitespace, +comments, the lot), which makes it a great tool for refactoring, +linting, and code analysis. Docs: https://parso.readthedocs.io +""" +from IPython.core.display import display, HTML + +# A small but realistic snippet of Python to chew on. +source = """\ +import math + +def area_of_circle(radius): + \\"\\"\\"Return the area of a circle with the given radius.\\"\\"\\" + return math.pi * radius ** 2 + +def area_of_square(side): + return side * side +""" + +heading("Parsing source into a module") +note( + "parso.parse returns the root node of the syntax tree -- a " + "Module. We can ask the tree to round-trip back to the exact " + "source it was parsed from." +) + +module = parso.parse(source) +note(f"Top-level node type: {module.type}") +note(f"Round-trip matches original: {module.get_code() == source}") + +heading("Listing top-level function definitions") +note( + "iter_funcdefs walks the module's direct children and yields " + "each function definition node. Each node knows its name and " + "where it lives in the source." +) + +rows = [] +for funcdef in module.iter_funcdefs(): + name = funcdef.name.value + start_line, start_col = funcdef.start_pos + end_line, end_col = funcdef.end_pos + params = [p.name.value for p in funcdef.get_params()] + rows.append( + f"{name}" + f"{', '.join(params) or '—'}" + f"lines {start_line}–{end_line}" + ) + +table = ( + "" + "" + + "".join(rows) + + "
FunctionParametersLocation
" +) +display(HTML(table), append=True) diff --git a/examples/parso/parse_and_walk/config.toml b/examples/parso/parse_and_walk/config.toml new file mode 100644 index 0000000..71aa241 --- /dev/null +++ b/examples/parso/parse_and_walk/config.toml @@ -0,0 +1 @@ +packages = ["parso"] diff --git a/examples/parso/parse_and_walk/setup.py b/examples/parso/parse_and_walk/setup.py new file mode 100644 index 0000000..de8da24 --- /dev/null +++ b/examples/parso/parse_and_walk/setup.py @@ -0,0 +1,43 @@ +""" +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): + 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 parso diff --git a/examples/parso/rename_identifier/code.py b/examples/parso/rename_identifier/code.py new file mode 100644 index 0000000..b6ed4c4 --- /dev/null +++ b/examples/parso/rename_identifier/code.py @@ -0,0 +1,61 @@ +# --------------------------------------------------------------------- +# Round-trip parsing means we can change a single leaf in the tree and +# get back the original source with just that change applied -- all +# whitespace, comments and formatting are preserved exactly. +# --------------------------------------------------------------------- + +heading("Renaming a variable while keeping formatting intact") +note( + "We'll rename every occurrence of tmp to " + "buffer in this snippet by walking the tree's " + "leaves and rewriting matching Name tokens." +) + +source = """\ +def normalize(values): + # Scale values into the unit interval. + tmp = max(values) # peak value + if tmp == 0: + return values + return [v / tmp for v in values] # divide each by the peak +""" + +display(HTML(f"
{source}
"), append=True) + +module = parso.parse(source) + + +def walk_leaves(node): + """Yield every leaf (token) in the tree, in source order.""" + if hasattr(node, "children"): + for child in node.children: + yield from walk_leaves(child) + else: + yield node + + +# Mutate matching Name leaves directly. The tree remembers the +# surrounding whitespace via each leaf's `prefix`, so get_code() +# stitches everything back together untouched. +renamed = 0 +for leaf in walk_leaves(module): + if leaf.type == "name" and leaf.value == "tmp": + leaf.value = "buffer" + renamed += 1 + +note(f"Rewrote {renamed} occurrence(s) of tmp:") +display( + HTML( + f"
{module.get_code()}
" + ), + append=True, +) + +heading("Why this works") +note( + "Each leaf carries its value (the token text) and a " + "prefix (whitespace and comments preceding it). " + "Editing only value leaves comments, indentation, and " + "blank lines exactly where they were -- the foundation parso lays " + "for refactoring tools." +) diff --git a/examples/parso/rename_identifier/config.toml b/examples/parso/rename_identifier/config.toml new file mode 100644 index 0000000..71aa241 --- /dev/null +++ b/examples/parso/rename_identifier/config.toml @@ -0,0 +1 @@ +packages = ["parso"] diff --git a/examples/parso/rename_identifier/setup.py b/examples/parso/rename_identifier/setup.py new file mode 100644 index 0000000..62782f6 --- /dev/null +++ b/examples/parso/rename_identifier/setup.py @@ -0,0 +1,22 @@ +"""Setup for the third example: same names, no 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 parso diff --git a/examples/parso/syntax_errors/code.py b/examples/parso/syntax_errors/code.py new file mode 100644 index 0000000..9eb4768 --- /dev/null +++ b/examples/parso/syntax_errors/code.py @@ -0,0 +1,58 @@ +# --------------------------------------------------------------------- +# Parso recovers from errors, so it can report ALL of them in one pass. +# Python's built-in compile() bails out at the first syntax error; +# parso keeps going and produces a tree plus a list of issues. +# --------------------------------------------------------------------- + +heading("A messy script with several problems") +note( + "Below is some intentionally broken Python. We'll ask parso " + "for every syntax error in one go, instead of fixing them one " + "at a time and re-running." +) + +broken_source = """\ +def greet(name) + print('hello', name) + +for i in range(3) + print(i) + +continue + +x = 1 + +""" + +# Pretty-print the source with line numbers so the errors line up. +numbered = "
"
+for i, line in enumerate(broken_source.splitlines(), start=1):
+    numbered += f"{i:>3}  {line}\\n"
+numbered += "
" +display(HTML(numbered), append=True) + +heading("Asking parso for all the issues") +note( + "load_grammar gives us a grammar object whose iter_errors " + "method yields one Issue per problem found. Each issue knows " + "its message and start/end position." +) + +grammar = parso.load_grammar() +module = grammar.parse(broken_source) +issues = list(grammar.iter_errors(module)) + +note(f"Found {len(issues)} issue(s):") +rows = [] +for issue in issues: + line, col = issue.start_pos + rows.append( + f"{line}:{col}" + f"{issue.message}" + ) +table = ( + "" + "" + + "".join(rows) + + "
PositionMessage
" +) +display(HTML(table), append=True) diff --git a/examples/parso/syntax_errors/config.toml b/examples/parso/syntax_errors/config.toml new file mode 100644 index 0000000..71aa241 --- /dev/null +++ b/examples/parso/syntax_errors/config.toml @@ -0,0 +1 @@ +packages = ["parso"] diff --git a/examples/parso/syntax_errors/setup.py b/examples/parso/syntax_errors/setup.py new file mode 100644 index 0000000..74a3680 --- /dev/null +++ b/examples/parso/syntax_errors/setup.py @@ -0,0 +1,22 @@ +"""Setup for the second example: same names, no 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 parso From c1751d3be52bc3f9f84752f8dda786b050e568cb Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Mon, 8 Jun 2026 17:20:33 +0100 Subject: [PATCH 2/2] Fix code. --- examples/parso/parse_and_walk/code.py | 3 +++ examples/parso/parse_and_walk/setup.py | 3 --- examples/parso/syntax_errors/code.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/parso/parse_and_walk/code.py b/examples/parso/parse_and_walk/code.py index 017c3f1..facb4bd 100644 --- a/examples/parso/parse_and_walk/code.py +++ b/examples/parso/parse_and_walk/code.py @@ -9,6 +9,9 @@ """ from IPython.core.display import display, HTML +import parso + + # A small but realistic snippet of Python to chew on. source = """\ import math diff --git a/examples/parso/parse_and_walk/setup.py b/examples/parso/parse_and_walk/setup.py index de8da24..84faac4 100644 --- a/examples/parso/parse_and_walk/setup.py +++ b/examples/parso/parse_and_walk/setup.py @@ -38,6 +38,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import parso diff --git a/examples/parso/syntax_errors/code.py b/examples/parso/syntax_errors/code.py index 9eb4768..8144fb2 100644 --- a/examples/parso/syntax_errors/code.py +++ b/examples/parso/syntax_errors/code.py @@ -26,7 +26,7 @@ def greet(name) # Pretty-print the source with line numbers so the errors line up. numbered = "
"
 for i, line in enumerate(broken_source.splitlines(), start=1):
-    numbered += f"{i:>3}  {line}\\n"
+    numbered += f"{i:>3}  {line}\n"
 numbered += "
" display(HTML(numbered), append=True)