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..facb4bd
--- /dev/null
+++ b/examples/parso/parse_and_walk/code.py
@@ -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: {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}| Function | Parameters | Location |
|---|
{text}
"), append=True) 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 oftmp 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) + + +import parso diff --git a/examples/parso/syntax_errors/code.py b/examples/parso/syntax_errors/code.py new file mode 100644 index 0000000..8144fb2 --- /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"{issue.message}| Position | Message |
|---|
{text}
"), append=True) + + +import parso