cruhon 2.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cruhon-2.10.0/PKG-INFO +1316 -0
- cruhon-2.10.0/README.md +1284 -0
- cruhon-2.10.0/cruhon/__init__.py +21 -0
- cruhon-2.10.0/cruhon/cli.py +933 -0
- cruhon-2.10.0/cruhon/core/__init__.py +14 -0
- cruhon-2.10.0/cruhon/core/ast_nodes.py +515 -0
- cruhon-2.10.0/cruhon/core/cache.py +250 -0
- cruhon-2.10.0/cruhon/core/dependency_resolver.py +114 -0
- cruhon-2.10.0/cruhon/core/diagnostics.py +354 -0
- cruhon-2.10.0/cruhon/core/lexer.py +309 -0
- cruhon-2.10.0/cruhon/core/libs/__init__.py +1 -0
- cruhon-2.10.0/cruhon/core/libs/abc_.py +40 -0
- cruhon-2.10.0/cruhon/core/libs/archive_.py +238 -0
- cruhon-2.10.0/cruhon/core/libs/argparse_.py +62 -0
- cruhon-2.10.0/cruhon/core/libs/array_.py +151 -0
- cruhon-2.10.0/cruhon/core/libs/ast_.py +106 -0
- cruhon-2.10.0/cruhon/core/libs/asyncio_.py +124 -0
- cruhon-2.10.0/cruhon/core/libs/atexit_.py +26 -0
- cruhon-2.10.0/cruhon/core/libs/base64_.py +38 -0
- cruhon-2.10.0/cruhon/core/libs/binascii_.py +72 -0
- cruhon-2.10.0/cruhon/core/libs/bisect_.py +36 -0
- cruhon-2.10.0/cruhon/core/libs/calendar_.py +153 -0
- cruhon-2.10.0/cruhon/core/libs/cmath_.py +102 -0
- cruhon-2.10.0/cruhon/core/libs/codecs_.py +76 -0
- cruhon-2.10.0/cruhon/core/libs/collections_.py +79 -0
- cruhon-2.10.0/cruhon/core/libs/color_.py +35 -0
- cruhon-2.10.0/cruhon/core/libs/colorsys_.py +78 -0
- cruhon-2.10.0/cruhon/core/libs/config_.py +185 -0
- cruhon-2.10.0/cruhon/core/libs/configparser_.py +97 -0
- cruhon-2.10.0/cruhon/core/libs/contextlib_.py +38 -0
- cruhon-2.10.0/cruhon/core/libs/copy_.py +17 -0
- cruhon-2.10.0/cruhon/core/libs/crypto_.py +165 -0
- cruhon-2.10.0/cruhon/core/libs/csv_.py +132 -0
- cruhon-2.10.0/cruhon/core/libs/ctypes_.py +134 -0
- cruhon-2.10.0/cruhon/core/libs/dataclasses_.py +41 -0
- cruhon-2.10.0/cruhon/core/libs/date_.py +211 -0
- cruhon-2.10.0/cruhon/core/libs/decimal_.py +103 -0
- cruhon-2.10.0/cruhon/core/libs/diff_.py +82 -0
- cruhon-2.10.0/cruhon/core/libs/dis_.py +56 -0
- cruhon-2.10.0/cruhon/core/libs/doctest_.py +49 -0
- cruhon-2.10.0/cruhon/core/libs/email_.py +242 -0
- cruhon-2.10.0/cruhon/core/libs/enum_.py +44 -0
- cruhon-2.10.0/cruhon/core/libs/env_.py +211 -0
- cruhon-2.10.0/cruhon/core/libs/errno_.py +39 -0
- cruhon-2.10.0/cruhon/core/libs/file_.py +244 -0
- cruhon-2.10.0/cruhon/core/libs/filecmp_.py +52 -0
- cruhon-2.10.0/cruhon/core/libs/fileinput_.py +113 -0
- cruhon-2.10.0/cruhon/core/libs/fnmatch_.py +50 -0
- cruhon-2.10.0/cruhon/core/libs/fraction_.py +82 -0
- cruhon-2.10.0/cruhon/core/libs/ftp_.py +94 -0
- cruhon-2.10.0/cruhon/core/libs/functools_.py +41 -0
- cruhon-2.10.0/cruhon/core/libs/futures_.py +96 -0
- cruhon-2.10.0/cruhon/core/libs/gc_.py +62 -0
- cruhon-2.10.0/cruhon/core/libs/getpass_.py +37 -0
- cruhon-2.10.0/cruhon/core/libs/gettext_.py +38 -0
- cruhon-2.10.0/cruhon/core/libs/glob_.py +125 -0
- cruhon-2.10.0/cruhon/core/libs/graphlib_.py +55 -0
- cruhon-2.10.0/cruhon/core/libs/heapq_.py +40 -0
- cruhon-2.10.0/cruhon/core/libs/html_.py +53 -0
- cruhon-2.10.0/cruhon/core/libs/http_.py +353 -0
- cruhon-2.10.0/cruhon/core/libs/httpserver_.py +67 -0
- cruhon-2.10.0/cruhon/core/libs/image_.py +104 -0
- cruhon-2.10.0/cruhon/core/libs/importlib_.py +57 -0
- cruhon-2.10.0/cruhon/core/libs/inspect_.py +101 -0
- cruhon-2.10.0/cruhon/core/libs/io_.py +65 -0
- cruhon-2.10.0/cruhon/core/libs/ip_.py +96 -0
- cruhon-2.10.0/cruhon/core/libs/itertools_.py +88 -0
- cruhon-2.10.0/cruhon/core/libs/json_.py +27 -0
- cruhon-2.10.0/cruhon/core/libs/keyword_.py +34 -0
- cruhon-2.10.0/cruhon/core/libs/linecache_.py +32 -0
- cruhon-2.10.0/cruhon/core/libs/locale_.py +58 -0
- cruhon-2.10.0/cruhon/core/libs/log_.py +104 -0
- cruhon-2.10.0/cruhon/core/libs/mail_.py +334 -0
- cruhon-2.10.0/cruhon/core/libs/math_.py +45 -0
- cruhon-2.10.0/cruhon/core/libs/mimetypes_.py +59 -0
- cruhon-2.10.0/cruhon/core/libs/mmap_.py +78 -0
- cruhon-2.10.0/cruhon/core/libs/multiprocessing_.py +108 -0
- cruhon-2.10.0/cruhon/core/libs/numbers_.py +31 -0
- cruhon-2.10.0/cruhon/core/libs/operator_.py +100 -0
- cruhon-2.10.0/cruhon/core/libs/pdb_.py +75 -0
- cruhon-2.10.0/cruhon/core/libs/pdf_.py +63 -0
- cruhon-2.10.0/cruhon/core/libs/pickle_.py +120 -0
- cruhon-2.10.0/cruhon/core/libs/platform_.py +63 -0
- cruhon-2.10.0/cruhon/core/libs/plist_.py +129 -0
- cruhon-2.10.0/cruhon/core/libs/pop3_.py +75 -0
- cruhon-2.10.0/cruhon/core/libs/pprint_.py +41 -0
- cruhon-2.10.0/cruhon/core/libs/profile_.py +41 -0
- cruhon-2.10.0/cruhon/core/libs/queue_.py +47 -0
- cruhon-2.10.0/cruhon/core/libs/random_.py +64 -0
- cruhon-2.10.0/cruhon/core/libs/re_.py +76 -0
- cruhon-2.10.0/cruhon/core/libs/reprlib_.py +38 -0
- cruhon-2.10.0/cruhon/core/libs/resource_.py +44 -0
- cruhon-2.10.0/cruhon/core/libs/runpy_.py +66 -0
- cruhon-2.10.0/cruhon/core/libs/sched_.py +52 -0
- cruhon-2.10.0/cruhon/core/libs/selectors_.py +71 -0
- cruhon-2.10.0/cruhon/core/libs/shell_.py +179 -0
- cruhon-2.10.0/cruhon/core/libs/shelve_.py +132 -0
- cruhon-2.10.0/cruhon/core/libs/shlex_.py +39 -0
- cruhon-2.10.0/cruhon/core/libs/shutil_.py +86 -0
- cruhon-2.10.0/cruhon/core/libs/signal_.py +65 -0
- cruhon-2.10.0/cruhon/core/libs/socket_.py +136 -0
- cruhon-2.10.0/cruhon/core/libs/sqlite_.py +438 -0
- cruhon-2.10.0/cruhon/core/libs/ssl_.py +75 -0
- cruhon-2.10.0/cruhon/core/libs/stat_.py +81 -0
- cruhon-2.10.0/cruhon/core/libs/statistics_.py +62 -0
- cruhon-2.10.0/cruhon/core/libs/store_.py +45 -0
- cruhon-2.10.0/cruhon/core/libs/string_.py +147 -0
- cruhon-2.10.0/cruhon/core/libs/struct_.py +111 -0
- cruhon-2.10.0/cruhon/core/libs/sys_.py +65 -0
- cruhon-2.10.0/cruhon/core/libs/sysconfig_.py +43 -0
- cruhon-2.10.0/cruhon/core/libs/tempfile_.py +81 -0
- cruhon-2.10.0/cruhon/core/libs/text_.py +329 -0
- cruhon-2.10.0/cruhon/core/libs/textwrap_.py +66 -0
- cruhon-2.10.0/cruhon/core/libs/threading_.py +88 -0
- cruhon-2.10.0/cruhon/core/libs/time_.py +27 -0
- cruhon-2.10.0/cruhon/core/libs/timeit_.py +36 -0
- cruhon-2.10.0/cruhon/core/libs/tokenize_.py +121 -0
- cruhon-2.10.0/cruhon/core/libs/toml_.py +46 -0
- cruhon-2.10.0/cruhon/core/libs/traceback_.py +58 -0
- cruhon-2.10.0/cruhon/core/libs/tracemalloc_.py +58 -0
- cruhon-2.10.0/cruhon/core/libs/types_.py +73 -0
- cruhon-2.10.0/cruhon/core/libs/typing_.py +68 -0
- cruhon-2.10.0/cruhon/core/libs/unicode_.py +93 -0
- cruhon-2.10.0/cruhon/core/libs/unittest_.py +82 -0
- cruhon-2.10.0/cruhon/core/libs/url_.py +50 -0
- cruhon-2.10.0/cruhon/core/libs/warnings_.py +50 -0
- cruhon-2.10.0/cruhon/core/libs/weakref_.py +61 -0
- cruhon-2.10.0/cruhon/core/libs/webbrowser_.py +30 -0
- cruhon-2.10.0/cruhon/core/libs/xml_.py +91 -0
- cruhon-2.10.0/cruhon/core/libs/xmlrpc_.py +44 -0
- cruhon-2.10.0/cruhon/core/libs/yaml_.py +54 -0
- cruhon-2.10.0/cruhon/core/libs/zipapp_.py +54 -0
- cruhon-2.10.0/cruhon/core/libs/zlib_.py +130 -0
- cruhon-2.10.0/cruhon/core/mod_loader.py +1021 -0
- cruhon-2.10.0/cruhon/core/namespace_runtime.py +186 -0
- cruhon-2.10.0/cruhon/core/parser.py +1765 -0
- cruhon-2.10.0/cruhon/core/registry.py +555 -0
- cruhon-2.10.0/cruhon/core/runner.py +733 -0
- cruhon-2.10.0/cruhon/core/syntax_engine.py +213 -0
- cruhon-2.10.0/cruhon/core/transpiler.py +1140 -0
- cruhon-2.10.0/cruhon/mods/cruhon-discord-example/__init__.py +36 -0
- cruhon-2.10.0/cruhon/tests/conftest.py +6 -0
- cruhon-2.10.0/cruhon/tests/test_cache.py +213 -0
- cruhon-2.10.0/cruhon/tests/test_cli.py +206 -0
- cruhon-2.10.0/cruhon/tests/test_concurrency.py +143 -0
- cruhon-2.10.0/cruhon/tests/test_core.py +6052 -0
- cruhon-2.10.0/cruhon/tests/test_crypto_log_config.py +331 -0
- cruhon-2.10.0/cruhon/tests/test_data.py +371 -0
- cruhon-2.10.0/cruhon/tests/test_db_env_migrate.py +190 -0
- cruhon-2.10.0/cruhon/tests/test_db_panel_bridge.py +151 -0
- cruhon-2.10.0/cruhon/tests/test_db_serialization.py +531 -0
- cruhon-2.10.0/cruhon/tests/test_db_serialization_extended.py +565 -0
- cruhon-2.10.0/cruhon/tests/test_db_serialization_v2.py +327 -0
- cruhon-2.10.0/cruhon/tests/test_dev_tools.py +209 -0
- cruhon-2.10.0/cruhon/tests/test_discord.py +1508 -0
- cruhon-2.10.0/cruhon/tests/test_dotted_block_commands.py +190 -0
- cruhon-2.10.0/cruhon/tests/test_expanded_methods.py +403 -0
- cruhon-2.10.0/cruhon/tests/test_expanded_methods2.py +203 -0
- cruhon-2.10.0/cruhon/tests/test_expanded_methods3.py +176 -0
- cruhon-2.10.0/cruhon/tests/test_expanded_methods4.py +428 -0
- cruhon-2.10.0/cruhon/tests/test_file_date.py +318 -0
- cruhon-2.10.0/cruhon/tests/test_file_mgmt_utils.py +243 -0
- cruhon-2.10.0/cruhon/tests/test_file_path.py +481 -0
- cruhon-2.10.0/cruhon/tests/test_gap_fill.py +442 -0
- cruhon-2.10.0/cruhon/tests/test_handlers_compile.py +67 -0
- cruhon-2.10.0/cruhon/tests/test_html_web.py +112 -0
- cruhon-2.10.0/cruhon/tests/test_language_v25.py +415 -0
- cruhon-2.10.0/cruhon/tests/test_language_v26.py +244 -0
- cruhon-2.10.0/cruhon/tests/test_namespace_isolation.py +83 -0
- cruhon-2.10.0/cruhon/tests/test_namespace_runtime.py +46 -0
- cruhon-2.10.0/cruhon/tests/test_networking.py +229 -0
- cruhon-2.10.0/cruhon/tests/test_os_introspect.py +219 -0
- cruhon-2.10.0/cruhon/tests/test_os_system.py +236 -0
- cruhon-2.10.0/cruhon/tests/test_panel.py +182 -0
- cruhon-2.10.0/cruhon/tests/test_schedule.py +140 -0
- cruhon-2.10.0/cruhon/tests/test_shell_archive_mail.py +363 -0
- cruhon-2.10.0/cruhon/tests/test_shortcuts_data.py +124 -0
- cruhon-2.10.0/cruhon/tests/test_shortcuts_data_v25.py +162 -0
- cruhon-2.10.0/cruhon/tests/test_testing_profiling.py +145 -0
- cruhon-2.10.0/cruhon/tests/test_text_http_csv.py +406 -0
- cruhon-2.10.0/cruhon/tests/test_text_math.py +211 -0
- cruhon-2.10.0/cruhon/tests/test_v27.py +169 -0
- cruhon-2.10.0/cruhon.egg-info/PKG-INFO +1316 -0
- cruhon-2.10.0/cruhon.egg-info/SOURCES.txt +188 -0
- cruhon-2.10.0/cruhon.egg-info/dependency_links.txt +1 -0
- cruhon-2.10.0/cruhon.egg-info/entry_points.txt +2 -0
- cruhon-2.10.0/cruhon.egg-info/requires.txt +14 -0
- cruhon-2.10.0/cruhon.egg-info/top_level.txt +1 -0
- cruhon-2.10.0/pyproject.toml +51 -0
- cruhon-2.10.0/setup.cfg +4 -0
cruhon-2.10.0/PKG-INFO
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cruhon
|
|
3
|
+
Version: 2.10.0
|
|
4
|
+
Summary: A modern, extensible scripting language built on Python — by CrucibleLab
|
|
5
|
+
Author-email: CrucibleLab <cruciblelab@hotmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cruciblelab/cruhon
|
|
8
|
+
Project-URL: Repository, https://github.com/cruciblelab/cruhon
|
|
9
|
+
Project-URL: Issues, https://github.com/cruciblelab/cruhon/issues
|
|
10
|
+
Project-URL: Discord, https://discord.gg/SPf5VZ6QPG
|
|
11
|
+
Keywords: language,dsl,scripting,transpiler,cruhon
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
19
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: requests
|
|
23
|
+
Requires-Dist: requests>=2.28; extra == "requests"
|
|
24
|
+
Provides-Extra: discord
|
|
25
|
+
Requires-Dist: discord.py>=2.0; extra == "discord"
|
|
26
|
+
Provides-Extra: async
|
|
27
|
+
Requires-Dist: httpx>=0.24; extra == "async"
|
|
28
|
+
Provides-Extra: full
|
|
29
|
+
Requires-Dist: requests>=2.28; extra == "full"
|
|
30
|
+
Requires-Dist: discord.py>=2.0; extra == "full"
|
|
31
|
+
Requires-Dist: httpx>=0.24; extra == "full"
|
|
32
|
+
|
|
33
|
+
# Cruhon
|
|
34
|
+
|
|
35
|
+
**A modern, extensible scripting language built on Python.**
|
|
36
|
+
By [CrucibleLab](https://github.com/cruciblelab) · `.clpy` files · MIT License · **v2.10.0**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## What is Cruhon?
|
|
41
|
+
|
|
42
|
+
Cruhon is a scripting language that compiles to Python. It replaces Python's
|
|
43
|
+
indented block syntax with a uniform `@command[args]` syntax, making scripts
|
|
44
|
+
easier to read and write — especially for automation, tooling, and data tasks.
|
|
45
|
+
|
|
46
|
+
Every Cruhon program is valid Python under the hood. You can always inspect
|
|
47
|
+
the generated Python with `cruhon run --show-python`.
|
|
48
|
+
|
|
49
|
+
The plugin system lets you extend everything: new commands, new block types,
|
|
50
|
+
new value syntax, new runtime objects — all without touching the core.
|
|
51
|
+
|
|
52
|
+
**128 stdlib namespaces · 1839+ built-in commands · 4039 tests**
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install cruhon
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Requirements:** Python 3.10+
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```clpy
|
|
69
|
+
# hello.clpy
|
|
70
|
+
@var[name; "Cruhon"]
|
|
71
|
+
@print[Hello from {name}!]
|
|
72
|
+
|
|
73
|
+
@func[add; a; b]
|
|
74
|
+
@return[a + b]
|
|
75
|
+
@end
|
|
76
|
+
|
|
77
|
+
@var[result; add(5, 3)]
|
|
78
|
+
@print[5 + 3 = {result}]
|
|
79
|
+
|
|
80
|
+
@for[i; range(3)]
|
|
81
|
+
@print[i = {i}]
|
|
82
|
+
@end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cruhon run hello.clpy
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## CLI
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `cruhon run file.clpy` | Run a script |
|
|
96
|
+
| `cruhon run file.clpy --show-python` | Show generated Python before running |
|
|
97
|
+
| `cruhon run file.clpy --watch` | Re-run automatically when `.clpy` files change |
|
|
98
|
+
| `cruhon run file.clpy --log cruhon.log [--log-level DEBUG]` | Write diagnostics to a log file |
|
|
99
|
+
| `cruhon repl` | Start an interactive session (`:help`, `:vars`, `:history`, `:load`, `:type`, `:clear`, `:quit`) |
|
|
100
|
+
| `cruhon docs` | List plugins that ship a command reference |
|
|
101
|
+
| `cruhon fmt file.clpy` | Normalize indentation (writes the file) |
|
|
102
|
+
| `cruhon fmt file.clpy --check` | Exit non-zero if not formatted (CI-friendly) |
|
|
103
|
+
| `cruhon fmt file.clpy --stdout` | Print formatted result without writing |
|
|
104
|
+
| `cruhon build file.clpy` | Compile `.clpy` → `.py` |
|
|
105
|
+
| `cruhon build file.clpy -o out.py` | Compile to a specific output file |
|
|
106
|
+
| `cruhon check file.clpy` | Check for syntax errors without running |
|
|
107
|
+
| `cruhon new myproject` | Create a new project scaffold |
|
|
108
|
+
| `cruhon new --plugin myplugin` | Create a plugin scaffold in `mods/myplugin/` |
|
|
109
|
+
| `cruhon libs` | List all supported libraries |
|
|
110
|
+
| `cruhon mods` | Show loaded plugins, APIs, block commands, overrides |
|
|
111
|
+
| `cruhon cache` | Show transpile cache stats |
|
|
112
|
+
| `cruhon cache --clear` | Clear the transpile cache |
|
|
113
|
+
| `cruhon --version` | Show version |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Language Reference
|
|
118
|
+
|
|
119
|
+
### Syntax Rules
|
|
120
|
+
|
|
121
|
+
Every statement starts with `@`. Arguments go inside `[...]`, separated by `;`.
|
|
122
|
+
Blocks are opened by a command and closed by `@end`.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
@command[arg1; arg2; key=value]
|
|
126
|
+
|
|
127
|
+
@block[args]
|
|
128
|
+
# body
|
|
129
|
+
@end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Variables and Constants
|
|
133
|
+
|
|
134
|
+
```clpy
|
|
135
|
+
@var[x; 42] # x = 42
|
|
136
|
+
@var[name; "Alice"] # name = "Alice"
|
|
137
|
+
@var[msg; Hello world] # msg = "Hello world" (bare text → string)
|
|
138
|
+
@var[copy; name] # copy = name (identifier → variable reference)
|
|
139
|
+
@const[MAX; 100] # MAX = 100 # const (convention: uppercase)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Named parameters work everywhere:
|
|
143
|
+
|
|
144
|
+
```clpy
|
|
145
|
+
@http.post["https://api.example.com/ban"; reason="spam"; days=7]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Type Annotations
|
|
149
|
+
|
|
150
|
+
Type annotations are first-class — no `@raw` required:
|
|
151
|
+
|
|
152
|
+
```clpy
|
|
153
|
+
@var[score: float; 0.0]
|
|
154
|
+
@const[LIMIT: int; 100]
|
|
155
|
+
@type[Matrix; list[list[float]]]
|
|
156
|
+
|
|
157
|
+
@dataclass[Point]
|
|
158
|
+
x: float = 0.0
|
|
159
|
+
y: float = 0.0
|
|
160
|
+
@end
|
|
161
|
+
|
|
162
|
+
@func[distance; p: Point; q: Point; return=float]
|
|
163
|
+
@return[((p.x - q.x)**2 + (p.y - q.y)**2) ** 0.5]
|
|
164
|
+
@end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Syntax | Emits |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `@var[x: int; 42]` | `x: int = 42` |
|
|
170
|
+
| `@var[x: int]` | `x: int` (annotation-only) |
|
|
171
|
+
| `@const[LIMIT: int; 100]` | `LIMIT: int = 100 # const` |
|
|
172
|
+
| `@func[f; a: int; b: str; return=bool]` | `def f(a: int, b: str) -> bool:` |
|
|
173
|
+
| `@type[Vector; list[float]]` | `Vector = list[float] # type alias` |
|
|
174
|
+
| `@dataclass[Point] ... @end` | `@dataclass` decorated class block |
|
|
175
|
+
|
|
176
|
+
### Output and Input
|
|
177
|
+
|
|
178
|
+
```clpy
|
|
179
|
+
@print[Hello, World!] # print("Hello, World!")
|
|
180
|
+
@print[Value is {x}] # print(f"Value is {x}")
|
|
181
|
+
@var[line; @input[Enter: ]] # line = input("Enter: ")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### String Interpolation
|
|
185
|
+
|
|
186
|
+
Use `{varname}` inside any value to embed a variable:
|
|
187
|
+
|
|
188
|
+
```clpy
|
|
189
|
+
@var[name; "Alice"]
|
|
190
|
+
@print[Hello, {name}!] # f"Hello, {name}!"
|
|
191
|
+
@var[msg; "Hi, {name}!"] # f"Hi, {name}!"
|
|
192
|
+
@var[info; age={user.age}] # f"age={user.age}"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Control Flow
|
|
196
|
+
|
|
197
|
+
```clpy
|
|
198
|
+
@if[x > 0]
|
|
199
|
+
@print[positive]
|
|
200
|
+
@elif[x == 0]
|
|
201
|
+
@print[zero]
|
|
202
|
+
@else
|
|
203
|
+
@print[negative]
|
|
204
|
+
@end
|
|
205
|
+
|
|
206
|
+
@for[i; range(10)]
|
|
207
|
+
@print[{i}]
|
|
208
|
+
@end
|
|
209
|
+
|
|
210
|
+
@while[x > 0]
|
|
211
|
+
@var[x; x - 1]
|
|
212
|
+
@end
|
|
213
|
+
|
|
214
|
+
@repeat[5]
|
|
215
|
+
@print[hello]
|
|
216
|
+
@end
|
|
217
|
+
|
|
218
|
+
@for[i; range(10)]
|
|
219
|
+
@if[i == 5]
|
|
220
|
+
@break
|
|
221
|
+
@end
|
|
222
|
+
@if[i % 2 == 0]
|
|
223
|
+
@continue
|
|
224
|
+
@end
|
|
225
|
+
@print[{i}]
|
|
226
|
+
@end
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Pattern Matching (Python 3.10+)
|
|
230
|
+
|
|
231
|
+
```clpy
|
|
232
|
+
@match[status]
|
|
233
|
+
@case[200]
|
|
234
|
+
@print[OK]
|
|
235
|
+
@case[404]
|
|
236
|
+
@print[Not Found]
|
|
237
|
+
@default
|
|
238
|
+
@print[Unknown status]
|
|
239
|
+
@end
|
|
240
|
+
|
|
241
|
+
@match[command.split()]
|
|
242
|
+
@case[["quit"]]
|
|
243
|
+
@print[Quitting]
|
|
244
|
+
@case[["go", direction]]
|
|
245
|
+
@print[Going {direction}]
|
|
246
|
+
@default
|
|
247
|
+
@print[Unknown command]
|
|
248
|
+
@end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> Case patterns are raw Python match-pattern syntax passed straight through
|
|
252
|
+
> — use a real comma `,` for structural list/tuple patterns like
|
|
253
|
+
> `["go", direction]`, not Cruhon's `;` argument separator.
|
|
254
|
+
|
|
255
|
+
### Functions
|
|
256
|
+
|
|
257
|
+
```clpy
|
|
258
|
+
@func[greet; name]
|
|
259
|
+
@print[Hello, {name}!]
|
|
260
|
+
@return[name]
|
|
261
|
+
@end
|
|
262
|
+
|
|
263
|
+
greet("Bob") # call with Python syntax
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Async functions:
|
|
267
|
+
|
|
268
|
+
```clpy
|
|
269
|
+
@async[main]
|
|
270
|
+
@var[res; @http.async_get["https://example.com"]]
|
|
271
|
+
@var[data; @http.json[res]]
|
|
272
|
+
@print[Got {len(data)} bytes]
|
|
273
|
+
@end
|
|
274
|
+
|
|
275
|
+
@asyncio.run[main()]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Async for and async with:
|
|
279
|
+
|
|
280
|
+
```clpy
|
|
281
|
+
@async[main]
|
|
282
|
+
@async.for[item; async_generator()]
|
|
283
|
+
@print[{item}]
|
|
284
|
+
@end
|
|
285
|
+
|
|
286
|
+
@async.with[aiofiles.open("file.txt") as f]
|
|
287
|
+
@var[content; await f.read()]
|
|
288
|
+
@print[{content}]
|
|
289
|
+
@end
|
|
290
|
+
@end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Classes
|
|
294
|
+
|
|
295
|
+
```clpy
|
|
296
|
+
@class[Animal]
|
|
297
|
+
@func[__init__; self; name]
|
|
298
|
+
@var[self.name; name]
|
|
299
|
+
@end
|
|
300
|
+
@end
|
|
301
|
+
|
|
302
|
+
@class[Dog; Animal]
|
|
303
|
+
@func[speak; self]
|
|
304
|
+
@print[Woof! I am {self.name}]
|
|
305
|
+
@end
|
|
306
|
+
@end
|
|
307
|
+
|
|
308
|
+
@var[dog; Dog("Rex")]
|
|
309
|
+
dog.speak()
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Multiple inheritance:
|
|
313
|
+
|
|
314
|
+
```clpy
|
|
315
|
+
@class[Cat; Animal; Serializable; JsonMixin]
|
|
316
|
+
...
|
|
317
|
+
@end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Error Handling
|
|
321
|
+
|
|
322
|
+
```clpy
|
|
323
|
+
@try
|
|
324
|
+
@var[x; int("bad")]
|
|
325
|
+
@catch[e]
|
|
326
|
+
@print[Error: {e}]
|
|
327
|
+
@else
|
|
328
|
+
@print[no error]
|
|
329
|
+
@finally
|
|
330
|
+
@print[done]
|
|
331
|
+
@end
|
|
332
|
+
|
|
333
|
+
@raise[ValueError; "invalid input"]
|
|
334
|
+
|
|
335
|
+
@try
|
|
336
|
+
risky_call()
|
|
337
|
+
@catch[e]
|
|
338
|
+
@raise
|
|
339
|
+
@end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Retry and timeout:
|
|
343
|
+
|
|
344
|
+
```clpy
|
|
345
|
+
@retry[3]
|
|
346
|
+
risky_api_call()
|
|
347
|
+
@end
|
|
348
|
+
|
|
349
|
+
@retry[5; requests.ConnectionError]
|
|
350
|
+
risky_api_call()
|
|
351
|
+
@end
|
|
352
|
+
|
|
353
|
+
@timeout[30]
|
|
354
|
+
slow_operation()
|
|
355
|
+
@end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Context Managers
|
|
359
|
+
|
|
360
|
+
```clpy
|
|
361
|
+
@with[open("data.txt") as f]
|
|
362
|
+
@var[content; f.read()]
|
|
363
|
+
@print[{content}]
|
|
364
|
+
@end
|
|
365
|
+
|
|
366
|
+
@with[lock]
|
|
367
|
+
do_work()
|
|
368
|
+
@end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Templates and Pipelines
|
|
372
|
+
|
|
373
|
+
```clpy
|
|
374
|
+
@template[greeting]
|
|
375
|
+
Hello, {name}! You have {count} messages.
|
|
376
|
+
@end
|
|
377
|
+
|
|
378
|
+
@var[msg; @render[greeting; name="Alice"; count=5]]
|
|
379
|
+
@print[{msg}]
|
|
380
|
+
|
|
381
|
+
@pipeline[normalize; str.strip; str.lower]
|
|
382
|
+
@var[result; @apply[normalize; " Hello "]] # "hello"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Multi-variable assignment and spread
|
|
386
|
+
|
|
387
|
+
```clpy
|
|
388
|
+
@let[x; 10; y; 20; z; 30]
|
|
389
|
+
|
|
390
|
+
@var[n; @spread[max; [3, 1, 4, 1, 5]]] # max(*[...]) → 5
|
|
391
|
+
@var[r; @unpack[dict; {"a": 1, "b": 2}]] # dict(**{...})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Other Commands
|
|
395
|
+
|
|
396
|
+
```clpy
|
|
397
|
+
@del[x]
|
|
398
|
+
@del[a; b; c]
|
|
399
|
+
@assert[x > 0; "x must be positive"]
|
|
400
|
+
@var[home; @env[HOME]]
|
|
401
|
+
@var[port; @env[PORT; 8080]]
|
|
402
|
+
@import[requests]
|
|
403
|
+
@import[requests; req]
|
|
404
|
+
@include[utils.clpy]
|
|
405
|
+
@swap[a; b]
|
|
406
|
+
@inc[counter]
|
|
407
|
+
@dec[counter; 5]
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Raw Python Blocks
|
|
411
|
+
|
|
412
|
+
```clpy
|
|
413
|
+
@raw
|
|
414
|
+
import sys
|
|
415
|
+
from pathlib import Path
|
|
416
|
+
x = [i**2 for i in range(10)]
|
|
417
|
+
@end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Collections
|
|
421
|
+
|
|
422
|
+
```clpy
|
|
423
|
+
@var[lst; @list[1; 2; 3]]
|
|
424
|
+
@var[d; @dict["name"; "Alice"; "age"; 30]]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Multi-line Expressions
|
|
428
|
+
|
|
429
|
+
```clpy
|
|
430
|
+
@var[result; max(
|
|
431
|
+
score_a,
|
|
432
|
+
score_b,
|
|
433
|
+
score_c
|
|
434
|
+
)]
|
|
435
|
+
|
|
436
|
+
@var[items; [
|
|
437
|
+
"apple",
|
|
438
|
+
"banana",
|
|
439
|
+
"cherry"
|
|
440
|
+
]]
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Errors & Diagnostics
|
|
446
|
+
|
|
447
|
+
When something goes wrong, Cruhon shows the exact line, a caret under the
|
|
448
|
+
problem, a plain-language hint, and a spelling suggestion:
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
✗ NameError greet.clpy:3
|
|
452
|
+
|
|
453
|
+
name 'price' is not defined
|
|
454
|
+
|
|
455
|
+
1 │ @var[name; "World"]
|
|
456
|
+
2 │ @print[Hello]
|
|
457
|
+
→ 3 │ @var[total; price + tax]
|
|
458
|
+
|
|
459
|
+
Hint: 'price' is not defined as a variable.
|
|
460
|
+
If you meant text, wrap it in quotes: "price".
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
`cruhon check file.clpy` shows the same rich excerpt for syntax errors without
|
|
464
|
+
running the script. Color auto-disables for pipes and when `NO_COLOR` is set.
|
|
465
|
+
|
|
466
|
+
### Logging
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
# Cruhon engine diagnostics
|
|
470
|
+
CRUHON_LOG=cruhon.log cruhon run app.clpy
|
|
471
|
+
CRUHON_LOG_LEVEL=DEBUG CRUHON_LOG=cruhon.log cruhon run app.clpy
|
|
472
|
+
cruhon run app.clpy --log cruhon.log --log-level DEBUG
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Levels: `ERROR`, `WARNING`, `INFO` (default), `DEBUG`.
|
|
476
|
+
|
|
477
|
+
For **script-level** application logging use `@log.*`:
|
|
478
|
+
|
|
479
|
+
```clpy
|
|
480
|
+
@log.setup["DEBUG"; "app.log"]
|
|
481
|
+
@log.info["Application started"]
|
|
482
|
+
@log.warning["Something looks off"]
|
|
483
|
+
@log.error["Critical failure"]
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Transpile Cache
|
|
487
|
+
|
|
488
|
+
Cruhon caches compiled output in `.cruhon_cache/`. Re-runs of unchanged
|
|
489
|
+
scripts skip parsing entirely.
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
cruhon cache # show stats
|
|
493
|
+
cruhon cache --clear # delete cache
|
|
494
|
+
cruhon run app.clpy --no-cache # bypass for one run
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Value Semantics
|
|
500
|
+
|
|
501
|
+
**`expr` context** — right-hand sides of `@var`, `@const`, `@return`, etc.:
|
|
502
|
+
- `"text"` → string literal
|
|
503
|
+
- `42`, `3.14` → numeric literal
|
|
504
|
+
- `True`, `False`, `None` → Python literal
|
|
505
|
+
- `[...]`, `{...}`, `(...)` → collection or expression, passed through
|
|
506
|
+
- Expression with operator / call / dot → passed through as Python expression
|
|
507
|
+
- Single identifier → Python variable reference
|
|
508
|
+
- Bare text → string literal
|
|
509
|
+
|
|
510
|
+
**`display` context** — `@print`, `@assert` message:
|
|
511
|
+
- Same as `expr`, except a **single identifier becomes a string literal**
|
|
512
|
+
- Use `{varname}` for variable interpolation
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Context Variables (`@ctx`)
|
|
517
|
+
|
|
518
|
+
`__ctx__` is a shared dict available throughout script execution.
|
|
519
|
+
|
|
520
|
+
```clpy
|
|
521
|
+
@ctx.set["username"; "Alice"]
|
|
522
|
+
@var[u; @ctx.get["username"]]
|
|
523
|
+
@var[u; @ctx.get["username"; "guest"]]
|
|
524
|
+
@ctx.delete["score"]
|
|
525
|
+
@ctx.clear[]
|
|
526
|
+
|
|
527
|
+
# Stack-based scope
|
|
528
|
+
@ctx.push[]
|
|
529
|
+
@ctx.set["x"; "inner"]
|
|
530
|
+
@var[inner_x; @ctx["x"]]
|
|
531
|
+
@print[{inner_x}]
|
|
532
|
+
@ctx.pop[]
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Standard Libraries — 128 Namespaces
|
|
538
|
+
|
|
539
|
+
All namespaces are available without `@import`. Just call them directly.
|
|
540
|
+
See [`library.md`](library.md) for the complete method reference.
|
|
541
|
+
|
|
542
|
+
### Core (Cruhon-native)
|
|
543
|
+
|
|
544
|
+
| Namespace | What it does |
|
|
545
|
+
|---|---|
|
|
546
|
+
| `@file.*` | Read, write, copy, move, glob, mkdir, stat, symlink, chmod… |
|
|
547
|
+
| `@date.*` | now, format, parse, add, diff, timezone, ISO, weekday… |
|
|
548
|
+
| `@text.*` | upper, lower, split, replace, regex, slug, encode, partition… |
|
|
549
|
+
| `@http.*` | GET, POST, PUT, DELETE, upload, auth, async variants… |
|
|
550
|
+
| `@crypto.*` | SHA-256/512, hmac, pbkdf2, scrypt, UUID, token, base64… |
|
|
551
|
+
| `@log.*` | setup, info, warning, error, to_file, get, child, formatter… |
|
|
552
|
+
| `@config.*` | load, save, get, set, keys, dotenv, env (JSON/TOML/INI)… |
|
|
553
|
+
| `@shell.*` | run, output, lines, bg, kill, wait, env, cpu_count… |
|
|
554
|
+
| `@archive.*` | zip, unzip, tar, gzip, bzip2, lzma and all their inverses… |
|
|
555
|
+
| `@mail.*` | send, send_html, IMAP connect/search/fetch, SMTP login… |
|
|
556
|
+
| `@csv.*` | read, write, filter, to_json, append… |
|
|
557
|
+
| `@store.*` | set, get, all, clear, delete (in-memory key-value) |
|
|
558
|
+
| `@color.*` | red, green, blue, yellow, bold, dim, reset… |
|
|
559
|
+
| `@ctx.*` | set, get, push, pop, clear, delete |
|
|
560
|
+
| `@json.*` | load, dump |
|
|
561
|
+
|
|
562
|
+
### Text & Math
|
|
563
|
+
|
|
564
|
+
| Namespace | Wraps | Highlights |
|
|
565
|
+
|---|---|---|
|
|
566
|
+
| `@math.*` | `math` | sqrt, floor, ceil, pow, log, sin/cos/tan, gcd, clamp… |
|
|
567
|
+
| `@random.*` | `random` | randint, choice, shuffle, sample, gauss, seed… |
|
|
568
|
+
| `@cmath.*` | `cmath` | Complex sqrt, exp, log, polar, rect, phase… |
|
|
569
|
+
| `@decimal.*` | `decimal` | Exact arithmetic: add, sub, mul, div, round, compare… |
|
|
570
|
+
| `@fraction.*` | `fractions` | make, add, sub, mul, div, simplify, to_float… |
|
|
571
|
+
| `@statistics.*` | `statistics` | mean, median, mode, stdev, variance, correlation… |
|
|
572
|
+
| `@textwrap.*` | `textwrap` | wrap, fill, indent, dedent, shorten, columns… |
|
|
573
|
+
| `@string.*` | `string` | ascii_letters, digits, punctuation, capwords, template… |
|
|
574
|
+
| `@unicode.*` | `unicodedata` | name, category, normalize, NFC/NFD, strip_accents… |
|
|
575
|
+
| `@colorsys.*` | `colorsys` | RGB↔HSV, RGB↔HLS, RGB↔YIQ, hex helpers, luminance, blend |
|
|
576
|
+
| `@codecs.*` | `codecs` | rot13, hex, zlib codec, encode/decode, stream wrappers… |
|
|
577
|
+
|
|
578
|
+
### Data & Formats
|
|
579
|
+
|
|
580
|
+
| Namespace | Wraps | Highlights |
|
|
581
|
+
|---|---|---|
|
|
582
|
+
| `@collections.*` | `collections` | Counter, defaultdict, deque, namedtuple, OrderedDict… |
|
|
583
|
+
| `@itertools.*` | `itertools` | chain, cycle, product, combinations, groupby, flatten… |
|
|
584
|
+
| `@functools.*` | `functools` | reduce, partial, lru_cache, wraps, singledispatch… |
|
|
585
|
+
| `@operator.*` | `operator` | add, sub, mul, itemgetter, attrgetter, lt, gt, and_… |
|
|
586
|
+
| `@xml.*` | `xml.etree.ElementTree` | parse, find, findall, text, attr, children, to_string… |
|
|
587
|
+
| `@toml.*` | `tomllib` | loads, load, dumps, get, has, keys, to_dict… |
|
|
588
|
+
| `@yaml.*` | `pyyaml` | loads, dumps, load_file, dump_file, get, keys… |
|
|
589
|
+
| `@diff.*` | `difflib` | ratio, best_match, ndiff, unified_diff, sequence_matcher… |
|
|
590
|
+
| `@re.*` | `re` | search, match, findall, sub, split, groups, compile… |
|
|
591
|
+
| `@struct.*` | `struct` | pack, unpack, calcsize, pack_into, unpack_from… |
|
|
592
|
+
| `@binascii.*` | `binascii` | hexlify, unhexlify, b2a_base64, a2b_base64, crc32… |
|
|
593
|
+
| `@pickle.*` | `pickle` | dump, dumps, load, loads, to_file, from_file, copy… |
|
|
594
|
+
| `@shelve.*` | `shelve` | open, get, set, delete, keys, values, items, sync… |
|
|
595
|
+
| `@plist.*` | `plistlib` | dumps, loads, to_file, from_file, to_dict… |
|
|
596
|
+
| `@reprlib.*` | `reprlib` | repr, shorten, maxstring, maxlist, maxdict, maxset… |
|
|
597
|
+
| `@graphlib.*` | `graphlib` | sort, is_dag, ancestors, descendants, roots, leaves… |
|
|
598
|
+
|
|
599
|
+
### File & Path
|
|
600
|
+
|
|
601
|
+
| Namespace | Wraps | Highlights |
|
|
602
|
+
|---|---|---|
|
|
603
|
+
| `@pathlib.*` | `pathlib` | path, join, name, stem, suffix, exists, is_file, mkdir… |
|
|
604
|
+
| `@glob.*` | `glob` | glob, iglob, recursive, escape, fnmatch… |
|
|
605
|
+
| `@tempfile.*` | `tempfile` | file, dir, named, spooled, mkstemp, mkdtemp… |
|
|
606
|
+
| `@fnmatch.*` | `fnmatch` | match, filter, translate, fnmatchcase… |
|
|
607
|
+
| `@fileinput.*` | `fileinput` | lines, input, filename, lineno, close… |
|
|
608
|
+
| `@stat.*` | `stat` | mode_str, is_dir, is_file, is_link, is_socket, permissions… |
|
|
609
|
+
| `@shutil.*` | `shutil` | copy, move, tree, rmtree, disk_usage, which, unpack_archive… |
|
|
610
|
+
| `@filecmp.*` | `filecmp` | equal, shallow, dircmp, same_files, diff_files, compare… |
|
|
611
|
+
| `@linecache.*` | `linecache` | line, lines, count, check, clear… |
|
|
612
|
+
| `@mmap.*` | `mmap` | read, slice, size, find, open, seek, put, flush, close… |
|
|
613
|
+
| `@zipapp.*` | `zipapp` | create, interpreter, is_archive, copy… |
|
|
614
|
+
|
|
615
|
+
### OS & System
|
|
616
|
+
|
|
617
|
+
| Namespace | Wraps | Highlights |
|
|
618
|
+
|---|---|---|
|
|
619
|
+
| `@os.*` | `os` | env, path, listdir, getcwd, makedirs… |
|
|
620
|
+
| `@sys.*` | `sys` | argv, exit, path, version, platform, getsizeof, stdin… |
|
|
621
|
+
| `@platform.*` | `platform` | system, node, release, version, machine, python_version… |
|
|
622
|
+
| `@gc.*` | `gc` | collect, enable, disable, isenabled, count, threshold… |
|
|
623
|
+
| `@inspect.*` | `inspect` | signature, members, source, module, file, isfunction… |
|
|
624
|
+
| `@traceback.*` | `traceback` | format, print, format_exc, extract, lines, last… |
|
|
625
|
+
| `@warnings.*` | `warnings` | warn, ignore, error, once, always, simplefilter… |
|
|
626
|
+
| `@weakref.*` | `weakref` | ref, proxy, finalize, deref, is_alive… |
|
|
627
|
+
| `@types.*` | `types` | new_class, SimpleNamespace, MappingProxy, is_function… |
|
|
628
|
+
| `@abc.*` | `abc` | abstract, abstractmethod, isabstract, ABC, ABCMeta… |
|
|
629
|
+
| `@signal.*` | `signal` | handler, send, alarm, pause, set_wakeup, getsignal… |
|
|
630
|
+
| `@atexit.*` | `atexit` | register, unregister, handlers… |
|
|
631
|
+
| `@locale.*` | `locale` | setlocale, getlocale, format_number, currency, strxfrm… |
|
|
632
|
+
| `@gettext.*` | `gettext` | translation, gettext, ngettext, bindtextdomain… |
|
|
633
|
+
| `@sysconfig.*` | `sysconfig` | get_path, get_config_var, get_platform, variables… |
|
|
634
|
+
| `@resource.*` | `resource` | getrlimit, setrlimit, getrusage, RLIMIT_CPU, RLIMIT_AS… |
|
|
635
|
+
| `@errno.*` | `errno` | name, description, code, ENOENT, EEXIST, EACCES… |
|
|
636
|
+
| `@getpass.*` | `getpass` | password, user, terminal… |
|
|
637
|
+
|
|
638
|
+
### Networking
|
|
639
|
+
|
|
640
|
+
| Namespace | Wraps | Highlights |
|
|
641
|
+
|---|---|---|
|
|
642
|
+
| `@http.*` | `requests` / `httpx` | GET/POST/PUT/DELETE, upload, auth, sessions, async… |
|
|
643
|
+
| `@httpx.*` | `httpx` | client, async_client, timeout, follow_redirects… |
|
|
644
|
+
| `@socket.*` | `socket` | connect, send, recv, server, bind, accept, udp, tcp… |
|
|
645
|
+
| `@ssl.*` | `ssl` | wrap, context, load_cert, verify_mode, check_hostname… |
|
|
646
|
+
| `@ftp.*` | `ftplib` | connect, login, list, download, upload, rename, mkdir… |
|
|
647
|
+
| `@pop3.*` | `poplib` | connect, list, retrieve, delete, stat, top, uidl… |
|
|
648
|
+
| `@xmlrpc.*` | `xmlrpc.client` | client, call, multi_call, fault, close… |
|
|
649
|
+
| `@httpserver.*` | `http.server` | serve, serve_async, threaded, stop, close, port… |
|
|
650
|
+
| `@selectors.*` | `selectors` | new, watch_read, watch_write, wait, count, modify… |
|
|
651
|
+
| `@ip.*` | `ipaddress` | address, network, is_private, is_global, hosts, netmask… |
|
|
652
|
+
| `@url.*` | `urllib.parse` | parse, join, quote, unquote, encode, scheme, netloc… |
|
|
653
|
+
| `@html.*` | `html` / `re` | escape, unescape, strip_tags, links, images, text… |
|
|
654
|
+
| `@webbrowser.*` | `webbrowser` | open, open_new, open_tab, get, browsers, controller… |
|
|
655
|
+
| `@mimetypes.*` | `mimetypes` | guess, guess_ext, guess_all, is_text, charset, known… |
|
|
656
|
+
|
|
657
|
+
### Concurrency
|
|
658
|
+
|
|
659
|
+
| Namespace | Wraps | Highlights |
|
|
660
|
+
|---|---|---|
|
|
661
|
+
| `@asyncio.*` | `asyncio` | run, gather, wait_for, task, lock, queue, semaphore, open… |
|
|
662
|
+
| `@threading.*` | `threading` | Thread, Lock, RLock, Event, Semaphore, Condition, Barrier… |
|
|
663
|
+
| `@multiprocessing.*` | `multiprocessing` | cpus, pool, map, starmap, process, queue, pipe, event… |
|
|
664
|
+
| `@futures.*` | `concurrent.futures` | threads, processes, submit, result, map, wait_first… |
|
|
665
|
+
| `@queue.*` | `queue` | Queue, LifoQueue, PriorityQueue, put, get, empty, full… |
|
|
666
|
+
| `@sched.*` | `sched` | new, run, after, at, cancel, empty, queue… |
|
|
667
|
+
|
|
668
|
+
### Serialization & Database
|
|
669
|
+
|
|
670
|
+
| Namespace | Wraps | Highlights |
|
|
671
|
+
|---|---|---|
|
|
672
|
+
| `@sqlite.*` | `sqlite3` | open, close, execute, fetchall, fetchone, commit, tables… |
|
|
673
|
+
| `@pickle.*` | `pickle` | dump, load, dumps, loads, to_file, from_file, copy… |
|
|
674
|
+
| `@shelve.*` | `shelve` | open, get, set, delete, keys, items, sync, close… |
|
|
675
|
+
| `@plist.*` | `plistlib` | dumps, loads, to_file, from_file… |
|
|
676
|
+
| `@configparser.*` | `configparser` | load, new, get, set, sections, add_section, save… |
|
|
677
|
+
|
|
678
|
+
### Testing & Profiling
|
|
679
|
+
|
|
680
|
+
| Namespace | Wraps | Highlights |
|
|
681
|
+
|---|---|---|
|
|
682
|
+
| `@unittest.*` | `unittest` | run, discover, assert_equal, assert_true, mock, patch… |
|
|
683
|
+
| `@doctest.*` | `doctest` | run, testmod, testfile, globs, verbose… |
|
|
684
|
+
| `@timeit.*` | `timeit` | time, repeat, stmt, setup, auto… |
|
|
685
|
+
| `@profile.*` | `cProfile` | run, sort, stats, dump, top, cumulative, callers… |
|
|
686
|
+
| `@tracemalloc.*` | `tracemalloc` | start, stop, snapshot, top, compare, peak, size… |
|
|
687
|
+
|
|
688
|
+
### Developer Tools
|
|
689
|
+
|
|
690
|
+
| Namespace | Wraps | Highlights |
|
|
691
|
+
|---|---|---|
|
|
692
|
+
| `@ast.*` | `ast` | parse, dump, unparse, walk, names, functions, is_valid… |
|
|
693
|
+
| `@dis.*` | `dis` | bytecode, instructions, opnames, consts, varnames… |
|
|
694
|
+
| `@tokenize.*` | `tokenize` | tokens, names, keywords, comments, ops, numbers, count… |
|
|
695
|
+
| `@keyword.*` | `keyword` | iskeyword, issoftkeyword, all, soft_all, kwlist… |
|
|
696
|
+
| `@importlib.*` | `importlib` | import_module, reload, find_spec, source_hash… |
|
|
697
|
+
| `@inspect.*` | `inspect` | signature, members, source, isfunction, isclass… |
|
|
698
|
+
| `@pdb.*` | `pdb` | bp, pm, run, runeval, runcall, new, set_bp… |
|
|
699
|
+
| `@runpy.*` | `runpy` | module, path, module_ns, path_ns, find, result… |
|
|
700
|
+
| `@numbers.*` | `numbers` | is_number, is_complex, is_real, is_rational, is_integral |
|
|
701
|
+
| `@reprlib.*` | `reprlib` | repr, shorten, maxstring, maxlist, maxdict… |
|
|
702
|
+
|
|
703
|
+
### Encoding & Compression
|
|
704
|
+
|
|
705
|
+
| Namespace | Wraps | Highlights |
|
|
706
|
+
|---|---|---|
|
|
707
|
+
| `@base64.*` | `base64` | encode, decode, urlsafe, b32, b16, standard, pad… |
|
|
708
|
+
| `@codecs.*` | `codecs` | encode, decode, rot13, hex, zlib codec, reader, writer… |
|
|
709
|
+
| `@binascii.*` | `binascii` | hexlify, unhexlify, b2a_base64, crc32, rlecode… |
|
|
710
|
+
| `@zlib.*` | `zlib` | compress, decompress, crc32, adler32, crc32_hex… |
|
|
711
|
+
| `@struct.*` | `struct` | pack, unpack, calcsize, iter_unpack… |
|
|
712
|
+
| `@archive.*` | `zipfile`/`tarfile`/`gzip`/`bz2`/`lzma` | zip, unzip, tar, gzip, bzip2, lzma and inverses |
|
|
713
|
+
|
|
714
|
+
### Foreign Function Interface
|
|
715
|
+
|
|
716
|
+
| Namespace | Wraps | Highlights |
|
|
717
|
+
|---|---|---|
|
|
718
|
+
| `@ctypes.*` | `ctypes` | load CDLL, all C scalar types, buffer, pointer, cast… |
|
|
719
|
+
| `@array.*` | `array` | typed compact arrays — append, extend, insert, pop, slice… |
|
|
720
|
+
|
|
721
|
+
### Configuration & Secrets
|
|
722
|
+
|
|
723
|
+
| Namespace | Wraps | Highlights |
|
|
724
|
+
|---|---|---|
|
|
725
|
+
| `@env.*` | `os.environ` | get, require, has, typed (`int`/`float`/`bool`/`list`/`json`), set, `.env` load/parse/save, **mask** (hide secrets), expand `$VAR` |
|
|
726
|
+
| `@config.*` | `json`/`tomllib`/`configparser` | load, save, get, set, keys, dotenv, env |
|
|
727
|
+
|
|
728
|
+
```clpy
|
|
729
|
+
# Read configuration safely from the environment / a .env file
|
|
730
|
+
@env.load[] # auto-load ./.env
|
|
731
|
+
@var[port; @env.int["PORT"; 8080]] # typed, with a default
|
|
732
|
+
@var[debug; @env.bool["DEBUG"]] # 1/true/yes/on → True
|
|
733
|
+
@var[key; @env.require["API_KEY"]] # raises if missing
|
|
734
|
+
@var[safe; @env.mask[key]] # "ab••••••89" — safe to log
|
|
735
|
+
@print[Using key {safe}]
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Utilities
|
|
739
|
+
|
|
740
|
+
| Namespace | Wraps | Highlights |
|
|
741
|
+
|---|---|---|
|
|
742
|
+
| `@argparse.*` | `argparse` | new, add, run, run_known, parse, parse_dict… |
|
|
743
|
+
| `@dataclasses.*` | `dataclasses` | dataclass, field, asdict, astuple, fields, replace… |
|
|
744
|
+
| `@typing.*` | `typing` | Optional, Union, List, Dict, Any, Callable, TypeVar… |
|
|
745
|
+
| `@enum.*` | `enum` | Enum, IntEnum, StrEnum, Flag, auto, create, names… |
|
|
746
|
+
| `@contextlib.*` | `contextlib` | contextmanager, suppress, redirect_stdout, ExitStack… |
|
|
747
|
+
| `@copy.*` | `copy` | copy, deepcopy, replace… |
|
|
748
|
+
| `@io.*` | `io` | StringIO, BytesIO, read, write, seek, getvalue… |
|
|
749
|
+
| `@heapq.*` | `heapq` | heappush, heappop, heapify, nlargest, nsmallest… |
|
|
750
|
+
| `@bisect.*` | `bisect` | bisect_left, bisect_right, insort, insort_left… |
|
|
751
|
+
| `@calendar.*` | `calendar` | is_leap, days_in_month, month_name, weekday… |
|
|
752
|
+
| `@pprint.*` | `pprint` | pformat, pprint, saferepr, isreadable, isrecursive… |
|
|
753
|
+
| `@shlex.*` | `shlex` | split, join, quote, lex, punctuation_chars… |
|
|
754
|
+
| `@colorsys.*` | `colorsys` | RGB↔HSV/HLS/YIQ, hex_to_rgb, rgb_to_hex, luminance… |
|
|
755
|
+
| `@zipapp.*` | `zipapp` | create, is_archive, interpreter, copy… |
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## Examples
|
|
760
|
+
|
|
761
|
+
### HTTP API script
|
|
762
|
+
|
|
763
|
+
```clpy
|
|
764
|
+
@var[res; @http.get["https://jsonplaceholder.typicode.com/todos/1"]]
|
|
765
|
+
@var[todo; @http.json[res]]
|
|
766
|
+
@print[Title: {todo["title"]}]
|
|
767
|
+
@print[Done: {todo["completed"]}]
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### File processing
|
|
771
|
+
|
|
772
|
+
```clpy
|
|
773
|
+
@var[lines; @file.readlines["data.txt"]]
|
|
774
|
+
@for[line; lines]
|
|
775
|
+
@var[clean; @text.strip[line]]
|
|
776
|
+
@if[clean]
|
|
777
|
+
@file.append["output.txt"; "{clean}\n"]
|
|
778
|
+
@end
|
|
779
|
+
@end
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### Async HTTP
|
|
783
|
+
|
|
784
|
+
```clpy
|
|
785
|
+
@async[fetch_all; urls]
|
|
786
|
+
@var[tasks; [@asyncio.task[@http.async_get[u]] for u in urls]]
|
|
787
|
+
@var[results; @asyncio.gather[*tasks]]
|
|
788
|
+
@return[results]
|
|
789
|
+
@end
|
|
790
|
+
|
|
791
|
+
@var[urls; ["https://example.com", "https://httpbin.org/get"]]
|
|
792
|
+
@asyncio.run[fetch_all(urls)]
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### CSV → JSON
|
|
796
|
+
|
|
797
|
+
```clpy
|
|
798
|
+
@var[rows; @csv.read["sales.csv"]]
|
|
799
|
+
@var[filtered; [r for r in rows if float(r["amount"]) > 100]]
|
|
800
|
+
@file.write["big_sales.json"; @json.dump[filtered]]
|
|
801
|
+
@print[Exported {len(filtered)} rows]
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Parallel processing
|
|
805
|
+
|
|
806
|
+
```clpy
|
|
807
|
+
@func[process; item]
|
|
808
|
+
@return[item * item]
|
|
809
|
+
@end
|
|
810
|
+
|
|
811
|
+
@var[data; list(range(1000))]
|
|
812
|
+
@var[pool; @futures.threads[8]]
|
|
813
|
+
@var[results; @futures.map[pool; process; data]]
|
|
814
|
+
@print[Sum: {sum(results)}]
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Color system conversion
|
|
818
|
+
|
|
819
|
+
```clpy
|
|
820
|
+
@var[rgb; @colorsys.hex_to_rgb["#3498db"]]
|
|
821
|
+
@var[h; @colorsys.to_hls[rgb[0]; rgb[1]; rgb[2]]]
|
|
822
|
+
@print[Hue: {h[0]:.2f} Lightness: {h[1]:.2f}]
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Tokenize Python source
|
|
826
|
+
|
|
827
|
+
```clpy
|
|
828
|
+
@var[src; "def add(a, b):\n return a + b\n"]
|
|
829
|
+
@var[keywords; @tokenize.keywords[src]]
|
|
830
|
+
@for[tok; keywords]
|
|
831
|
+
@print[keyword: {tok.string}]
|
|
832
|
+
@end
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Run code under debugger
|
|
836
|
+
|
|
837
|
+
```clpy
|
|
838
|
+
@var[dbg; @pdb.new[]]
|
|
839
|
+
@pdb.set_bp[dbg; "mymodule.py"; 42]
|
|
840
|
+
@pdb.run[dbg; "mymodule.run()"]
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## Plugin System
|
|
846
|
+
|
|
847
|
+
Cruhon's plugin system lets you extend the language itself. Plugins can:
|
|
848
|
+
|
|
849
|
+
- Add new `@commands` and `@block ... @end` commands
|
|
850
|
+
- Override existing commands with middleware chains
|
|
851
|
+
- Inject runtime objects into scripts (database connections, config, etc.)
|
|
852
|
+
- Register inline expression commands (`@var[x; @uuid[]]`)
|
|
853
|
+
- Hook into value evaluation at transpile-time
|
|
854
|
+
- Manipulate the AST before code generation
|
|
855
|
+
- Communicate with other plugins via expose/consume
|
|
856
|
+
- Hook into lifecycle events (before/after run, parse, transpile)
|
|
857
|
+
|
|
858
|
+
### Project Structure
|
|
859
|
+
|
|
860
|
+
```
|
|
861
|
+
myproject/
|
|
862
|
+
├── src/
|
|
863
|
+
│ └── main.clpy
|
|
864
|
+
└── mods/
|
|
865
|
+
└── my-plugin/
|
|
866
|
+
├── mod.json
|
|
867
|
+
└── __init__.py
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
cruhon new --plugin my-plugin
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### mod.json
|
|
875
|
+
|
|
876
|
+
```json
|
|
877
|
+
{
|
|
878
|
+
"name": "my-plugin",
|
|
879
|
+
"version": "1.0.0",
|
|
880
|
+
"description": "What this plugin does",
|
|
881
|
+
"cruhon": ">=2.0.0"
|
|
882
|
+
}
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
Any extra field is accessible via `api.config("key")` inside the plugin.
|
|
886
|
+
|
|
887
|
+
### `register(api)` — Entry Point
|
|
888
|
+
|
|
889
|
+
Every plugin must have a `register(api)` function in `__init__.py`:
|
|
890
|
+
|
|
891
|
+
```python
|
|
892
|
+
def register(api):
|
|
893
|
+
api.command("greet", parse_greet, visit_greet)
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## Plugin API Reference
|
|
899
|
+
|
|
900
|
+
### `api.command(name, parser_fn, visitor_fn)`
|
|
901
|
+
|
|
902
|
+
```python
|
|
903
|
+
from cruhon.core.ast_nodes import Node
|
|
904
|
+
from dataclasses import dataclass
|
|
905
|
+
|
|
906
|
+
@dataclass
|
|
907
|
+
class GreetNode(Node):
|
|
908
|
+
target: str = ""
|
|
909
|
+
|
|
910
|
+
def parse_greet(parser):
|
|
911
|
+
parser.advance()
|
|
912
|
+
args = parser.parse_args()
|
|
913
|
+
return GreetNode(target=args[0] if args else '"world"', line=0)
|
|
914
|
+
|
|
915
|
+
def visit_greet(transpiler, node):
|
|
916
|
+
return transpiler._line(f'print("Hello, " + str({node.target}))', node.line)
|
|
917
|
+
|
|
918
|
+
def register(api):
|
|
919
|
+
api.command("greet", parse_greet, visit_greet)
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### `api.block_command(name, visitor_fn, scoped=False)`
|
|
923
|
+
|
|
924
|
+
```python
|
|
925
|
+
def visit_section(transpiler, node):
|
|
926
|
+
title = node.args[0] if node.args else '"Untitled"'
|
|
927
|
+
body_code = "\n".join(r for n in node.body if (r := n.accept(transpiler)))
|
|
928
|
+
return (
|
|
929
|
+
transpiler._line(f'print("=== " + {title} + " ===")') +
|
|
930
|
+
"\n" + body_code
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
def register(api):
|
|
934
|
+
api.block_command("section", visit_section)
|
|
935
|
+
# scoped=True → __ctx__ snapshot, changes don't leak
|
|
936
|
+
api.block_command("isolated", visit_isolated, scoped=True)
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### `api.override(command, fn)`
|
|
940
|
+
|
|
941
|
+
Multiple overrides form a middleware chain:
|
|
942
|
+
|
|
943
|
+
```python
|
|
944
|
+
def timed_print(transpiler, node, next_fn):
|
|
945
|
+
before = transpiler._line('__t0__ = __import__("time").monotonic()')
|
|
946
|
+
result = next_fn()
|
|
947
|
+
after = transpiler._line('print(f"[{__import__(\"time\").monotonic()-__t0__:.3f}s]")')
|
|
948
|
+
return before + "\n" + result + "\n" + after
|
|
949
|
+
|
|
950
|
+
def register(api):
|
|
951
|
+
api.override("print", timed_print)
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### `api.inject(key, value_or_factory)`
|
|
955
|
+
|
|
956
|
+
```python
|
|
957
|
+
def register(api):
|
|
958
|
+
api.inject("APP_VERSION", "2.9.0")
|
|
959
|
+
api.inject("db", lambda: sqlite3.connect(":memory:"))
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### `api.inject_once(key, factory)`
|
|
963
|
+
|
|
964
|
+
Factory runs once at load time; shared across all runs:
|
|
965
|
+
|
|
966
|
+
```python
|
|
967
|
+
def register(api):
|
|
968
|
+
api.inject_once("pool", lambda: ConnectionPool(max_connections=10))
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### `api.inline_command(name, handler_fn)`
|
|
972
|
+
|
|
973
|
+
```python
|
|
974
|
+
def handle_uuid(parser):
|
|
975
|
+
parser.advance()
|
|
976
|
+
parser.parse_args()
|
|
977
|
+
return "str(__import__('uuid').uuid4())"
|
|
978
|
+
|
|
979
|
+
def register(api):
|
|
980
|
+
api.inline_command("uuid", handle_uuid)
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### `api.eval_hook(fn)`
|
|
984
|
+
|
|
985
|
+
Hook into value evaluation at transpile-time:
|
|
986
|
+
|
|
987
|
+
```python
|
|
988
|
+
def dollar_env(value, context):
|
|
989
|
+
if value.startswith("$") and value[1:].isidentifier():
|
|
990
|
+
return f'__import__("os").environ.get("{value[1:]}", "")'
|
|
991
|
+
return None
|
|
992
|
+
|
|
993
|
+
def register(api):
|
|
994
|
+
api.eval_hook(dollar_env)
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### `api.ast_hook(node_type, fn)`
|
|
998
|
+
|
|
999
|
+
```python
|
|
1000
|
+
def prefix_vars(node):
|
|
1001
|
+
if not node.name.startswith("_"):
|
|
1002
|
+
node.name = "safe_" + node.name
|
|
1003
|
+
return node
|
|
1004
|
+
|
|
1005
|
+
def register(api):
|
|
1006
|
+
api.ast_hook("VarNode", prefix_vars)
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### `api.transform(target, fn)`
|
|
1010
|
+
|
|
1011
|
+
Wrap another plugin's block output:
|
|
1012
|
+
|
|
1013
|
+
```python
|
|
1014
|
+
def register(api):
|
|
1015
|
+
api.transform("route", time_route)
|
|
1016
|
+
|
|
1017
|
+
def time_route(transpiler, node, code):
|
|
1018
|
+
path = node.args[0] if node.args else '"unknown"'
|
|
1019
|
+
before = transpiler._line('__t0__ = __import__("time").monotonic()')
|
|
1020
|
+
after = transpiler._line(f'print(f"route {path} took {{__import__(\"time\").monotonic()-__t0__:.3f}}s")')
|
|
1021
|
+
return before + "\n" + code + "\n" + after
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### `api.hook(event, fn)`
|
|
1025
|
+
|
|
1026
|
+
| Event | Signature | When |
|
|
1027
|
+
|---|---|---|
|
|
1028
|
+
| `before_run` | `fn(source=str)` | Before parsing |
|
|
1029
|
+
| `after_run` | `fn(source=str, python_code=str)` | After exec finishes |
|
|
1030
|
+
| `before_parse` | `fn(source) -> source` | Modify source text |
|
|
1031
|
+
| `after_parse` | `fn(ast) -> ast` | Modify AST |
|
|
1032
|
+
| `before_transpile` | `fn(ast) -> ast` | Transpiler pre-hook |
|
|
1033
|
+
| `after_transpile` | `fn(code) -> code` | Modify generated Python |
|
|
1034
|
+
| `on_error` | `fn(error=exc)` | On any error |
|
|
1035
|
+
|
|
1036
|
+
```python
|
|
1037
|
+
def register(api):
|
|
1038
|
+
api.hook("before_run", lambda source: print("[run start]"))
|
|
1039
|
+
api.hook("on_error", lambda error: print(f"[error] {error}"))
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### `api.expose` / `api.consume`
|
|
1043
|
+
|
|
1044
|
+
```python
|
|
1045
|
+
# Plugin A
|
|
1046
|
+
def register(api):
|
|
1047
|
+
api.expose("slugify", lambda s: s.lower().replace(" ", "-"))
|
|
1048
|
+
|
|
1049
|
+
# Plugin B
|
|
1050
|
+
def register(api):
|
|
1051
|
+
slugify = api.consume("plugin-a", "slugify")
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### `api.namespace(name)`
|
|
1055
|
+
|
|
1056
|
+
```python
|
|
1057
|
+
def register(api):
|
|
1058
|
+
ns = api.namespace("db")
|
|
1059
|
+
ns.register("query", lambda args: f'__db__.execute({args[0]}).fetchall()')
|
|
1060
|
+
ns.hook("init", lambda ns: print("db ready"))
|
|
1061
|
+
ns.hook("destroy", lambda ns: print("db closed"))
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Cleanup
|
|
1065
|
+
|
|
1066
|
+
```python
|
|
1067
|
+
api.unregister_command("print")
|
|
1068
|
+
api.remove_hook("before_run", fn)
|
|
1069
|
+
api.remove_inject("db_pool")
|
|
1070
|
+
api.remove_eval_hook(dollar_env)
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
### Other helpers
|
|
1074
|
+
|
|
1075
|
+
```python
|
|
1076
|
+
api.alias("say", "print") # @say → @print
|
|
1077
|
+
api.require("cruhon-utils >= 1.2.0")
|
|
1078
|
+
api.is_loaded("cruhon-redis")
|
|
1079
|
+
api.config("my_setting", default="x")
|
|
1080
|
+
api.block_hook("enter", on_enter) # runtime block start/end events
|
|
1081
|
+
api.lexer_hook(lambda src: src.replace("§", "@"))
|
|
1082
|
+
api.token_hook(lambda toks: [t for t in toks if t.type != "COMMENT"])
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
## Complete Plugin Example
|
|
1088
|
+
|
|
1089
|
+
> Pick a namespace your plugin doesn't already share with a stdlib
|
|
1090
|
+
> namespace (`@log.*`, `@http.*`, `@file.*`, … are all reserved — see
|
|
1091
|
+
> [`library.md`](library.md) for the full list). This example uses
|
|
1092
|
+
> `applog` to stay clear of the built-in `@log.*` namespace. Even when a
|
|
1093
|
+
> custom `api.block_command`/`api.command` registration *does* share a
|
|
1094
|
+
> prefix with a stdlib namespace, the plugin's own registration always
|
|
1095
|
+
> wins for that exact dotted name — but picking a distinct name avoids
|
|
1096
|
+
> the confusion of two different things answering to `@log.*`.
|
|
1097
|
+
>
|
|
1098
|
+
> `api.command(name, parser_fn, visitor_fn)` expects a matching AST node
|
|
1099
|
+
> class named `name.title() + "Node"` — for `"applog"` that's `ApplogNode`.
|
|
1100
|
+
|
|
1101
|
+
```python
|
|
1102
|
+
"""
|
|
1103
|
+
mods/cruhon-applog/__init__.py
|
|
1104
|
+
Adds @applog["msg"] and @applog.timed[label] ... @end
|
|
1105
|
+
"""
|
|
1106
|
+
import datetime
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
class Logger:
|
|
1110
|
+
def __init__(self):
|
|
1111
|
+
self.lines = []
|
|
1112
|
+
|
|
1113
|
+
def log(self, msg):
|
|
1114
|
+
ts = datetime.datetime.now().strftime("%H:%M:%S")
|
|
1115
|
+
line = f"[{ts}] {msg}"
|
|
1116
|
+
self.lines.append(line)
|
|
1117
|
+
print(line)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
_logger = Logger()
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def parse_log(parser):
|
|
1124
|
+
from cruhon.core.ast_nodes import Node
|
|
1125
|
+
from dataclasses import dataclass
|
|
1126
|
+
|
|
1127
|
+
@dataclass
|
|
1128
|
+
class ApplogNode(Node):
|
|
1129
|
+
msg: str = ""
|
|
1130
|
+
|
|
1131
|
+
parser.advance()
|
|
1132
|
+
args = parser.parse_args()
|
|
1133
|
+
return ApplogNode(msg=args[0] if args else '""', line=0)
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def visit_log(transpiler, node):
|
|
1137
|
+
# Route the message through Cruhon's own value evaluator so
|
|
1138
|
+
# {variable} interpolation works exactly like it does in @print.
|
|
1139
|
+
msg = transpiler._eval_value(node.msg, "display")
|
|
1140
|
+
return transpiler._line(f'logger.log({msg})', node.line)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def visit_timed(transpiler, node):
|
|
1144
|
+
label = (node.args[0] if node.args else '"block"').strip('"')
|
|
1145
|
+
body = "\n".join(r for n in node.body if (r := n.accept(transpiler)))
|
|
1146
|
+
t = transpiler
|
|
1147
|
+
return "\n".join([
|
|
1148
|
+
t._line("__t0__ = __import__('time').monotonic()"),
|
|
1149
|
+
body or t._line("pass"),
|
|
1150
|
+
t._line(f'logger.log(f"{label} took {{__import__(\'time\').monotonic()-__t0__:.3f}}s")'),
|
|
1151
|
+
])
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def register(api):
|
|
1155
|
+
api.inject("logger", lambda: _logger)
|
|
1156
|
+
api.command("applog", parse_log, visit_log)
|
|
1157
|
+
api.block_command("applog.timed", visit_timed)
|
|
1158
|
+
api.expose("get_log_lines", lambda: _logger.lines)
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
Usage:
|
|
1162
|
+
|
|
1163
|
+
```clpy
|
|
1164
|
+
@applog["Script started"]
|
|
1165
|
+
|
|
1166
|
+
@applog.timed["data processing"]
|
|
1167
|
+
@var[data; [1, 2, 3]]
|
|
1168
|
+
@var[result; sum(data)]
|
|
1169
|
+
@applog["Processed {result} items"]
|
|
1170
|
+
@end
|
|
1171
|
+
|
|
1172
|
+
@applog["Done"]
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
## Publishing a Plugin
|
|
1178
|
+
|
|
1179
|
+
```
|
|
1180
|
+
cruhon-logger/
|
|
1181
|
+
├── pyproject.toml
|
|
1182
|
+
└── cruhon_logger/
|
|
1183
|
+
├── __init__.py
|
|
1184
|
+
└── mod.json
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
```toml
|
|
1188
|
+
# pyproject.toml
|
|
1189
|
+
[project]
|
|
1190
|
+
name = "cruhon-logger"
|
|
1191
|
+
version = "1.0.0"
|
|
1192
|
+
|
|
1193
|
+
[project.entry-points."cruhon.mods"]
|
|
1194
|
+
cruhon-logger = "cruhon_logger:register"
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
```bash
|
|
1198
|
+
pip install cruhon-logger
|
|
1199
|
+
cruhon run script.clpy # plugin auto-loaded
|
|
1200
|
+
cruhon mods # see it in the list
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
**Load order:** core → stdlib → pip plugins (alphabetical) → local `mods/` (alphabetical)
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
## Shortcut Plugins
|
|
1208
|
+
|
|
1209
|
+
Four configurable shortcut plugins add shorter aliases and extra convenience
|
|
1210
|
+
methods. All four load together without conflicts:
|
|
1211
|
+
|
|
1212
|
+
```clpy
|
|
1213
|
+
# cruhon-shortcuts (base)
|
|
1214
|
+
@var[text; @read["notes.txt"]] # @read → @file.read
|
|
1215
|
+
@var[stamp; @now[]] # @now → @date.now
|
|
1216
|
+
@var[id; @uuid[]] # @uuid → @crypto.uuid
|
|
1217
|
+
|
|
1218
|
+
# cruhon-shortcuts-pro (math / lists / dicts / text / logic)
|
|
1219
|
+
@var[x; @clamp[value; 0; 100]]
|
|
1220
|
+
@var[slug; @snake_case["Hello World"]]
|
|
1221
|
+
@var[g; @group_by[items; key_fn]]
|
|
1222
|
+
|
|
1223
|
+
# cruhon-shortcuts-data (xml / toml / diff / decimal / re / yaml / image / pdf)
|
|
1224
|
+
@var[cfg; @toml_load["port = 8080"]]
|
|
1225
|
+
@var[amt; @money["3.14159"]] # decimal.money → 2dp
|
|
1226
|
+
@var[ok; @is_private_ip["10.0.0.1"]]
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
---
|
|
1230
|
+
|
|
1231
|
+
## Bundled Plugins
|
|
1232
|
+
|
|
1233
|
+
These ship in `mods/` and load automatically:
|
|
1234
|
+
|
|
1235
|
+
### `@db.*` — multi-backend database (cruhon-db)
|
|
1236
|
+
|
|
1237
|
+
SQLite, PostgreSQL, MySQL with full CRUD, transactions, and async — 170+
|
|
1238
|
+
commands. Also env-aware connection, migrations, seeding, and a live
|
|
1239
|
+
`@panel` bridge:
|
|
1240
|
+
|
|
1241
|
+
```clpy
|
|
1242
|
+
@db.connect_env["DATABASE_URL"] # DSN from the environment
|
|
1243
|
+
@db.migrate["./migrations"] # apply *.sql files once, in order
|
|
1244
|
+
@db.seed["users"; "fixtures/users.json"] # bulk-load JSON or CSV
|
|
1245
|
+
@var[safe; @db.dsn_safe[]]
|
|
1246
|
+
@print[Connected to {safe}] # DSN with the password masked
|
|
1247
|
+
|
|
1248
|
+
@panel.start[8787]
|
|
1249
|
+
@db.attach_panel[] # every query now streams to the panel
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
### `@panel.*` — live log-stream dashboard (cruhon-panel)
|
|
1253
|
+
|
|
1254
|
+
A zero-dependency web panel that streams logs, metrics, and events to the
|
|
1255
|
+
browser over Server-Sent Events — pure standard library.
|
|
1256
|
+
|
|
1257
|
+
```clpy
|
|
1258
|
+
@panel.start[8787] # → http://127.0.0.1:8787
|
|
1259
|
+
@panel.attach_logging["INFO"] # mirror all @log.* output to the panel
|
|
1260
|
+
@log.info["server warming up"] # …shows up live in the browser
|
|
1261
|
+
@panel.metric["users online"; 42] # update a metric tile
|
|
1262
|
+
@panel.event["deploy"; {"version": "2.10"}]
|
|
1263
|
+
@db.attach_panel[] # stream every SQL query too (needs @db)
|
|
1264
|
+
@panel.open[] # open the dashboard in a browser
|
|
1265
|
+
@panel.wait[] # keep the panel alive
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
### `@discord.*` — Discord bot toolkit (cruhon-discord)
|
|
1269
|
+
|
|
1270
|
+
~60 commands for building bots — see `mods/cruhon-discord`.
|
|
1271
|
+
|
|
1272
|
+
---
|
|
1273
|
+
|
|
1274
|
+
## Creating a New Project
|
|
1275
|
+
|
|
1276
|
+
```bash
|
|
1277
|
+
cruhon new myproject
|
|
1278
|
+
cd myproject
|
|
1279
|
+
cruhon run src/main.clpy
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
Creates:
|
|
1283
|
+
|
|
1284
|
+
```
|
|
1285
|
+
myproject/
|
|
1286
|
+
├── src/
|
|
1287
|
+
│ └── main.clpy
|
|
1288
|
+
└── mods/
|
|
1289
|
+
└── README.md
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
---
|
|
1293
|
+
|
|
1294
|
+
## Contributing
|
|
1295
|
+
|
|
1296
|
+
```bash
|
|
1297
|
+
git clone https://github.com/cruciblelab/cruhon
|
|
1298
|
+
cd cruhon
|
|
1299
|
+
pip install -e .
|
|
1300
|
+
python -m pytest cruhon/tests/
|
|
1301
|
+
cruhon run cruhon/examples/hello.clpy
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
---
|
|
1305
|
+
|
|
1306
|
+
## Contact
|
|
1307
|
+
|
|
1308
|
+
- **Discord:** [discord.gg/SPf5VZ6QPG](https://discord.gg/SPf5VZ6QPG)
|
|
1309
|
+
- **Email:** [cruciblelab@hotmail.com](mailto:cruciblelab@hotmail.com)
|
|
1310
|
+
- **GitHub:** [github.com/cruciblelab](https://github.com/cruciblelab)
|
|
1311
|
+
|
|
1312
|
+
---
|
|
1313
|
+
|
|
1314
|
+
## License
|
|
1315
|
+
|
|
1316
|
+
MIT — CrucibleLab
|