https://github.com/devbisme/simp_sexp
A simple S-expression parser/beautifier written in Python.
https://github.com/devbisme/simp_sexp
kicad lisp s-expression serialization sexp
Last synced: 8 months ago
JSON representation
A simple S-expression parser/beautifier written in Python.
- Host: GitHub
- URL: https://github.com/devbisme/simp_sexp
- Owner: devbisme
- License: mit
- Created: 2025-05-05T14:12:03.000Z (11 months ago)
- Default Branch: master
- Last Pushed: 2025-05-19T02:11:24.000Z (11 months ago)
- Last Synced: 2025-05-19T03:27:43.305Z (11 months ago)
- Topics: kicad, lisp, s-expression, serialization, sexp
- Language: Python
- Homepage:
- Size: 56.6 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# simp_sexp
A simple S-expression parser for Python.
## Features
- Simple and lightweight S-expression parser
- Parse and manipulate S-expressions with an intuitive object-oriented interface
- Convert between string representations and Python data structures
- Nested expressions are handled automatically
- Pretty-printing support for readable output
- Advanced search capabilities for finding elements within complex S-expressions
- Support for quoted strings with proper escape handling
- Automatic type conversion for numbers
- Convenient `value` property for extracting single values from labeled expressions
## Installation
```bash
pip install simp_sexp
```
## Usage
### Basic Parsing and Formatting
```python
from simp_sexp import Sexp, prettify_sexp
# Parse a string into an S-expression
expr = Sexp("(define (factorial n) (if (= n 0) 1 (* n (factorial (- n 1)))))")
print(expr)
# Output: ['define', ['factorial', 'n'], ['if', ['=', 'n', 0], 1, ['*', 'n', ['factorial', ['-', 'n', 1]]]]]
# Convert an S-expression back to a string
s_expr = expr.to_str()
print(s_expr)
# Output: (define (factorial "n") (if (= "n" 0) 1 (* "n" (factorial (- "n" 1)))))
# Format with pretty printing (default behavior)
pretty = expr.to_str(indent=4)
print(pretty)
"""
Output:
(define
(factorial "n")
(if
(= "n" 0)
1
(* "n" (factorial (- "n" 1)))))
"""
# Format without line breaks
compact = expr.to_str(break_inc=0)
print(compact)
# Output: (define (factorial "n") (if (= "n" 0) 1 (* "n" (factorial (- "n" 1)))))
```
### Working with Simple Expressions
```python
from simp_sexp import Sexp
# Simple list
expr1 = Sexp("(a b c)")
print(expr1) # ['a', 'b', 'c']
# Numbers are automatically converted
expr2 = Sexp("(1 2.5 -3)")
print(expr2) # [1, 2.5, -3]
# Mixed types
expr3 = Sexp("(add 10 20)")
print(expr3) # ['add', 10, 20]
# Create S-expressions from Python lists
list_expr = Sexp(['define', ['square', 'x'], ['*', 'x', 'x']])
print(list_expr.to_str(break_inc=0))
# Output: (define (square "x") (* "x" "x"))
# Control quoting behavior
print(list_expr.to_str(quote_strs=False, break_inc=0))
# Output: (define (square x) (* x x))
```
### Extracting Values with the `value` Property
The `value` property provides a convenient way to extract single values from S-expressions
that contain a single labeled element (a two-element list with a label and value).
```python
from simp_sexp import Sexp
# Extract simple values
version = Sexp("((version 20171130))")
print(version.value) # 20171130
description = Sexp('((description "A test component"))')
print(description.value) # A test component
# Combining with search results
config = Sexp("""
(kicad_pcb
(version 20171130)
(general
(thickness 1.6)
(drawings 5))
(layers
(0 F.Cu signal)))
""")
# Find and extract version
print(f"PCB version: {config.search('/kicad_pcb/version').value}") # PCB version: 20171130
# Find and extract thickness
print(f"Board thickness: {config.search('/kicad_pcb/general/thickness').value}mm") # Board thickness: 1.6mm
# Extract values from multiple search results
print(f"Number of drawings: {config.search('/kicad_pcb/general/drawings').value}") # Number of drawings: 5
# Error handling - value property requires specific structure
try:
invalid = Sexp("(multiple elements here)")
print(invalid.value) # This will raise ValueError
except ValueError as e:
print(f"Error: {e}") # Error: Sexp isn't in a form that permits extracting a single value.
try:
empty = Sexp("()")
print(empty.value) # This will also raise ValueError
except ValueError as e:
print(f"Error: {e}") # Error: Sexp isn't in a form that permits extracting a single value.
```
### Handling Nested Expressions
```python
from simp_sexp import Sexp
# Nested lists
nested = Sexp("(a (b c) (d (e f)))")
print(nested) # ['a', ['b', 'c'], ['d', ['e', 'f']]]
# Access elements
print(nested[0]) # 'a'
print(nested[1]) # ['b', 'c']
print(nested[2][1][0]) # 'e'
# Modify elements
nested[1][1] = 'modified'
print(nested) # ['a', ['b', 'modified'], ['d', ['e', 'f']]]
# Add elements
nested[2][1].append('g')
print(nested) # ['a', ['b', 'modified'], ['d', ['e', 'f', 'g']]]
# Lisp-like function calls
lambda_expr = Sexp("(lambda (x) (+ x 1))")
print(lambda_expr) # ['lambda', ['x'], ['+', 'x', 1]]
```
### Searching S-expressions
```python
from simp_sexp import Sexp
import re
# Create a complex S-expression
config = Sexp("""
(config
(version 1.0)
(settings
(theme dark)
(font "Courier New")
(size 12))
(keybindings
(save "Ctrl+S")
(open "Ctrl+O")
(preferences
(toggle "Ctrl+P")
(help "F1"))))
""")
# Search by key path (relative)
font_results = config.search("font")
print(font_results[0][1]) # ['font', 'Courier New']
# Search by absolute path
version_results = config.search("/config/version")
print(version_results[0][1]) # ['version', 1.0]
# Search using a function
results = config.search(lambda x: len(x) > 2 and x[0] == 'settings')
print(results[0][1]) # ['settings', ['theme', 'dark'], ['font', 'Courier New'], ['size', 12]]
# Search using regex
ctrl_bindings = config.search(re.compile(r'^Ctrl\+'))
print([match[1] for _, match in ctrl_bindings]) # Will show all Ctrl+ keybindings
# Search with contains=True to match any element
theme_results = config.search("dark", contains=True)
print(theme_results[0][1]) # ['theme', 'dark']
# Case-insensitive search
prefs = config.search("PREFERENCES", ignore_case=True)
print(prefs[0][1]) # ['preferences', ['toggle', 'Ctrl+P'], ['help', 'F1']]
```
### Manipulating S-expressions
```python
from simp_sexp import Sexp
# Start with a simple expression
expr = Sexp("(define x 10)")
# Convert to list and modify
expr[2] = 20
print(expr.to_str()) # (define "x" 20)
# Add elements
expr.append(['comment', 'updated value'])
print(expr.to_str(break_inc=0)) # (define "x" 20 (comment "updated value"))
# Create a new expression from scratch
new_expr = Sexp()
new_expr.append('if')
new_expr.append(['>', 'x', 0])
new_expr.append('positive')
new_expr.append('negative')
print(new_expr.to_str(quote_strs=False, break_inc=0))
# Output: (if (> x 0) positive negative)
# Replace parts of an expression
def replace_value(sublist):
if sublist and sublist[0] == 'x':
return ['y']
return sublist
# Find and replace operations in complex expressions
math_expr = Sexp("(+ (* x 3) (/ x 2))")
for path, match in math_expr.search('x'):
# Create the full path to the parent element
parent_path = path[:-1]
index = path[-1]
# Navigate to the parent element
parent = math_expr
for i in parent_path:
parent = parent[i]
# Replace 'x' with 'y'
parent[index] = 'y'
print(math_expr.to_str(quote_strs=False, break_inc=0))
# Output: (+ (* y 3) (/ y 2))
```
### Working with Files
```python
from simp_sexp import Sexp
# Example of loading an S-expression from a file
def load_config(filename):
with open(filename, 'r') as f:
config_str = f.read()
return Sexp(config_str)
# Example of saving an S-expression to a file
def save_config(config_sexp, filename):
with open(filename, 'w') as f:
f.write(config_sexp.to_str(indent=2))
# Usage example (pseudo-code)
# config = load_config("config.sexp")
# config[1][2] = "new_value" # Modify the config
# save_config(config, "config.sexp")
```
## License
MIT