astpp 0.1.0__py3-none-any.whl
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.
- astpp-0.1.0.dist-info/METADATA +258 -0
- astpp-0.1.0.dist-info/RECORD +6 -0
- astpp-0.1.0.dist-info/WHEEL +4 -0
- astpp-0.1.0.dist-info/entry_points.txt +3 -0
- astpp-0.1.0.dist-info/licenses/LICENSE +21 -0
- astpp.py +637 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astpp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-Inlining Python Macro Preprocessor & Runtime Polyfill Library
|
|
5
|
+
Keywords: ast,preprocessor,micropython,pypy,macros,inlining,const
|
|
6
|
+
Author: f-fix
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
18
|
+
Classifier: Topic :: Software Development :: Pre-processors
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Project-URL: Homepage, https://github.com/f-fix/astpp
|
|
21
|
+
Project-URL: Issues, https://github.com/f-fix/astpp/issues
|
|
22
|
+
Project-URL: Repository, https://github.com/f-fix/astpp
|
|
23
|
+
|
|
24
|
+
# astpp
|
|
25
|
+
|
|
26
|
+
**Self-Inlining Python Macro Preprocessor & Runtime Polyfill Library**
|
|
27
|
+
|
|
28
|
+
Project Homepage: [https://github.com/f-fix/astpp](https://github.com/f-fix/astpp)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
`astpp` is a dual-use Python library and AST-level macro preprocessor designed for cross-runtime codebases targeting **MicroPython**, **PyPy**, and **CPython**:
|
|
35
|
+
|
|
36
|
+
1. **Runtime Polyfill (Zero-Build Development)**:
|
|
37
|
+
Importing `astpp` provides zero-overhead, runtime-compatible implementations of MicroPython compiler pragmas (`const`, `micropython.native`, `micropython.viper`) and conditional decorators (`keep`, `omit`, `keep_if`, `omit_if`). Scripts run directly under CPython, PyPy, or MicroPython without requiring an explicit compilation or preprocessing step.
|
|
38
|
+
|
|
39
|
+
2. **Compile-Time Preprocessor (Zero-Dependency Production Builds)**:
|
|
40
|
+
When invoked as a script or via `astpp_main()`, `astpp` inspects the consumer's Abstract Syntax Tree (AST), removes `from astpp import ...` statements completely, and inlines minimal self-contained fallback definitions copied directly from its own source code. The emitted code has **zero external runtime dependencies** on `astpp.py`.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Via `pip` (CPython / PyPy / Host PC)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install from PyPI (when published):
|
|
48
|
+
pip install astpp
|
|
49
|
+
|
|
50
|
+
# Or install directly from GitHub:
|
|
51
|
+
pip install git+https://github.com/f-fix/astpp.git
|
|
52
|
+
|
|
53
|
+
# Or install locally in editable development mode:
|
|
54
|
+
pip install -e .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Via `mip` (MicroPython)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# In MicroPython REPL (on Wi-Fi / Ethernet enabled board):
|
|
61
|
+
import mip
|
|
62
|
+
mip.install("github:f-fix/astpp")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Or from your host computer over USB/UART via mpremote:
|
|
67
|
+
mpremote mip install github:f-fix/astpp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Key Features
|
|
75
|
+
|
|
76
|
+
- **Macro Definitions (`-DNAME=VALUE`)**:
|
|
77
|
+
Rewrites target assignments `NAME = ...` to `NAME = const(VALUE)`. Inlined constants are propagated directly into expressions throughout the module.
|
|
78
|
+
- **Dead-Branch Elimination**:
|
|
79
|
+
Resolves `if`, `elif`, and `else` branches whose conditions evaluate to compile-time constants. Dead branches are pruned from the AST, while active branches are promoted or spliced without leaving ghost `pass` statements.
|
|
80
|
+
- **Symmetric Conditional Decorators (`@keep`, `@omit`, `@keep_if`, `@omit_if`)**:
|
|
81
|
+
- `@keep`: Preserves the definition and strips the decorator (zero runtime overhead).
|
|
82
|
+
- `@omit`: Drops the decorated function or class from the parse tree completely.
|
|
83
|
+
- `@keep_if(cond)` / `@omit_if(cond)`: Evaluates compile-time conditions and resolves symmetrically to `@keep` or `@omit`.
|
|
84
|
+
- **Compiler Optimization Parity (`-O`, `-OO`, `-O0`–`-O3`)**:
|
|
85
|
+
- **Default / `-O0`**: Preserves `__debug__` and assertions for downstream runtime evaluation.
|
|
86
|
+
- **`-O` / `-O1`**: Sets `__debug__ = False` and purges all `assert` statements.
|
|
87
|
+
- **`-OO` / `-O2`**: Purges assertions and strips docstrings from modules, classes, and functions.
|
|
88
|
+
- **`-O3`**: Full constant propagation, dead branch pruning, and inlining.
|
|
89
|
+
- **Self-Contained Testing (`--test`)**:
|
|
90
|
+
Executes built-in regression unit tests that parse and validate the documented examples directly from the module docstring.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Command-Line Usage
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Transform via stdin / stdout filter:
|
|
98
|
+
python3 astpp.py [-O[n]] [-DNAME=VAL ...] < input.py > output.py
|
|
99
|
+
|
|
100
|
+
# Transform using named input and output files:
|
|
101
|
+
python3 astpp.py -O3 -D_IS_DESKTOP_PY=False input.py -o output.py
|
|
102
|
+
|
|
103
|
+
# Display rich formatted help:
|
|
104
|
+
python3 astpp.py --help
|
|
105
|
+
|
|
106
|
+
# Run self-contained test suite:
|
|
107
|
+
python3 astpp.py --test
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### CLI Options
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
usage: astpp [-h] [-o OUTPUT] [-D NAME=VALUE] [-O [LEVEL]] [-OO] [--test]
|
|
114
|
+
[input]
|
|
115
|
+
|
|
116
|
+
Self-Inlining Macro Preprocessor & Runtime Polyfill Library.
|
|
117
|
+
|
|
118
|
+
positional arguments:
|
|
119
|
+
input Input Python source file or '-' for stdin (default: '-')
|
|
120
|
+
|
|
121
|
+
options:
|
|
122
|
+
-h, --help Show this help message and exit
|
|
123
|
+
-o OUTPUT, --output OUTPUT
|
|
124
|
+
Output destination file or '-' for stdout (default: '-')
|
|
125
|
+
-D NAME=VALUE Define macro replacement or override constant (e.g. -DNAME=VAL or -DNAME)
|
|
126
|
+
-O [LEVEL] Optimization level: -O (strip asserts, force __debug__=False),
|
|
127
|
+
-OO or -O2 (strip docstrings), -O3 (full)
|
|
128
|
+
-OO Optimization level 2: strip assertions, __debug__=False, and docstrings
|
|
129
|
+
--test Run self-contained unit test suite
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Programmatic API
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from astpp import astpp_main
|
|
138
|
+
import io
|
|
139
|
+
|
|
140
|
+
source = """
|
|
141
|
+
from astpp import const
|
|
142
|
+
BUFFER_SIZE = const(1024)
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
in_stream = io.StringIO(source)
|
|
146
|
+
out_stream = io.StringIO()
|
|
147
|
+
|
|
148
|
+
rc = astpp_main(["-O3", "-DBUFFER_SIZE=512"], in_stream, out_stream)
|
|
149
|
+
assert rc == 0
|
|
150
|
+
print(out_stream.getvalue())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Documented & Tested Transformation Examples
|
|
156
|
+
|
|
157
|
+
### Example 1: Import Inlining & Self-Parsing
|
|
158
|
+
**Input**:
|
|
159
|
+
```python
|
|
160
|
+
from astpp import const
|
|
161
|
+
X = const(10)
|
|
162
|
+
```
|
|
163
|
+
**Flags**: `-O3`
|
|
164
|
+
**Output**:
|
|
165
|
+
```python
|
|
166
|
+
try:
|
|
167
|
+
from micropython import const
|
|
168
|
+
except ImportError:
|
|
169
|
+
|
|
170
|
+
def const(x):
|
|
171
|
+
return x
|
|
172
|
+
X = const(10)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Example 2: Constant Definition & In-Line Override
|
|
176
|
+
**Input**:
|
|
177
|
+
```python
|
|
178
|
+
_X = const(True)
|
|
179
|
+
BUFFER_SIZE = const(1024)
|
|
180
|
+
```
|
|
181
|
+
**Flags**: `-DBUFFER_SIZE=512 -O`
|
|
182
|
+
**Output**:
|
|
183
|
+
```python
|
|
184
|
+
_X = const(True)
|
|
185
|
+
BUFFER_SIZE = const(512)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Example 3: Conditional Decorator Pruning
|
|
189
|
+
**Input**:
|
|
190
|
+
```python
|
|
191
|
+
@keep_if(_IS_DESKTOP_PY)
|
|
192
|
+
def desktop_helper():
|
|
193
|
+
return 42
|
|
194
|
+
```
|
|
195
|
+
**Flags**: `-D_IS_DESKTOP_PY=False`
|
|
196
|
+
**Output**: *(Empty — function deleted from AST)*
|
|
197
|
+
|
|
198
|
+
### Example 4: Assertions and Debug Block Stripping
|
|
199
|
+
**Input**:
|
|
200
|
+
```python
|
|
201
|
+
assert 1 == 1, 'invariant'
|
|
202
|
+
if __debug__:
|
|
203
|
+
def debug_only():
|
|
204
|
+
pass
|
|
205
|
+
```
|
|
206
|
+
**Flags**: `-O`
|
|
207
|
+
**Output**: *(Empty — assertions and debug blocks pruned)*
|
|
208
|
+
|
|
209
|
+
### Example 5: Native Decorator Stripping via Macro Mapping
|
|
210
|
+
**Input**:
|
|
211
|
+
```python
|
|
212
|
+
@micropython.native
|
|
213
|
+
def fast_calc(x):
|
|
214
|
+
return x * 2
|
|
215
|
+
```
|
|
216
|
+
**Flags**: `-Dmicropython.native=keep`
|
|
217
|
+
**Output**:
|
|
218
|
+
```python
|
|
219
|
+
def fast_calc(x):
|
|
220
|
+
return x * 2
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Function & Runtime Reference
|
|
226
|
+
|
|
227
|
+
### `const(x)`
|
|
228
|
+
Identity fallback for compile-time constants on non-MicroPython runtimes. On MicroPython, `const()` is intercepted natively at parse time to inline values. On CPython and PyPy, this function acts as an identity pass-through returning `x`.
|
|
229
|
+
|
|
230
|
+
### `keep(obj)`
|
|
231
|
+
Identity decorator: keeps the decorated function or class as-is. When processed by `astpp`, the `@keep` decorator is stripped from the AST so that the compiled output incurs zero runtime decorator invocation overhead.
|
|
232
|
+
|
|
233
|
+
### `omit(obj)`
|
|
234
|
+
Omission decorator: drops the decorated function or class completely. When processed by `astpp`, any node decorated with `@omit` is purged from the parse tree.
|
|
235
|
+
|
|
236
|
+
### `keep_if(cond)`
|
|
237
|
+
Conditional decorator factory that preserves the target definition if `cond` is True. Returns the identity decorator `keep` if `cond` is True, or `omit` if False.
|
|
238
|
+
|
|
239
|
+
### `omit_if(cond)`
|
|
240
|
+
Conditional decorator factory that drops the target definition if `cond` is True. Returns the omission decorator `omit` if `cond` is True, or `keep` if False.
|
|
241
|
+
|
|
242
|
+
### `_MicroPythonShim`
|
|
243
|
+
Desktop compatibility shim providing no-op decorators (`native = staticmethod(keep)`, `viper = staticmethod(keep)`) for MicroPython compiler pragmas on non-MicroPython runtimes.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT License. See `LICENSE` for details.
|
|
250
|
+
|
|
251
|
+
## Note on the code and the tools used to write it
|
|
252
|
+
|
|
253
|
+
Parts of this code were written (including some initial ones that began in other, separate projects) with assistance from LLM-integrated coding tools. If you don't like it, feel free to use other software or rewrite parts you dislike. PRs are welcome!
|
|
254
|
+
|
|
255
|
+
### How did I end up using those? Don't I dislike slop?
|
|
256
|
+
|
|
257
|
+
Yes, I hate slop. This project began because I wanted tape image conversion tools where the conversion steps were all clearly documented and readable code, but which also performed well enough in terms of accuracy to actually be the tool I use. I started out writing the tools myself, but my manual attempts hadn't yielded comparable accuracy to existing closed-source tools for some steps, so I started using the tools to help find the bugs and suggest improvements, and IMO the result is now good enough to actually be useful in some scenarios. In terms of slop, the tool-generated code doesn't closely resemble any existing solutions I have found. Rather it's a fairly passable translation of my requests into Python.
|
|
258
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
astpp.py,sha256=TAf-PH9N2m3zIK4JYw7S9780fxzbe-q3cgpf3n8PKJI,21781
|
|
2
|
+
astpp-0.1.0.dist-info/entry_points.txt,sha256=idXuWsk8gclhTlS-mYBeJm_jD5yin0XDduRGizVVdgw,36
|
|
3
|
+
astpp-0.1.0.dist-info/licenses/LICENSE,sha256=EPqk9kB5Zgygl1BXt-KJzSPRam4GcjyrEJPZw_mvMac,1062
|
|
4
|
+
astpp-0.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
5
|
+
astpp-0.1.0.dist-info/METADATA,sha256=_4uXHcsQpkaW5d0uHDCY3fGmRyk71FnlihbxjxfgXw0,9400
|
|
6
|
+
astpp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 -ffix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
astpp.py
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Self-Inlining Python Macro Preprocessor & Runtime Polyfill Library.
|
|
3
|
+
|
|
4
|
+
Homepage:
|
|
5
|
+
---------
|
|
6
|
+
https://github.com/f-fix/astpp
|
|
7
|
+
|
|
8
|
+
Overview:
|
|
9
|
+
---------
|
|
10
|
+
`astpp` is a dual-use Python library and AST-level macro preprocessor designed
|
|
11
|
+
for cross-runtime codebases that target MicroPython, PyPy, and CPython.
|
|
12
|
+
|
|
13
|
+
1. Runtime Polyfill (Zero-Build Development):
|
|
14
|
+
Importing `astpp` provides zero-overhead, runtime-compatible implementations of
|
|
15
|
+
MicroPython compiler pragmas (`const`, `micropython.native`, `micropython.viper`)
|
|
16
|
+
and conditional decorators (`keep`, `omit`, `keep_if`, `omit_if`). Scripts can be
|
|
17
|
+
executed directly under CPython, PyPy, or MicroPython without requiring an explicit
|
|
18
|
+
build or preprocessing step.
|
|
19
|
+
|
|
20
|
+
2. Compile-Time Preprocessor (Zero-Dependency Builds):
|
|
21
|
+
When invoked as a script or via `astpp_main()`, `astpp` inspects the consumer's
|
|
22
|
+
Abstract Syntax Tree (AST), removes the `from astpp import ...` statement entirely,
|
|
23
|
+
and inlines minimal self-contained fallback definitions copied directly from its own
|
|
24
|
+
source code. The emitted code has zero external dependencies on `astpp.py`.
|
|
25
|
+
|
|
26
|
+
Documentation & Tested Examples:
|
|
27
|
+
--------------------------------
|
|
28
|
+
The following verified examples illustrate source transformations across various CLI flags.
|
|
29
|
+
These exact inputs, flags, and outputs are parsed directly from this docstring and validated
|
|
30
|
+
by `astpp_main(["--test"])`:
|
|
31
|
+
|
|
32
|
+
### Example 1: Import Inlining
|
|
33
|
+
Input:
|
|
34
|
+
```python
|
|
35
|
+
from astpp import const
|
|
36
|
+
X = const(10)
|
|
37
|
+
```
|
|
38
|
+
Flags: -O3
|
|
39
|
+
Output:
|
|
40
|
+
```python
|
|
41
|
+
try:
|
|
42
|
+
from micropython import const
|
|
43
|
+
except ImportError:
|
|
44
|
+
|
|
45
|
+
def const(x):
|
|
46
|
+
return x
|
|
47
|
+
X = const(10)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Example 2: Constant Definition & In-Line Override
|
|
51
|
+
Input:
|
|
52
|
+
```python
|
|
53
|
+
_X = const(True)
|
|
54
|
+
BUFFER_SIZE = const(1024)
|
|
55
|
+
```
|
|
56
|
+
Flags: -DBUFFER_SIZE=512 -O
|
|
57
|
+
Output:
|
|
58
|
+
```python
|
|
59
|
+
_X = const(True)
|
|
60
|
+
BUFFER_SIZE = const(512)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Example 3: Conditional Decorator Pruning
|
|
64
|
+
Input:
|
|
65
|
+
```python
|
|
66
|
+
@keep_if(_IS_DESKTOP_PY)
|
|
67
|
+
def desktop_helper():
|
|
68
|
+
return 42
|
|
69
|
+
```
|
|
70
|
+
Flags: -D_IS_DESKTOP_PY=False
|
|
71
|
+
Output:
|
|
72
|
+
```python
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Example 4: Assertions and Debug Block Stripping
|
|
76
|
+
Input:
|
|
77
|
+
```python
|
|
78
|
+
assert 1 == 1, 'invariant'
|
|
79
|
+
if __debug__:
|
|
80
|
+
def debug_only():
|
|
81
|
+
pass
|
|
82
|
+
```
|
|
83
|
+
Flags: -O
|
|
84
|
+
Output:
|
|
85
|
+
```python
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Example 5: Native Decorator Stripping via Macro Mapping
|
|
89
|
+
Input:
|
|
90
|
+
```python
|
|
91
|
+
@micropython.native
|
|
92
|
+
def fast_calc(x):
|
|
93
|
+
return x * 2
|
|
94
|
+
```
|
|
95
|
+
Flags: -Dmicropython.native=keep
|
|
96
|
+
Output:
|
|
97
|
+
```python
|
|
98
|
+
def fast_calc(x):
|
|
99
|
+
return x * 2
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
CLI Usage:
|
|
103
|
+
----------
|
|
104
|
+
python3 astpp.py [-O[n]] [-DNAME=VAL ...] [input] [-o output]
|
|
105
|
+
python3 astpp.py --help
|
|
106
|
+
python3 astpp.py --test
|
|
107
|
+
|
|
108
|
+
API Usage:
|
|
109
|
+
----------
|
|
110
|
+
from astpp import astpp_main
|
|
111
|
+
astpp_main(argv=['-O3', '-D_IS_DESKTOP_PY=False'], stdin=in_stream, stdout=out_stream)
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# ==============================================================================
|
|
115
|
+
# 1. RUNTIME EXPORTS
|
|
116
|
+
# ==============================================================================
|
|
117
|
+
try:
|
|
118
|
+
from micropython import const
|
|
119
|
+
except ImportError:
|
|
120
|
+
|
|
121
|
+
def const(x):
|
|
122
|
+
"""Identity fallback for compile-time constants on non-MicroPython runtimes.
|
|
123
|
+
|
|
124
|
+
On MicroPython, `const()` is intercepted natively at parse time to inline values.
|
|
125
|
+
On CPython and PyPy, this function acts as an identity pass-through returning `x`.
|
|
126
|
+
"""
|
|
127
|
+
return x
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def keep(obj):
|
|
131
|
+
"""Identity decorator: keeps the decorated function or class as-is.
|
|
132
|
+
|
|
133
|
+
When processed by `astpp`, the `@keep` decorator is stripped from the AST so that the
|
|
134
|
+
compiled output incurs zero runtime decorator invocation overhead.
|
|
135
|
+
"""
|
|
136
|
+
return obj
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def omit(obj):
|
|
140
|
+
"""Omission decorator: drops the decorated function or class completely.
|
|
141
|
+
|
|
142
|
+
When processed by `astpp`, any node decorated with `@omit` is purged from the parse tree.
|
|
143
|
+
"""
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def keep_if(cond):
|
|
148
|
+
"""Conditional decorator factory that preserves the target definition if cond is True.
|
|
149
|
+
|
|
150
|
+
Parameters:
|
|
151
|
+
cond: Expression evaluating to a truthy or falsy value.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The identity decorator `keep` if cond is True, or `omit` if False.
|
|
155
|
+
When processed by `astpp`, the decorator is stripped if kept, or the entire node is
|
|
156
|
+
deleted from the AST if omitted.
|
|
157
|
+
"""
|
|
158
|
+
return keep if cond else omit
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def omit_if(cond):
|
|
162
|
+
"""Conditional decorator factory that drops the target definition if cond is True.
|
|
163
|
+
|
|
164
|
+
Parameters:
|
|
165
|
+
cond: Expression evaluating to a truthy or falsy value.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The omission decorator `omit` if cond is True, or `keep` if False.
|
|
169
|
+
When processed by `astpp`, the entire target node is pruned from the AST if omitted.
|
|
170
|
+
"""
|
|
171
|
+
return omit if cond else keep
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
import micropython as _real_mpy
|
|
176
|
+
except ImportError:
|
|
177
|
+
_real_mpy = None
|
|
178
|
+
|
|
179
|
+
if _real_mpy is not None:
|
|
180
|
+
micropython = _real_mpy
|
|
181
|
+
else:
|
|
182
|
+
|
|
183
|
+
class _MicroPythonShim:
|
|
184
|
+
"""Desktop compatibility shim providing no-op decorators for MicroPython pragmas."""
|
|
185
|
+
|
|
186
|
+
native = staticmethod(keep)
|
|
187
|
+
viper = staticmethod(keep)
|
|
188
|
+
|
|
189
|
+
micropython = _MicroPythonShim()
|
|
190
|
+
|
|
191
|
+
__version__ = "0.1.0"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ==============================================================================
|
|
195
|
+
# 2. PREPROCESSOR ENTRYPOINT & SELF-TEST SUITE
|
|
196
|
+
# ==============================================================================
|
|
197
|
+
def astpp_main(argv=None, stdin=None, stdout=None, stderr=None):
|
|
198
|
+
"""Programmatic entrypoint for AST-level preprocessing, help display, and self-testing.
|
|
199
|
+
|
|
200
|
+
Parameters:
|
|
201
|
+
argv: List of command-line argument strings (e.g. ['-O3', '-DFOO=1']).
|
|
202
|
+
Defaults to sys.argv[1:] if None.
|
|
203
|
+
stdin: Readable text stream containing Python source code. Defaults to sys.stdin.
|
|
204
|
+
stdout: Writable text stream to receive transformed source code. Defaults to sys.stdout.
|
|
205
|
+
stderr: Writable text stream for diagnostic logging and error messages. Defaults to sys.stderr.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
0 on successful transformation, help output, or passing test suite; non-zero on error.
|
|
209
|
+
"""
|
|
210
|
+
import ast
|
|
211
|
+
import copy
|
|
212
|
+
import argparse
|
|
213
|
+
|
|
214
|
+
if argv is None or stdin is None or stdout is None or stderr is None:
|
|
215
|
+
import sys
|
|
216
|
+
|
|
217
|
+
if argv is None:
|
|
218
|
+
argv = sys.argv[1:]
|
|
219
|
+
if stdin is None:
|
|
220
|
+
stdin = sys.stdin
|
|
221
|
+
if stdout is None:
|
|
222
|
+
stdout = sys.stdout
|
|
223
|
+
if stderr is None:
|
|
224
|
+
stderr = sys.stderr
|
|
225
|
+
|
|
226
|
+
# --- Argument Parser & Normal Filter Mode ---
|
|
227
|
+
def _build_parser():
|
|
228
|
+
parser = argparse.ArgumentParser(
|
|
229
|
+
prog="astpp",
|
|
230
|
+
description="Self-Inlining Macro Preprocessor & Runtime Polyfill Library.",
|
|
231
|
+
add_help=False,
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"-h", "--help", action="store_true", help="Show this help message and exit"
|
|
235
|
+
)
|
|
236
|
+
parser.add_argument(
|
|
237
|
+
"input",
|
|
238
|
+
nargs="?",
|
|
239
|
+
default="-",
|
|
240
|
+
help="Input Python source file or '-' for stdin (default: '-')",
|
|
241
|
+
)
|
|
242
|
+
parser.add_argument(
|
|
243
|
+
"-o",
|
|
244
|
+
"--output",
|
|
245
|
+
default="-",
|
|
246
|
+
help="Output destination file or '-' for stdout (default: '-')",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"-D",
|
|
250
|
+
dest="defines",
|
|
251
|
+
action="append",
|
|
252
|
+
default=[],
|
|
253
|
+
metavar="NAME=VALUE",
|
|
254
|
+
help="Define macro replacement or override constant (e.g. -DNAME=VAL or -DNAME)",
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"-O",
|
|
258
|
+
dest="opt",
|
|
259
|
+
nargs="?",
|
|
260
|
+
const="1",
|
|
261
|
+
default=None,
|
|
262
|
+
metavar="LEVEL",
|
|
263
|
+
help="Optimization level: -O (strip asserts, force __debug__=False), -OO or -O2 (strip docstrings), -O3 (full)",
|
|
264
|
+
)
|
|
265
|
+
parser.add_argument(
|
|
266
|
+
"-OO",
|
|
267
|
+
dest="opt_oo",
|
|
268
|
+
action="store_true",
|
|
269
|
+
help="Optimization level 2: strip assertions, __debug__=False, and docstrings",
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"--test",
|
|
273
|
+
action="store_true",
|
|
274
|
+
help="Run self-contained unit test suite",
|
|
275
|
+
)
|
|
276
|
+
return parser
|
|
277
|
+
|
|
278
|
+
parser = _build_parser()
|
|
279
|
+
|
|
280
|
+
if "-h" in argv or "--help" in argv:
|
|
281
|
+
parser.print_help(file=stdout)
|
|
282
|
+
if hasattr(stdout, "flush"):
|
|
283
|
+
stdout.flush()
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
args = parser.parse_args(argv)
|
|
288
|
+
except SystemExit as e:
|
|
289
|
+
c = getattr(e, "code", None)
|
|
290
|
+
if c is None:
|
|
291
|
+
c = e.args[0] if e.args else 0
|
|
292
|
+
return c
|
|
293
|
+
|
|
294
|
+
# --- Self-Contained Test Mode ---
|
|
295
|
+
if args.test:
|
|
296
|
+
if not __debug__:
|
|
297
|
+
stderr.write("--test is not available in optimized builds\n")
|
|
298
|
+
if hasattr(stderr, "flush"):
|
|
299
|
+
stderr.flush()
|
|
300
|
+
return 1
|
|
301
|
+
try:
|
|
302
|
+
import unittest
|
|
303
|
+
except ImportError:
|
|
304
|
+
stderr.write(
|
|
305
|
+
"--test disabled: unittest is not available in this environment\n"
|
|
306
|
+
)
|
|
307
|
+
return 1
|
|
308
|
+
|
|
309
|
+
global TestAstpp
|
|
310
|
+
|
|
311
|
+
class TestAstpp(unittest.TestCase):
|
|
312
|
+
"""Self-test suite for astpp runtime helpers, inlining, and docstring examples."""
|
|
313
|
+
|
|
314
|
+
def test_runtime_helpers(self):
|
|
315
|
+
"""Validate behavior of const, keep, omit, keep_if, omit_if, and micropython shims."""
|
|
316
|
+
self.assertEqual(const(123), 123)
|
|
317
|
+
self.assertEqual(keep(42), 42)
|
|
318
|
+
self.assertIsNone(omit(42))
|
|
319
|
+
self.assertEqual(keep_if(True)(99), 99)
|
|
320
|
+
self.assertIsNone(keep_if(False)(99))
|
|
321
|
+
self.assertIsNone(omit_if(True)(99))
|
|
322
|
+
self.assertEqual(omit_if(False)(99), 99)
|
|
323
|
+
self.assertEqual(micropython.native(10), 10)
|
|
324
|
+
self.assertEqual(micropython.viper(20), 20)
|
|
325
|
+
|
|
326
|
+
def test_docstring_examples(self):
|
|
327
|
+
"""Parse all documented examples directly from __doc__ and verify against astpp_main."""
|
|
328
|
+
import io, re, shlex, sys
|
|
329
|
+
|
|
330
|
+
doc = sys.modules[__name__].__doc__ or __doc__
|
|
331
|
+
pattern = re.compile(
|
|
332
|
+
r"Input:\s*```python\s*(.*?)\s*```\s*Flags:\s*(.*?)\s*Output:\s*```python\s*(.*?)\s*```",
|
|
333
|
+
re.DOTALL,
|
|
334
|
+
)
|
|
335
|
+
matches = pattern.findall(doc)
|
|
336
|
+
self.assertGreaterEqual(
|
|
337
|
+
len(matches), 5, "Must find at least 5 docstring examples"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
for idx, (inp, flags_str, expected) in enumerate(matches):
|
|
341
|
+
flags = shlex.split(flags_str.strip())
|
|
342
|
+
fake_in = io.StringIO(inp.strip() + "\n")
|
|
343
|
+
fake_out = io.StringIO()
|
|
344
|
+
fake_err = io.StringIO()
|
|
345
|
+
rc = astpp_main(flags, fake_in, fake_out, fake_err)
|
|
346
|
+
self.assertEqual(
|
|
347
|
+
rc, 0, f"Example {idx+1} failed with rc={rc}: {flags}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
actual_ast = ast.parse(fake_out.getvalue().strip())
|
|
351
|
+
exp_ast = ast.parse(expected.strip())
|
|
352
|
+
self.assertEqual(
|
|
353
|
+
ast.dump(actual_ast),
|
|
354
|
+
ast.dump(exp_ast),
|
|
355
|
+
f"Example {idx+1} AST mismatch!\nActual:\n{fake_out.getvalue()}\nExpected:\n{expected}",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def test_cli_help_flag(self):
|
|
359
|
+
"""Validate that --help writes formatted usage to stdout without process exit."""
|
|
360
|
+
import io, re
|
|
361
|
+
|
|
362
|
+
help_out = io.StringIO()
|
|
363
|
+
rc = astpp_main(["--help"], stdout=help_out, stderr=io.StringIO())
|
|
364
|
+
self.assertEqual(rc, 0)
|
|
365
|
+
val = help_out.getvalue()
|
|
366
|
+
clean_val = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", val)
|
|
367
|
+
self.assertIn("usage: astpp", clean_val)
|
|
368
|
+
self.assertIn("-D NAME=VALUE", clean_val)
|
|
369
|
+
|
|
370
|
+
loader = getattr(unittest, "TestLoader", None)
|
|
371
|
+
if loader is not None:
|
|
372
|
+
suite = loader().loadTestsFromTestCase(TestAstpp)
|
|
373
|
+
runner = unittest.TextTestRunner(stream=stderr, verbosity=2)
|
|
374
|
+
else:
|
|
375
|
+
suite = unittest.TestSuite()
|
|
376
|
+
suite.addTest(TestAstpp)
|
|
377
|
+
runner = unittest.TextTestRunner()
|
|
378
|
+
res = runner.run(suite)
|
|
379
|
+
return (
|
|
380
|
+
0
|
|
381
|
+
if (
|
|
382
|
+
res.wasSuccessful()
|
|
383
|
+
if hasattr(res, "wasSuccessful")
|
|
384
|
+
else len(getattr(res, "errors", [])) == 0
|
|
385
|
+
)
|
|
386
|
+
else 1
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def _parse_val(v_str):
|
|
390
|
+
if v_str == "True":
|
|
391
|
+
return True
|
|
392
|
+
if v_str == "False":
|
|
393
|
+
return False
|
|
394
|
+
if v_str == "None":
|
|
395
|
+
return None
|
|
396
|
+
try:
|
|
397
|
+
return int(v_str)
|
|
398
|
+
except ValueError:
|
|
399
|
+
return v_str
|
|
400
|
+
|
|
401
|
+
defines = {}
|
|
402
|
+
for item in args.defines:
|
|
403
|
+
if "=" in item:
|
|
404
|
+
k, v = item.split("=", 1)
|
|
405
|
+
defines[k] = _parse_val(v)
|
|
406
|
+
else:
|
|
407
|
+
defines[item] = True
|
|
408
|
+
|
|
409
|
+
opt_level = None
|
|
410
|
+
if args.opt_oo:
|
|
411
|
+
opt_level = 2
|
|
412
|
+
elif args.opt is not None:
|
|
413
|
+
opt_level = int(args.opt)
|
|
414
|
+
|
|
415
|
+
if opt_level and opt_level >= 1:
|
|
416
|
+
defines["__debug__"] = False
|
|
417
|
+
|
|
418
|
+
if args.input == "-":
|
|
419
|
+
in_source = stdin.read()
|
|
420
|
+
else:
|
|
421
|
+
with open(args.input, "r", encoding="utf-8") as f:
|
|
422
|
+
in_source = f.read()
|
|
423
|
+
|
|
424
|
+
# Self-parsing: extract live AST nodes preceding astpp_main directly from this source file
|
|
425
|
+
with open(__file__, "r", encoding="utf-8") as f:
|
|
426
|
+
self_tree = ast.parse(f.read(), filename=__file__)
|
|
427
|
+
|
|
428
|
+
self_helpers = []
|
|
429
|
+
for n in self_tree.body:
|
|
430
|
+
if isinstance(n, ast.FunctionDef) and n.name == "astpp_main":
|
|
431
|
+
break
|
|
432
|
+
self_helpers.append(n)
|
|
433
|
+
|
|
434
|
+
def _defined_names(node):
|
|
435
|
+
defs = set()
|
|
436
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
437
|
+
defs.add(node.name)
|
|
438
|
+
elif isinstance(node, ast.Assign):
|
|
439
|
+
for t in node.targets:
|
|
440
|
+
if isinstance(t, ast.Name):
|
|
441
|
+
defs.add(t.id)
|
|
442
|
+
elif isinstance(node, ast.Try):
|
|
443
|
+
for n in node.body:
|
|
444
|
+
defs.update(_defined_names(n))
|
|
445
|
+
for h in node.handlers:
|
|
446
|
+
for n in h.body:
|
|
447
|
+
defs.update(_defined_names(n))
|
|
448
|
+
elif isinstance(node, ast.If):
|
|
449
|
+
for n in node.body + node.orelse:
|
|
450
|
+
defs.update(_defined_names(n))
|
|
451
|
+
return defs
|
|
452
|
+
|
|
453
|
+
class Preprocessor(ast.NodeTransformer):
|
|
454
|
+
"""AST transformer enforcing constant propagation, branch pruning, and inlining."""
|
|
455
|
+
|
|
456
|
+
def __init__(self):
|
|
457
|
+
self.constants = dict(defines)
|
|
458
|
+
|
|
459
|
+
def _eval_const(self, node):
|
|
460
|
+
"""Evaluate constant expressions statically at compile time."""
|
|
461
|
+
if isinstance(node, ast.Constant):
|
|
462
|
+
return node.value
|
|
463
|
+
if isinstance(node, ast.Name):
|
|
464
|
+
return self.constants.get(node.id, None)
|
|
465
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
466
|
+
v = self._eval_const(node.operand)
|
|
467
|
+
return (not v) if v is not None else None
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
def _strip_docstrings(self, body):
|
|
471
|
+
"""Strip docstring expression nodes when optimization level >= 2 (-OO / -O2)."""
|
|
472
|
+
if opt_level and opt_level >= 2 and body:
|
|
473
|
+
if (
|
|
474
|
+
isinstance(body[0], ast.Expr)
|
|
475
|
+
and isinstance(body[0].value, ast.Constant)
|
|
476
|
+
and isinstance(body[0].value.value, str)
|
|
477
|
+
):
|
|
478
|
+
body.pop(0)
|
|
479
|
+
if not body:
|
|
480
|
+
body.append(ast.Pass())
|
|
481
|
+
|
|
482
|
+
def visit_Module(self, node):
|
|
483
|
+
"""Process top-level module statements, stripping astpp imports and inlining helpers."""
|
|
484
|
+
self._strip_docstrings(node.body)
|
|
485
|
+
new_body = []
|
|
486
|
+
for stmt in node.body:
|
|
487
|
+
if (
|
|
488
|
+
isinstance(stmt, ast.ImportFrom)
|
|
489
|
+
and stmt.module in ("astpp", "pypp")
|
|
490
|
+
and not stmt.level
|
|
491
|
+
):
|
|
492
|
+
names = {a.name for a in stmt.names}
|
|
493
|
+
needed = set(names)
|
|
494
|
+
if any(k in needed for k in ("keep_if", "omit_if")):
|
|
495
|
+
needed.update(["keep", "omit"])
|
|
496
|
+
if "micropython" in needed:
|
|
497
|
+
needed.update(["keep", "_real_mpy", "_MicroPythonShim"])
|
|
498
|
+
|
|
499
|
+
for helper in self_helpers:
|
|
500
|
+
h_defs = _defined_names(helper)
|
|
501
|
+
if needed.intersection(h_defs):
|
|
502
|
+
new_body.append(self.visit(copy.deepcopy(helper)))
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
transformed = self.visit(stmt)
|
|
506
|
+
if transformed is not None:
|
|
507
|
+
if isinstance(transformed, list):
|
|
508
|
+
new_body.extend(transformed)
|
|
509
|
+
else:
|
|
510
|
+
new_body.append(transformed)
|
|
511
|
+
node.body = new_body
|
|
512
|
+
return node
|
|
513
|
+
|
|
514
|
+
def visit_Assert(self, node):
|
|
515
|
+
"""Eliminate assert statements when optimization level >= 1 (-O / -O1)."""
|
|
516
|
+
return None if (opt_level and opt_level >= 1) else node
|
|
517
|
+
|
|
518
|
+
def visit_Assign(self, node):
|
|
519
|
+
"""Harvest in-code const() definitions and apply -D constant overrides."""
|
|
520
|
+
for target in node.targets:
|
|
521
|
+
if isinstance(target, ast.Name):
|
|
522
|
+
name = target.id
|
|
523
|
+
if name in defines:
|
|
524
|
+
val = defines[name]
|
|
525
|
+
self.constants[name] = val
|
|
526
|
+
node.value = ast.Call(
|
|
527
|
+
func=ast.Name(id="const", ctx=ast.Load()),
|
|
528
|
+
args=[ast.Constant(value=val)],
|
|
529
|
+
keywords=[],
|
|
530
|
+
)
|
|
531
|
+
elif (
|
|
532
|
+
isinstance(node.value, ast.Call)
|
|
533
|
+
and getattr(node.value.func, "id", None) == "const"
|
|
534
|
+
):
|
|
535
|
+
val = self._eval_const(node.value.args[0])
|
|
536
|
+
if val is not None:
|
|
537
|
+
self.constants[name] = val
|
|
538
|
+
return self.generic_visit(node)
|
|
539
|
+
|
|
540
|
+
def visit_Name(self, node):
|
|
541
|
+
"""Inline known compile-time constants directly into load expressions."""
|
|
542
|
+
if (
|
|
543
|
+
opt_level
|
|
544
|
+
and opt_level >= 1
|
|
545
|
+
and isinstance(node.ctx, ast.Load)
|
|
546
|
+
and node.id in self.constants
|
|
547
|
+
):
|
|
548
|
+
val = self.constants[node.id]
|
|
549
|
+
if isinstance(val, (bool, int, str, bytes, type(None))):
|
|
550
|
+
return ast.Constant(value=val)
|
|
551
|
+
return node
|
|
552
|
+
|
|
553
|
+
def visit_If(self, node):
|
|
554
|
+
"""Eliminate dead branches and promote reachable bodies for compile-time if/elif/else."""
|
|
555
|
+
self.generic_visit(node)
|
|
556
|
+
cond = self._eval_const(node.test)
|
|
557
|
+
if cond is True:
|
|
558
|
+
node.orelse = []
|
|
559
|
+
return node.body
|
|
560
|
+
if cond is False:
|
|
561
|
+
if not node.orelse:
|
|
562
|
+
return None
|
|
563
|
+
if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If):
|
|
564
|
+
return node.orelse[0]
|
|
565
|
+
return node.orelse
|
|
566
|
+
if (
|
|
567
|
+
len(node.orelse) == 1
|
|
568
|
+
and isinstance(node.orelse[0], ast.If)
|
|
569
|
+
and self._eval_const(node.orelse[0].test) is False
|
|
570
|
+
):
|
|
571
|
+
node.orelse = node.orelse[0].orelse
|
|
572
|
+
return node
|
|
573
|
+
|
|
574
|
+
def _handle_def(self, node):
|
|
575
|
+
"""Handle conditional decorators (@keep, @omit, @keep_if, @omit_if, macro mappings)."""
|
|
576
|
+
self._strip_docstrings(node.body)
|
|
577
|
+
new_decs = []
|
|
578
|
+
for d in node.decorator_list:
|
|
579
|
+
dec_str = ast.unparse(d)
|
|
580
|
+
if isinstance(d, ast.Call) and getattr(d.func, "id", None) in (
|
|
581
|
+
"keep_if",
|
|
582
|
+
"omit_if",
|
|
583
|
+
):
|
|
584
|
+
cond = self._eval_const(d.args[0])
|
|
585
|
+
if d.func.id == "omit_if":
|
|
586
|
+
cond = not cond
|
|
587
|
+
res = "keep" if cond else "omit"
|
|
588
|
+
elif dec_str in ("keep", "omit"):
|
|
589
|
+
res = dec_str
|
|
590
|
+
else:
|
|
591
|
+
res = defines.get(dec_str, dec_str)
|
|
592
|
+
|
|
593
|
+
if res == "omit":
|
|
594
|
+
return None
|
|
595
|
+
if res == "keep":
|
|
596
|
+
continue
|
|
597
|
+
new_decs.append(d)
|
|
598
|
+
node.decorator_list = new_decs
|
|
599
|
+
return self.generic_visit(node)
|
|
600
|
+
|
|
601
|
+
def visit_FunctionDef(self, node):
|
|
602
|
+
"""Process function definitions, handling decorators and docstring stripping."""
|
|
603
|
+
return self._handle_def(node)
|
|
604
|
+
|
|
605
|
+
def visit_AsyncFunctionDef(self, node):
|
|
606
|
+
"""Process async function definitions, handling decorators and docstring stripping."""
|
|
607
|
+
return self._handle_def(node)
|
|
608
|
+
|
|
609
|
+
def visit_ClassDef(self, node):
|
|
610
|
+
"""Process class definitions, handling decorators and docstring stripping."""
|
|
611
|
+
return self._handle_def(node)
|
|
612
|
+
|
|
613
|
+
tree = ast.parse(in_source)
|
|
614
|
+
out_text = ast.unparse(Preprocessor().visit(tree))
|
|
615
|
+
if not out_text.endswith("\n"):
|
|
616
|
+
out_text += "\n"
|
|
617
|
+
|
|
618
|
+
if args.output == "-":
|
|
619
|
+
stdout.write(out_text)
|
|
620
|
+
if hasattr(stdout, "flush"):
|
|
621
|
+
stdout.flush()
|
|
622
|
+
else:
|
|
623
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
624
|
+
f.write(out_text)
|
|
625
|
+
|
|
626
|
+
return 0
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def main():
|
|
630
|
+
"""Command-line entrypoint for pip-installed astpp console script."""
|
|
631
|
+
import sys
|
|
632
|
+
|
|
633
|
+
sys.exit(astpp_main(sys.argv[1:], sys.stdin, sys.stdout, sys.stderr))
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
if __name__ == "__main__":
|
|
637
|
+
main()
|