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/parso/README.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions examples/parso/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"parse_and_walk",
"syntax_errors",
"rename_identifier"
]
63 changes: 63 additions & 0 deletions examples/parso/parse_and_walk/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
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

import parso


# 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: <code>{module.type}</code>")
note(f"Round-trip matches original: <strong>{module.get_code() == source}</strong>")

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"<tr><td><code>{name}</code></td>"
f"<td>{', '.join(params) or '&mdash;'}</td>"
f"<td>lines {start_line}&ndash;{end_line}</td></tr>"
)

table = (
"<table border='1' cellpadding='6' cellspacing='0'>"
"<tr><th>Function</th><th>Parameters</th><th>Location</th></tr>"
+ "".join(rows)
+ "</table>"
)
display(HTML(table), append=True)
1 change: 1 addition & 0 deletions examples/parso/parse_and_walk/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["parso"]
40 changes: 40 additions & 0 deletions examples/parso/parse_and_walk/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
61 changes: 61 additions & 0 deletions examples/parso/rename_identifier/code.py
Original file line number Diff line number Diff line change
@@ -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 <code>tmp</code> to "
"<code>buffer</code> 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"<pre style='background:#f6f8fa;padding:8px'>{source}</pre>"), 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 <strong>{renamed}</strong> occurrence(s) of <code>tmp</code>:")
display(
HTML(
f"<pre style='background:#eef7ee;padding:8px'>{module.get_code()}</pre>"
),
append=True,
)

heading("Why this works")
note(
"Each leaf carries its <code>value</code> (the token text) and a "
"<code>prefix</code> (whitespace and comments preceding it). "
"Editing only <code>value</code> leaves comments, indentation, and "
"blank lines exactly where they were -- the foundation parso lays "
"for refactoring tools."
)
1 change: 1 addition & 0 deletions examples/parso/rename_identifier/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["parso"]
22 changes: 22 additions & 0 deletions examples/parso/rename_identifier/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


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


import parso
58 changes: 58 additions & 0 deletions examples/parso/syntax_errors/code.py
Original file line number Diff line number Diff line change
@@ -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 = "<pre style='background:#f6f8fa;padding:8px'>"
for i, line in enumerate(broken_source.splitlines(), start=1):
numbered += f"{i:>3} {line}\n"
numbered += "</pre>"
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 <strong>{len(issues)}</strong> issue(s):")
rows = []
for issue in issues:
line, col = issue.start_pos
rows.append(
f"<tr><td>{line}:{col}</td>"
f"<td><code>{issue.message}</code></td></tr>"
)
table = (
"<table border='1' cellpadding='6' cellspacing='0'>"
"<tr><th>Position</th><th>Message</th></tr>"
+ "".join(rows)
+ "</table>"
)
display(HTML(table), append=True)
1 change: 1 addition & 0 deletions examples/parso/syntax_errors/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["parso"]
22 changes: 22 additions & 0 deletions examples/parso/syntax_errors/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


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


import parso