A modern, extensible scripting language built on Python — by CrucibleLab
Project description
Cruhon
A modern, extensible scripting language built on Python.
By CrucibleLab · .clpy files · MIT License · v2.10.2
What is Cruhon?
Cruhon is a scripting language that compiles to Python. It replaces Python's
indented block syntax with a uniform @command[args] syntax, making scripts
easier to read and write — especially for automation, tooling, and data tasks.
Every Cruhon program is valid Python under the hood. You can always inspect
the generated Python with cruhon run --show-python.
The plugin system lets you extend everything: new commands, new block types, new value syntax, new runtime objects — all without touching the core.
128 stdlib namespaces · 1880+ built-in commands · 6169 tests
Install
pip install cruhon
Requirements: Python 3.10+
Quick Start
# hello.clpy
@var[name; "Cruhon"]
@print[Hello from {name}!]
@func[add; a; b]
@return[a + b]
@end
@var[result; add(5, 3)]
@print[5 + 3 = {result}]
@for[i; range(3)]
@print[i = {i}]
@end
cruhon run hello.clpy
CLI
| Command | Description |
|---|---|
cruhon run file.clpy |
Run a script |
cruhon run file.clpy --show-python |
Show generated Python before running |
cruhon run file.clpy --watch |
Re-run automatically when .clpy files change |
cruhon run file.clpy --log cruhon.log [--log-level DEBUG] |
Write diagnostics to a log file |
cruhon repl |
Start an interactive session (:help, :vars, :history, :load, :type, :clear, :quit) |
cruhon docs |
List plugins that ship a command reference |
cruhon fmt file.clpy |
Normalize indentation (writes the file) |
cruhon fmt file.clpy --check |
Exit non-zero if not formatted (CI-friendly) |
cruhon fmt file.clpy --stdout |
Print formatted result without writing |
cruhon build file.clpy |
Compile .clpy → .py |
cruhon build file.clpy -o out.py |
Compile to a specific output file |
cruhon check file.clpy |
Check for syntax errors without running |
cruhon new myproject |
Create a new project scaffold |
cruhon new --plugin myplugin |
Create a plugin scaffold in mods/myplugin/ |
cruhon libs |
List all supported libraries |
cruhon mods |
Show loaded plugins, APIs, block commands, overrides |
cruhon cache |
Show transpile cache stats |
cruhon cache --clear |
Clear the transpile cache |
cruhon --version |
Show version |
Language Reference
Syntax Rules
Every statement starts with @. Arguments go inside [...], separated by ;.
Blocks are opened by a command and closed by @end.
@command[arg1; arg2; key=value]
@block[args]
# body
@end
Variables and Constants
@var[x; 42] # x = 42
@var[name; "Alice"] # name = "Alice"
@var[msg; Hello world] # msg = "Hello world" (bare text → string)
@var[copy; name] # copy = name (identifier → variable reference)
@const[MAX; 100] # MAX = 100 # const (convention: uppercase)
Named parameters work everywhere:
@http.post["https://api.example.com/ban"; reason="spam"; days=7]
Type Annotations
Type annotations are first-class — no @raw required:
@var[score: float; 0.0]
@const[LIMIT: int; 100]
@type[Matrix; list[list[float]]]
@dataclass[Point]
x: float = 0.0
y: float = 0.0
@end
@func[distance; p: Point; q: Point; return=float]
@return[((p.x - q.x)**2 + (p.y - q.y)**2) ** 0.5]
@end
| Syntax | Emits |
|---|---|
@var[x: int; 42] |
x: int = 42 |
@var[x: int] |
x: int (annotation-only) |
@const[LIMIT: int; 100] |
LIMIT: int = 100 # const |
@func[f; a: int; b: str; return=bool] |
def f(a: int, b: str) -> bool: |
@type[Vector; list[float]] |
Vector = list[float] # type alias |
@dataclass[Point] ... @end |
@dataclass decorated class block |
Output and Input
@print[Hello, World!] # print("Hello, World!")
@print[Value is {x}] # print(f"Value is {x}")
@var[line; @input[Enter: ]] # line = input("Enter: ")
String Interpolation
Use {varname} inside any value to embed a variable:
@var[name; "Alice"]
@print[Hello, {name}!] # f"Hello, {name}!"
@var[msg; "Hi, {name}!"] # f"Hi, {name}!"
@var[info; age={user.age}] # f"age={user.age}"
Limitation:
@command[...]cannot be embedded inside another nested Python expression — not inside{}interpolation (@print[{@db.dsn_safe[]}]✗), and not inside a list comprehension or other bracketed sub-expression ([f.name for f in @dataclasses.fields[p]]✗). Bind the inline call to a variable first, then use the variable — this always works and is the idiomatic pattern throughout this doc:@var[safe; @db.dsn_safe[]] @print[Connected to {safe}] @var[fields; @dataclasses.fields[p]] @var[names; [f.name for f in fields]]
Control Flow
@if[x > 0]
@print[positive]
@elif[x == 0]
@print[zero]
@else
@print[negative]
@end
@for[i; range(10)]
@print[{i}]
@end
@while[x > 0]
@var[x; x - 1]
@end
@repeat[5]
@print[hello]
@end
@for[i; range(10)]
@if[i == 5]
@break
@end
@if[i % 2 == 0]
@continue
@end
@print[{i}]
@end
Pattern Matching (Python 3.10+)
@match[status]
@case[200]
@print[OK]
@case[404]
@print[Not Found]
@default
@print[Unknown status]
@end
@match[command.split()]
@case[["quit"]]
@print[Quitting]
@case[["go", direction]]
@print[Going {direction}]
@default
@print[Unknown command]
@end
Case patterns are raw Python match-pattern syntax passed straight through — use a real comma
,for structural list/tuple patterns like["go", direction], not Cruhon's;argument separator.
Functions
@func[greet; name]
@print[Hello, {name}!]
@return[name]
@end
greet("Bob") # call with Python syntax
Async functions:
@async[main]
@var[res; @http.async_get["https://example.com"]]
@var[data; @http.json[res]]
@print[Got {len(data)} bytes]
@end
@asyncio.run[main()]
Async for and async with:
@async[main]
@async.for[item; async_generator()]
@print[{item}]
@end
@async.with[aiofiles.open("file.txt") as f]
@var[content; await f.read()]
@print[{content}]
@end
@end
Classes
@class[Animal]
@func[__init__; self; name]
@var[self.name; name]
@end
@end
@class[Dog; Animal]
@func[speak; self]
@print[Woof! I am {self.name}]
@end
@end
@var[dog; Dog("Rex")]
dog.speak()
Multiple inheritance:
@class[Cat; Animal; Serializable; JsonMixin]
...
@end
Error Handling
@try
@var[x; int("bad")]
@catch[e]
@print[Error: {e}]
@else
@print[no error]
@finally
@print[done]
@end
@raise[ValueError; "invalid input"]
@try
risky_call()
@catch[e]
@raise
@end
Retry and timeout:
@retry[3]
risky_api_call()
@end
@retry[5; requests.ConnectionError]
risky_api_call()
@end
@timeout[30]
slow_operation()
@end
Context Managers
@with[open("data.txt") as f]
@var[content; f.read()]
@print[{content}]
@end
@with[lock]
do_work()
@end
Templates and Pipelines
@template[greeting]
Hello, {name}! You have {count} messages.
@end
@var[msg; @render[greeting; name="Alice"; count=5]]
@print[{msg}]
@pipeline[normalize; str.strip; str.lower]
@var[result; @apply[normalize; " Hello "]] # "hello"
Multi-variable assignment and spread
@let[x; 10; y; 20; z; 30]
@var[n; @spread[max; [3, 1, 4, 1, 5]]] # max(*[...]) → 5
@var[r; @unpack[dict; {"a": 1, "b": 2}]] # dict(**{...})
Other Commands
@del[x]
@del[a; b; c]
@assert[x > 0; "x must be positive"]
@var[home; @env[HOME]]
@var[port; @env[PORT; 8080]]
@import[requests]
@import[requests; req]
@include[utils.clpy]
@swap[a; b]
@inc[counter]
@dec[counter; 5]
Raw Python Blocks
@raw
import sys
from pathlib import Path
x = [i**2 for i in range(10)]
@end
Collections
@var[lst; @list[1; 2; 3]]
@var[d; @dict["name"; "Alice"; "age"; 30]]
Multi-line Expressions
@var[result; max(
score_a,
score_b,
score_c
)]
@var[items; [
"apple",
"banana",
"cherry"
]]
Errors & Diagnostics
When something goes wrong, Cruhon shows the exact line, a caret under the problem, a plain-language hint, and a spelling suggestion:
✗ NameError greet.clpy:3
name 'price' is not defined
1 │ @var[name; "World"]
2 │ @print[Hello]
→ 3 │ @var[total; price + tax]
Hint: 'price' is not defined as a variable.
If you meant text, wrap it in quotes: "price".
cruhon check file.clpy shows the same rich excerpt for syntax errors without
running the script. Color auto-disables for pipes and when NO_COLOR is set.
Logging
# Cruhon engine diagnostics
CRUHON_LOG=cruhon.log cruhon run app.clpy
CRUHON_LOG_LEVEL=DEBUG CRUHON_LOG=cruhon.log cruhon run app.clpy
cruhon run app.clpy --log cruhon.log --log-level DEBUG
Levels: ERROR, WARNING, INFO (default), DEBUG.
For script-level application logging use @log.*:
@log.setup["DEBUG"; "app.log"]
@log.info["Application started"]
@log.warning["Something looks off"]
@log.error["Critical failure"]
Transpile Cache
Cruhon caches compiled output in .cruhon_cache/. Re-runs of unchanged
scripts skip parsing entirely.
cruhon cache # show stats
cruhon cache --clear # delete cache
cruhon run app.clpy --no-cache # bypass for one run
Value Semantics
expr context — right-hand sides of @var, @const, @return, etc.:
"text"→ string literal42,3.14→ numeric literalTrue,False,None→ Python literal[...],{...},(...)→ collection or expression, passed through- Expression with operator / call / dot → passed through as Python expression
- Single identifier → Python variable reference
- Bare text → string literal
display context — @print, @assert message:
- Same as
expr, except a single identifier becomes a string literal - Use
{varname}for variable interpolation
Context Variables (@ctx)
__ctx__ is a shared dict available throughout script execution.
@ctx.set["username"; "Alice"]
@var[u; @ctx.get["username"]]
@var[u; @ctx.get["username"; "guest"]]
@ctx.delete["score"]
@ctx.clear[]
# Stack-based scope
@ctx.push[]
@ctx.set["x"; "inner"]
@var[inner_x; @ctx["x"]]
@print[{inner_x}]
@ctx.pop[]
Standard Libraries — 128 Namespaces
All namespaces are available without @import. Just call them directly.
See library.md for the complete method reference.
Core (Cruhon-native)
| Namespace | What it does |
|---|---|
@file.* |
Read, write, copy, move, glob, mkdir, stat, symlink, chmod… |
@date.* |
now, format, parse, add, diff, timezone, ISO, weekday… |
@text.* |
upper, lower, split, replace, regex, slug, encode, partition… |
@http.* |
GET, POST, PUT, DELETE, upload, auth, async variants… |
@crypto.* |
SHA-256/512, hmac, pbkdf2, scrypt, UUID, token, base64… |
@log.* |
setup, info, warning, error, to_file, get, child, formatter… |
@config.* |
load, save, get, set, keys, dotenv, env (JSON/TOML/INI)… |
@shell.* |
run, output, lines, bg, kill, wait, env, cpu_count… |
@archive.* |
zip, unzip, tar, gzip, bzip2, lzma and all their inverses… |
@mail.* |
send, send_html, IMAP connect/search/fetch, SMTP login… |
@csv.* |
read, write, filter, to_json, append… |
@store.* |
set, get, all, clear, delete (in-memory key-value) |
@color.* |
red, green, blue, yellow, bold, dim, reset… |
@ctx.* |
set, get, push, pop, clear, delete |
@json.* |
load, dump |
Text & Math
| Namespace | Wraps | Highlights |
|---|---|---|
@math.* |
math |
sqrt, floor, ceil, pow, log, sin/cos/tan, gcd, clamp… |
@random.* |
random |
randint, choice, shuffle, sample, gauss, seed… |
@cmath.* |
cmath |
Complex sqrt, exp, log, polar, rect, phase… |
@decimal.* |
decimal |
Exact arithmetic: add, sub, mul, div, round, compare… |
@fraction.* |
fractions |
make, add, sub, mul, div, simplify, to_float… |
@statistics.* |
statistics |
mean, median, mode, stdev, variance, correlation… |
@textwrap.* |
textwrap |
wrap, fill, indent, dedent, shorten, columns… |
@string.* |
string |
ascii_letters, digits, punctuation, capwords, template… |
@unicode.* |
unicodedata |
name, category, normalize, NFC/NFD, strip_accents… |
@colorsys.* |
colorsys |
RGB↔HSV, RGB↔HLS, RGB↔YIQ, hex helpers, luminance, blend |
@codecs.* |
codecs |
rot13, hex, zlib codec, encode/decode, stream wrappers… |
Data & Formats
| Namespace | Wraps | Highlights |
|---|---|---|
@collections.* |
collections |
Counter, defaultdict, deque, namedtuple, OrderedDict… |
@itertools.* |
itertools |
chain, cycle, product, combinations, groupby, flatten… |
@functools.* |
functools |
reduce, partial, lru_cache, wraps, singledispatch… |
@operator.* |
operator |
add, sub, mul, itemgetter, attrgetter, lt, gt, and_… |
@xml.* |
xml.etree.ElementTree |
parse, find, findall, text, attr, children, to_string… |
@toml.* |
tomllib |
loads, load, dumps, get, has, keys, to_dict… |
@yaml.* |
pyyaml |
loads, dumps, load_file, dump_file, get, keys… |
@diff.* |
difflib |
ratio, best_match, ndiff, unified_diff, sequence_matcher… |
@re.* |
re |
search, match, findall, sub, split, groups, compile… |
@struct.* |
struct |
pack, unpack, calcsize, pack_into, unpack_from… |
@binascii.* |
binascii |
hexlify, unhexlify, b2a_base64, a2b_base64, crc32… |
@pickle.* |
pickle |
dump, dumps, load, loads, to_file, from_file, copy… |
@shelve.* |
shelve |
open, get, set, delete, keys, values, items, sync… |
@plist.* |
plistlib |
dumps, loads, to_file, from_file, to_dict… |
@reprlib.* |
reprlib |
repr, shorten, maxstring, maxlist, maxdict, maxset… |
@graphlib.* |
graphlib |
sort, is_dag, ancestors, descendants, roots, leaves… |
File & Path
Path manipulation is covered by @file.* (above) — there is no separate
@pathlib.* namespace.
| Namespace | Wraps | Highlights |
|---|---|---|
@glob.* |
glob |
glob, iglob, recursive, escape, fnmatch… |
@tempfile.* |
tempfile |
file, dir, named, spooled, mkstemp, mkdtemp… |
@fnmatch.* |
fnmatch |
match, filter, translate, fnmatchcase… |
@fileinput.* |
fileinput |
lines, input, filename, lineno, close… |
@stat.* |
stat |
mode_str, is_dir, is_file, is_link, is_socket, permissions… |
@shutil.* |
shutil |
copy, move, tree, rmtree, disk_usage, which, unpack_archive… |
@filecmp.* |
filecmp |
equal, shallow, dircmp, same_files, diff_files, compare… |
@linecache.* |
linecache |
line, lines, count, check, clear… |
@mmap.* |
mmap |
read, slice, size, find, open, seek, put, flush, close… |
@zipapp.* |
zipapp |
create, interpreter, is_archive, copy… |
OS & System
| Namespace | Wraps | Highlights |
|---|---|---|
@os.* |
os |
env, path, listdir, getcwd, makedirs… |
@sys.* |
sys |
argv, exit, path, version, platform, getsizeof, stdin… |
@platform.* |
platform |
system, node, release, version, machine, python_version… |
@gc.* |
gc |
collect, enable, disable, isenabled, count, threshold… |
@inspect.* |
inspect |
signature, members, source, module, file, isfunction… |
@traceback.* |
traceback |
format, print, format_exc, extract, lines, last… |
@warnings.* |
warnings |
warn, ignore, error, once, always, simplefilter… |
@weakref.* |
weakref |
ref, proxy, finalize, deref, is_alive… |
@types.* |
types |
new_class, SimpleNamespace, MappingProxy, is_function… |
@abc.* |
abc |
abstract, abstractmethod, isabstract, ABC, ABCMeta… |
@signal.* |
signal |
handler, send, alarm, pause, set_wakeup, getsignal… |
@atexit.* |
atexit |
register, unregister, handlers… |
@locale.* |
locale |
setlocale, getlocale, format_number, currency, strxfrm… |
@gettext.* |
gettext |
translation, gettext, ngettext, bindtextdomain… |
@sysconfig.* |
sysconfig |
get_path, get_config_var, get_platform, variables… |
@resource.* |
resource |
getrlimit, setrlimit, getrusage, RLIMIT_CPU, RLIMIT_AS… |
@errno.* |
errno |
name, description, code, ENOENT, EEXIST, EACCES… |
@getpass.* |
getpass |
password, user, terminal… |
Networking
| Namespace | Wraps | Highlights |
|---|---|---|
@http.* |
requests / httpx |
GET/POST/PUT/DELETE, upload, auth, sessions, async_get/post/put/patch/delete (httpx-backed)… |
@socket.* |
socket |
connect, send, recv, server, bind, accept, udp, tcp… |
@ssl.* |
ssl |
wrap, context, load_cert, verify_mode, check_hostname… |
@ftp.* |
ftplib |
connect, login, list, download, upload, rename, mkdir… |
@pop3.* |
poplib |
connect, list, retrieve, delete, stat, top, uidl… |
@xmlrpc.* |
xmlrpc.client |
client, call, multi_call, fault, close… |
@httpserver.* |
http.server |
serve, serve_async, threaded, stop, close, port… |
@selectors.* |
selectors |
new, watch_read, watch_write, wait, count, modify… |
@ip.* |
ipaddress |
address, network, is_private, is_global, hosts, netmask… |
@url.* |
urllib.parse |
parse, join, quote, unquote, encode, scheme, netloc… |
@html.* |
html / re |
escape, unescape, strip_tags, links, images, text… |
@webbrowser.* |
webbrowser |
open, open_new, open_tab, get, browsers, controller… |
@mimetypes.* |
mimetypes |
guess, guess_ext, guess_all, is_text, charset, known… |
Concurrency
| Namespace | Wraps | Highlights |
|---|---|---|
@asyncio.* |
asyncio |
run, gather, wait_for, task, lock, queue, semaphore, open… |
@threading.* |
threading |
Thread, Lock, RLock, Event, Semaphore, Condition, Barrier… |
@multiprocessing.* |
multiprocessing |
cpus, pool, map, starmap, process, queue, pipe, event… |
@futures.* |
concurrent.futures |
threads, processes, submit, result, map, wait_first… |
@queue.* |
queue |
Queue, LifoQueue, PriorityQueue, put, get, empty, full… |
@sched.* |
sched |
new, run, after, at, cancel, empty, queue… |
Serialization & Database
| Namespace | Wraps | Highlights |
|---|---|---|
@sqlite.* |
sqlite3 |
open, close, execute, fetchall, fetchone, commit, tables… |
@pickle.* |
pickle |
dump, load, dumps, loads, to_file, from_file, copy… |
@shelve.* |
shelve |
open, get, set, delete, keys, items, sync, close… |
@plist.* |
plistlib |
dumps, loads, to_file, from_file… |
@configparser.* |
configparser |
load, new, get, set, sections, add_section, save… |
Testing & Profiling
| Namespace | Wraps | Highlights |
|---|---|---|
@unittest.* |
unittest |
run, discover, assert_equal, assert_true, mock, patch… |
@doctest.* |
doctest |
run, testmod, testfile, globs, verbose… |
@timeit.* |
timeit |
time, repeat, stmt, setup, auto… |
@profile.* |
cProfile |
run, sort, stats, dump, top, cumulative, callers… |
@tracemalloc.* |
tracemalloc |
start, stop, snapshot, top, compare, peak, size… |
Developer Tools
| Namespace | Wraps | Highlights |
|---|---|---|
@ast.* |
ast |
parse, dump, unparse, walk, names, functions, is_valid… |
@dis.* |
dis |
bytecode, instructions, opnames, consts, varnames… |
@tokenize.* |
tokenize |
tokens, names, keywords, comments, ops, numbers, count… |
@keyword.* |
keyword |
iskeyword, issoftkeyword, all, soft_all, kwlist… |
@importlib.* |
importlib |
import_module, reload, find_spec, source_hash… |
@inspect.* |
inspect |
signature, members, source, isfunction, isclass… |
@pdb.* |
pdb |
bp, pm, run, runeval, runcall, new, set_bp… |
@runpy.* |
runpy |
module, path, module_ns, path_ns, find, result… |
@numbers.* |
numbers |
is_number, is_complex, is_real, is_rational, is_integral |
@reprlib.* |
reprlib |
repr, shorten, maxstring, maxlist, maxdict… |
Encoding & Compression
| Namespace | Wraps | Highlights |
|---|---|---|
@base64.* |
base64 |
encode, decode, urlsafe, b32, b16, standard, pad… |
@codecs.* |
codecs |
encode, decode, rot13, hex, zlib codec, reader, writer… |
@binascii.* |
binascii |
hexlify, unhexlify, b2a_base64, crc32, rlecode… |
@zlib.* |
zlib |
compress, decompress, crc32, adler32, crc32_hex… |
@struct.* |
struct |
pack, unpack, calcsize, iter_unpack… |
@archive.* |
zipfile/tarfile/gzip/bz2/lzma |
zip, unzip, tar, gzip, bzip2, lzma and inverses |
Foreign Function Interface
| Namespace | Wraps | Highlights |
|---|---|---|
@ctypes.* |
ctypes |
load CDLL, all C scalar types, buffer, pointer, cast… |
@array.* |
array |
typed compact arrays — append, extend, insert, pop, slice… |
Configuration & Secrets
| Namespace | Wraps | Highlights |
|---|---|---|
@env.* |
os.environ |
get, require, has, typed (int/float/bool/list/json), set, .env load/parse/save, mask (hide secrets), expand $VAR |
@config.* |
json/tomllib/configparser |
load, save, get, set, keys, dotenv, env |
# Read configuration safely from the environment / a .env file
@env.load[] # auto-load ./.env
@var[port; @env.int["PORT"; 8080]] # typed, with a default
@var[debug; @env.bool["DEBUG"]] # 1/true/yes/on → True
@var[key; @env.require["API_KEY"]] # raises if missing
@var[safe; @env.mask[key]] # "ab••••••89" — safe to log
@print[Using key {safe}]
Utilities
| Namespace | Wraps | Highlights |
|---|---|---|
@argparse.* |
argparse |
new, add, run, run_known, parse, parse_dict… |
@dataclasses.* |
dataclasses |
dataclass, field, asdict, astuple, fields, replace… |
@typing.* |
typing |
Optional, Union, List, Dict, Any, Callable, TypeVar… |
@enum.* |
enum |
Enum, IntEnum, StrEnum, Flag, auto, create, names… |
@contextlib.* |
contextlib |
contextmanager, suppress, redirect_stdout, ExitStack… |
@copy.* |
copy |
copy, deepcopy, replace… |
@io.* |
io |
StringIO, BytesIO, read, write, seek, getvalue… |
@heapq.* |
heapq |
heappush, heappop, heapify, nlargest, nsmallest… |
@bisect.* |
bisect |
bisect_left, bisect_right, insort, insort_left… |
@calendar.* |
calendar |
is_leap, days_in_month, month_name, weekday… |
@pprint.* |
pprint |
pformat, pprint, saferepr, isreadable, isrecursive… |
@shlex.* |
shlex |
split, join, quote, lex, punctuation_chars… |
@colorsys.* |
colorsys |
RGB↔HSV/HLS/YIQ, hex_to_rgb, rgb_to_hex, luminance… |
@zipapp.* |
zipapp |
create, is_archive, interpreter, copy… |
Examples
HTTP API script
@var[res; @http.get["https://jsonplaceholder.typicode.com/todos/1"]]
@var[todo; @http.json[res]]
@print[Title: {todo["title"]}]
@print[Done: {todo["completed"]}]
File processing
@var[lines; @file.readlines["data.txt"]]
@for[line; lines]
@var[clean; @text.strip[line]]
@if[clean]
@file.append["output.txt"; "{clean}\n"]
@end
@end
Async HTTP
@async[fetch_all; urls]
@var[tasks; [@asyncio.task[@http.async_get[u]] for u in urls]]
@var[results; @asyncio.gather[*tasks]]
@return[results]
@end
@var[urls; ["https://example.com", "https://httpbin.org/get"]]
@asyncio.run[fetch_all(urls)]
CSV → JSON
@var[rows; @csv.read["sales.csv"]]
@var[filtered; [r for r in rows if float(r["amount"]) > 100]]
@file.write["big_sales.json"; @json.dump[filtered]]
@print[Exported {len(filtered)} rows]
Parallel processing
@func[process; item]
@return[item * item]
@end
@var[data; list(range(1000))]
@var[pool; @futures.threads[8]]
@var[results; @futures.map[pool; process; data]]
@print[Sum: {sum(results)}]
Color system conversion
@var[rgb; @colorsys.hex_to_rgb["#3498db"]]
@var[h; @colorsys.to_hls[rgb[0]; rgb[1]; rgb[2]]]
@print[Hue: {h[0]:.2f} Lightness: {h[1]:.2f}]
Tokenize Python source
@var[src; "def add(a, b):\n return a + b\n"]
@var[keywords; @tokenize.keywords[src]]
@for[tok; keywords]
@print[keyword: {tok.string}]
@end
Run code under debugger
@var[dbg; @pdb.new[]]
@pdb.set_bp[dbg; "mymodule.py"; 42]
@pdb.run[dbg; "mymodule.run()"]
Plugin System
Cruhon's plugin system lets you extend the language itself. Plugins can:
- Add new
@commandsand@block ... @endcommands - Override existing commands with middleware chains
- Inject runtime objects into scripts (database connections, config, etc.)
- Register inline expression commands (
@var[x; @uuid[]]) - Hook into value evaluation at transpile-time
- Manipulate the AST before code generation
- Communicate with other plugins via expose/consume
- Hook into lifecycle events (before/after run, parse, transpile)
Project Structure
myproject/
├── src/
│ └── main.clpy
└── mods/
└── my-plugin/
├── mod.json
└── __init__.py
cruhon new --plugin my-plugin
mod.json
{
"name": "my-plugin",
"version": "1.0.0",
"description": "What this plugin does",
"cruhon": ">=2.0.0"
}
Any extra field is accessible via api.config("key") inside the plugin.
register(api) — Entry Point
Every plugin must have a register(api) function in __init__.py:
def register(api):
api.command("greet", parse_greet, visit_greet)
Plugin API Reference
api.command(name, parser_fn, visitor_fn)
from cruhon.core.ast_nodes import Node
from dataclasses import dataclass
@dataclass
class GreetNode(Node):
target: str = ""
def parse_greet(parser):
parser.advance()
args = parser.parse_args()
return GreetNode(target=args[0] if args else '"world"', line=0)
def visit_greet(transpiler, node):
return transpiler._line(f'print("Hello, " + str({node.target}))', node.line)
def register(api):
api.command("greet", parse_greet, visit_greet)
api.block_command(name, visitor_fn, scoped=False)
def visit_section(transpiler, node):
title = node.args[0] if node.args else '"Untitled"'
body_code = "\n".join(r for n in node.body if (r := n.accept(transpiler)))
return (
transpiler._line(f'print("=== " + {title} + " ===")') +
"\n" + body_code
)
def register(api):
api.block_command("section", visit_section)
# scoped=True → __ctx__ snapshot, changes don't leak
api.block_command("isolated", visit_isolated, scoped=True)
api.override(command, fn)
Multiple overrides form a middleware chain:
def timed_print(transpiler, node, next_fn):
before = transpiler._line('__t0__ = __import__("time").monotonic()')
result = next_fn()
after = transpiler._line('print(f"[{__import__(\"time\").monotonic()-__t0__:.3f}s]")')
return before + "\n" + result + "\n" + after
def register(api):
api.override("print", timed_print)
api.inject(key, value_or_factory)
def register(api):
api.inject("APP_VERSION", "2.9.0")
api.inject("db", lambda: sqlite3.connect(":memory:"))
api.inject_once(key, factory)
Factory runs once at load time; shared across all runs:
def register(api):
api.inject_once("pool", lambda: ConnectionPool(max_connections=10))
api.inline_command(name, handler_fn)
def handle_uuid(parser):
parser.advance()
parser.parse_args()
return "str(__import__('uuid').uuid4())"
def register(api):
api.inline_command("uuid", handle_uuid)
api.eval_hook(fn)
Hook into value evaluation at transpile-time:
def dollar_env(value, context):
if value.startswith("$") and value[1:].isidentifier():
return f'__import__("os").environ.get("{value[1:]}", "")'
return None
def register(api):
api.eval_hook(dollar_env)
api.ast_hook(node_type, fn)
def prefix_vars(node):
if not node.name.startswith("_"):
node.name = "safe_" + node.name
return node
def register(api):
api.ast_hook("VarNode", prefix_vars)
api.transform(target, fn)
Wrap another plugin's block output:
def register(api):
api.transform("route", time_route)
def time_route(transpiler, node, code):
path = node.args[0] if node.args else '"unknown"'
before = transpiler._line('__t0__ = __import__("time").monotonic()')
after = transpiler._line(f'print(f"route {path} took {{__import__(\"time\").monotonic()-__t0__:.3f}}s")')
return before + "\n" + code + "\n" + after
api.hook(event, fn)
| Event | Signature | When |
|---|---|---|
before_run |
fn(source=str) |
Before parsing |
after_run |
fn(source=str, python_code=str) |
After exec finishes |
before_parse |
fn(source) -> source |
Modify source text |
after_parse |
fn(ast) -> ast |
Modify AST |
before_transpile |
fn(ast) -> ast |
Transpiler pre-hook |
after_transpile |
fn(code) -> code |
Modify generated Python |
on_error |
fn(error=exc) |
On any error |
def register(api):
api.hook("before_run", lambda source: print("[run start]"))
api.hook("on_error", lambda error: print(f"[error] {error}"))
api.expose / api.consume
# Plugin A
def register(api):
api.expose("slugify", lambda s: s.lower().replace(" ", "-"))
# Plugin B
def register(api):
slugify = api.consume("plugin-a", "slugify")
api.namespace(name)
def register(api):
ns = api.namespace("db")
ns.register("query", lambda args: f'__db__.execute({args[0]}).fetchall()')
ns.hook("init", lambda ns: print("db ready"))
ns.hook("destroy", lambda ns: print("db closed"))
Cleanup
api.unregister_command("print")
api.remove_hook("before_run", fn)
api.remove_inject("db_pool")
api.remove_eval_hook(dollar_env)
Other helpers
api.alias("say", "print") # @say → @print
api.require("cruhon-utils >= 1.2.0")
api.is_loaded("cruhon-redis")
api.config("my_setting", default="x")
api.block_hook("enter", on_enter) # runtime block start/end events
api.lexer_hook(lambda src: src.replace("§", "@"))
api.token_hook(lambda toks: [t for t in toks if t.type != "COMMENT"])
Complete Plugin Example
Pick a namespace your plugin doesn't already share with a stdlib namespace (
@log.*,@http.*,@file.*, … are all reserved — seelibrary.mdfor the full list). This example usesapplogto stay clear of the built-in@log.*namespace. Even when a customapi.block_command/api.commandregistration does share a prefix with a stdlib namespace, the plugin's own registration always wins for that exact dotted name — but picking a distinct name avoids the confusion of two different things answering to@log.*.
api.command(name, parser_fn, visitor_fn)expects a matching AST node class namedname.title() + "Node"— for"applog"that'sApplogNode.
"""
mods/cruhon-applog/__init__.py
Adds @applog["msg"] and @applog.timed[label] ... @end
"""
import datetime
class Logger:
def __init__(self):
self.lines = []
def log(self, msg):
ts = datetime.datetime.now().strftime("%H:%M:%S")
line = f"[{ts}] {msg}"
self.lines.append(line)
print(line)
_logger = Logger()
def parse_log(parser):
from cruhon.core.ast_nodes import Node
from dataclasses import dataclass
@dataclass
class ApplogNode(Node):
msg: str = ""
parser.advance()
args = parser.parse_args()
return ApplogNode(msg=args[0] if args else '""', line=0)
def visit_log(transpiler, node):
# Route the message through Cruhon's own value evaluator so
# {variable} interpolation works exactly like it does in @print.
msg = transpiler._eval_value(node.msg, "display")
return transpiler._line(f'logger.log({msg})', node.line)
def visit_timed(transpiler, node):
label = (node.args[0] if node.args else '"block"').strip('"')
body = "\n".join(r for n in node.body if (r := n.accept(transpiler)))
t = transpiler
return "\n".join([
t._line("__t0__ = __import__('time').monotonic()"),
body or t._line("pass"),
t._line(f'logger.log(f"{label} took {{__import__(\'time\').monotonic()-__t0__:.3f}}s")'),
])
def register(api):
api.inject("logger", lambda: _logger)
api.command("applog", parse_log, visit_log)
api.block_command("applog.timed", visit_timed)
api.expose("get_log_lines", lambda: _logger.lines)
Usage:
@applog["Script started"]
@applog.timed["data processing"]
@var[data; [1, 2, 3]]
@var[result; sum(data)]
@applog["Processed {result} items"]
@end
@applog["Done"]
Publishing a Plugin
cruhon-logger/
├── pyproject.toml
└── cruhon_logger/
├── __init__.py
└── mod.json
# pyproject.toml
[project]
name = "cruhon-logger"
version = "1.0.0"
[project.entry-points."cruhon.mods"]
cruhon-logger = "cruhon_logger:register"
pip install cruhon-logger
cruhon run script.clpy # plugin auto-loaded
cruhon mods # see it in the list
Load order: core → stdlib → pip plugins (alphabetical) → local mods/ (alphabetical)
Shortcut Plugins
Four configurable shortcut plugins add shorter aliases and extra convenience methods. All four load together without conflicts:
# cruhon-shortcuts (base)
@var[text; @read["notes.txt"]] # @read → @file.read
@var[stamp; @now[]] # @now → @date.now
@var[id; @uuid[]] # @uuid → @crypto.uuid
# cruhon-shortcuts-pro (math / lists / dicts / text / logic)
@var[x; @clamp[value; 0; 100]]
@var[slug; @snake_case["Hello World"]]
@var[g; @group_by[items; key_fn]]
# cruhon-shortcuts-data (xml / toml / diff / decimal / re / yaml / image / pdf)
@var[cfg; @toml_load["port = 8080"]]
@var[amt; @money["3.14159"]] # decimal.money → 2dp
@var[ok; @is_private_ip["10.0.0.1"]]
Bundled Plugins
These ship in mods/ and load automatically:
@db.* — multi-backend database (cruhon-db)
SQLite, PostgreSQL, MySQL with full CRUD, transactions, and async — 170+
commands. Also env-aware connection, migrations, seeding, and a live
@panel bridge:
@db.connect_env["DATABASE_URL"] # DSN from the environment
@db.migrate["./migrations"] # apply *.sql files once, in order
@db.seed["users"; "fixtures/users.json"] # bulk-load JSON or CSV
@var[safe; @db.dsn_safe[]]
@print[Connected to {safe}] # DSN with the password masked
@panel.start[8787]
@db.attach_panel[] # every query now streams to the panel
@panel.* — live log-stream dashboard (cruhon-panel)
A zero-dependency web panel that streams logs, metrics, and events to the browser over Server-Sent Events — pure standard library.
@panel.start[8787] # → http://127.0.0.1:8787
@panel.attach_logging["INFO"] # mirror all @log.* output to the panel
@log.info["server warming up"] # …shows up live in the browser
@panel.metric["users online"; 42] # update a metric tile
@panel.event["deploy"; {"version": "2.10"}]
@db.attach_panel[] # stream every SQL query too (needs @db)
@panel.open[] # open the dashboard in a browser
@panel.wait[] # keep the panel alive
@discord.* — Discord bot toolkit (cruhon-discord)
~60 commands for building bots — see mods/cruhon-discord.
Creating a New Project
cruhon new myproject
cd myproject
cruhon run src/main.clpy
Creates:
myproject/
├── src/
│ └── main.clpy
└── mods/
└── README.md
Contributing
git clone https://github.com/cruciblelab/cruhon
cd cruhon
pip install -e .
python -m pytest cruhon/tests/
cruhon run cruhon/examples/hello.clpy
Contact
- Discord: discord.gg/SPf5VZ6QPG
- Email: cruciblelab@hotmail.com
- GitHub: github.com/cruciblelab
License
MIT — CrucibleLab
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cruhon-2.10.2.tar.gz.
File metadata
- Download URL: cruhon-2.10.2.tar.gz
- Upload date:
- Size: 336.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1960e5e8feae9d0c317da2158319d1b1c3651a8cc31ef5c5bb044fb74ba2a758
|
|
| MD5 |
c9b1c833107dbf3b4d5c5aded346d2be
|
|
| BLAKE2b-256 |
d771e36a932d19018fe69195c29ba1459f146aa87cb0bad5faf385b0357576ea
|
Provenance
The following attestation bundles were made for cruhon-2.10.2.tar.gz:
Publisher:
publish.yml on cruciblelab/Crucible-cruhon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cruhon-2.10.2.tar.gz -
Subject digest:
1960e5e8feae9d0c317da2158319d1b1c3651a8cc31ef5c5bb044fb74ba2a758 - Sigstore transparency entry: 2168351794
- Sigstore integration time:
-
Permalink:
cruciblelab/Crucible-cruhon@cb037686cbedc4d9577577cc0578346e91867a90 -
Branch / Tag:
refs/tags/v2.10.1 - Owner: https://github.com/cruciblelab
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cb037686cbedc4d9577577cc0578346e91867a90 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cruhon-2.10.2-py3-none-any.whl.
File metadata
- Download URL: cruhon-2.10.2-py3-none-any.whl
- Upload date:
- Size: 377.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40fe6940ecb926454971757793ee0e8afb9aba380e169819bc04999bb57ff666
|
|
| MD5 |
eae22cac0db19213ef1100a3d1eec28b
|
|
| BLAKE2b-256 |
23bbacbee35116b46d882360e73a3c6df11fdedbc92bae97202b3d6eb2d0f7af
|
Provenance
The following attestation bundles were made for cruhon-2.10.2-py3-none-any.whl:
Publisher:
publish.yml on cruciblelab/Crucible-cruhon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cruhon-2.10.2-py3-none-any.whl -
Subject digest:
40fe6940ecb926454971757793ee0e8afb9aba380e169819bc04999bb57ff666 - Sigstore transparency entry: 2168351818
- Sigstore integration time:
-
Permalink:
cruciblelab/Crucible-cruhon@cb037686cbedc4d9577577cc0578346e91867a90 -
Branch / Tag:
refs/tags/v2.10.1 - Owner: https://github.com/cruciblelab
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cb037686cbedc4d9577577cc0578346e91867a90 -
Trigger Event:
push
-
Statement type: