Barmaja-PyArduino 2.0.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.
- Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
- Barmaja_PyArduino/__init__.py +528 -0
- Barmaja_PyArduino/__init__.pyi +331 -0
- Barmaja_PyArduino/__main__.py +2 -0
- Barmaja_PyArduino/adapters/__init__.py +35 -0
- Barmaja_PyArduino/api_registry.py +459 -0
- Barmaja_PyArduino/backend/__init__.py +3 -0
- Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
- Barmaja_PyArduino/board.py +80 -0
- Barmaja_PyArduino/cli.py +223 -0
- Barmaja_PyArduino/compiler.py +1721 -0
- Barmaja_PyArduino/console.py +217 -0
- Barmaja_PyArduino/device_packs.py +367 -0
- Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
- Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
- Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
- Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
- Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
- Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
- Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
- Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
- Barmaja_PyArduino/doctor.py +406 -0
- Barmaja_PyArduino/environment.py +595 -0
- Barmaja_PyArduino/errors.py +166 -0
- Barmaja_PyArduino/flow.py +754 -0
- Barmaja_PyArduino/frontend.py +611 -0
- Barmaja_PyArduino/installer.py +151 -0
- Barmaja_PyArduino/ir.py +660 -0
- Barmaja_PyArduino/library_adder.py +440 -0
- Barmaja_PyArduino/live.py +859 -0
- Barmaja_PyArduino/lowering.py +596 -0
- Barmaja_PyArduino/numeric.py +748 -0
- Barmaja_PyArduino/pack_builder.py +544 -0
- Barmaja_PyArduino/py.typed +0 -0
- Barmaja_PyArduino/serial_monitor.py +172 -0
- Barmaja_PyArduino/source.py +184 -0
- Barmaja_PyArduino/stubgen.py +148 -0
- Barmaja_PyArduino/symbols.py +646 -0
- Barmaja_PyArduino/target.py +1631 -0
- Barmaja_PyArduino/toolchain.py +506 -0
- Barmaja_PyArduino/typesys.py +917 -0
- Barmaja_PyArduino/workflow.py +213 -0
- barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
- barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
- barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
- barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
- barmaja_pyarduino-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""PC-side Serial monitor for ``upload()`` sketches (BARMAJA PyArduino 1.1.0.dev8).
|
|
2
|
+
|
|
3
|
+
After a successful compile AND upload the Arduino CLI has released the COM port.
|
|
4
|
+
Only then does this module open the SAME port with pyserial and copy everything
|
|
5
|
+
the Uno prints into the host's normal Python console (PyCharm Run, IDLE,
|
|
6
|
+
terminal ...). The sketch itself is a normal standalone Arduino program: it
|
|
7
|
+
keeps running when the monitor is closed or the USB cable is removed.
|
|
8
|
+
|
|
9
|
+
NEVER use this module while LiveRun/Telemetrix owns the port. LiveRun has its
|
|
10
|
+
own stdout compatibility layer (see ``Serial`` in ``__init__``).
|
|
11
|
+
|
|
12
|
+
Environment variables (all optional, for advanced users / teachers):
|
|
13
|
+
BARMAJA_SERIAL_MONITOR auto (default) | off - never changes student code
|
|
14
|
+
BARMAJA_SERIAL_WAIT seconds to wait for the Uno bootloader after the
|
|
15
|
+
port is opened (default 0.0; bytes are buffered by
|
|
16
|
+
the OS so startup output is not lost)
|
|
17
|
+
BARMAJA_SERIAL_OPEN_TRIES attempts to open the freshly released port (default 8)
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import codecs
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from typing import Callable
|
|
26
|
+
|
|
27
|
+
__all__ = ["monitor_baud", "monitor_enabled", "stream_serial_output"]
|
|
28
|
+
|
|
29
|
+
_OFF = {"0", "off", "false", "no", "none", "disabled"}
|
|
30
|
+
DEFAULT_BAUD = 9600
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def monitor_baud(result) -> int | None:
|
|
34
|
+
"""Return the baud for a compiled sketch that writes to Serial, else None.
|
|
35
|
+
|
|
36
|
+
Uses compiler metadata (TargetPlan), not text search of the generated C++:
|
|
37
|
+
* ``print()``, ``Serial.print()``, ``Serial.println()`` and ``Serial.write()``
|
|
38
|
+
set ``serial_output``;
|
|
39
|
+
* ``resolved_serial_baud`` is the single compile-time baud that the compiler
|
|
40
|
+
itself emitted (automatic 9600 for ``print()``, or the explicit value).
|
|
41
|
+
A sketch with only ``Serial.begin()`` / ``Serial.read()`` has no output, so an
|
|
42
|
+
LED-only or input-only program exits normally instead of blocking forever.
|
|
43
|
+
"""
|
|
44
|
+
plan = getattr(result, "target_plan", None)
|
|
45
|
+
if plan is None or not getattr(plan, "serial_output", False):
|
|
46
|
+
return None
|
|
47
|
+
baud = getattr(plan, "resolved_serial_baud", None)
|
|
48
|
+
return int(baud) if baud else DEFAULT_BAUD
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def monitor_enabled(requested: bool | None = None) -> bool:
|
|
52
|
+
"""``upload(monitor=False)`` wins, then ``BARMAJA_SERIAL_MONITOR``."""
|
|
53
|
+
if requested is not None:
|
|
54
|
+
return bool(requested)
|
|
55
|
+
return os.environ.get("BARMAJA_SERIAL_MONITOR", "auto").strip().lower() not in _OFF
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _float_env(name: str, default: float) -> float:
|
|
59
|
+
try:
|
|
60
|
+
return max(0.0, float(os.environ.get(name, default)))
|
|
61
|
+
except ValueError:
|
|
62
|
+
return default
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _int_env(name: str, default: int) -> int:
|
|
66
|
+
try:
|
|
67
|
+
return max(1, int(os.environ.get(name, default)))
|
|
68
|
+
except ValueError:
|
|
69
|
+
return default
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _write_console(text: str) -> None:
|
|
73
|
+
"""Write to stdout without ever raising on odd characters (cp1252 consoles)."""
|
|
74
|
+
try:
|
|
75
|
+
sys.stdout.write(text)
|
|
76
|
+
except UnicodeEncodeError:
|
|
77
|
+
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
|
78
|
+
sys.stdout.write(text.encode(enc, errors="replace").decode(enc, errors="replace"))
|
|
79
|
+
try:
|
|
80
|
+
sys.stdout.flush()
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _open_with_retry(serial_module, port: str, baud: int, tries: int):
|
|
86
|
+
"""Open the COM port. Windows may need a moment after avrdude releases it."""
|
|
87
|
+
last: BaseException | None = None
|
|
88
|
+
for attempt in range(tries):
|
|
89
|
+
try:
|
|
90
|
+
return serial_module.Serial(port=port, baudrate=baud, timeout=0.2)
|
|
91
|
+
except Exception as exc: # SerialException / PermissionError / OSError
|
|
92
|
+
last = exc
|
|
93
|
+
text = str(exc).lower()
|
|
94
|
+
busy = any(w in text for w in ("access is denied", "permissionerror", "busy", "denied"))
|
|
95
|
+
gone = any(w in text for w in ("could not open", "file not found", "cannot find"))
|
|
96
|
+
if attempt + 1 < tries and (busy or gone):
|
|
97
|
+
time.sleep(0.4)
|
|
98
|
+
continue
|
|
99
|
+
break
|
|
100
|
+
assert last is not None
|
|
101
|
+
raise last
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def stream_serial_output(
|
|
105
|
+
port: str,
|
|
106
|
+
baud: int,
|
|
107
|
+
*,
|
|
108
|
+
announce: Callable[..., None] = print,
|
|
109
|
+
) -> int:
|
|
110
|
+
"""Show the Uno's Serial output until Ctrl+C / IDE Stop / USB removal.
|
|
111
|
+
|
|
112
|
+
Returns 0 after a normal stop, 1 if the monitor could not run. The upload has
|
|
113
|
+
already succeeded in both cases; the monitor never changes what the Arduino
|
|
114
|
+
does. The port is always closed in ``finally``.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
import serial # pyserial 3.5 (pinned, bundled with the offline package)
|
|
118
|
+
except ImportError:
|
|
119
|
+
announce("Serial viewer unavailable: pyserial is not installed. "
|
|
120
|
+
"Open the Arduino IDE Serial Monitor instead.")
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
announce(f"Serial output from {port} at {baud} baud. Press Ctrl+C (or Stop) to stop viewing.")
|
|
124
|
+
announce("(The Arduino keeps running its program after the viewer stops.)")
|
|
125
|
+
announce("")
|
|
126
|
+
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
127
|
+
connection = None
|
|
128
|
+
try:
|
|
129
|
+
connection = _open_with_retry(serial, port, baud, _int_env("BARMAJA_SERIAL_OPEN_TRIES", 8))
|
|
130
|
+
wait = _float_env("BARMAJA_SERIAL_WAIT", 0.0)
|
|
131
|
+
if wait:
|
|
132
|
+
time.sleep(wait)
|
|
133
|
+
while True:
|
|
134
|
+
# read(1) blocks at most `timeout`; then drain whatever else is waiting.
|
|
135
|
+
chunk = connection.read(1)
|
|
136
|
+
if chunk:
|
|
137
|
+
try:
|
|
138
|
+
waiting = connection.in_waiting
|
|
139
|
+
except Exception:
|
|
140
|
+
waiting = 0
|
|
141
|
+
if waiting:
|
|
142
|
+
chunk += connection.read(min(waiting, 4096))
|
|
143
|
+
text = decoder.decode(chunk)
|
|
144
|
+
if text:
|
|
145
|
+
_write_console(text)
|
|
146
|
+
# unreachable
|
|
147
|
+
except KeyboardInterrupt:
|
|
148
|
+
announce("")
|
|
149
|
+
announce("Serial viewer stopped. The Arduino continues running its program.")
|
|
150
|
+
return 0
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
tail = decoder.decode(b"", final=True)
|
|
153
|
+
if tail:
|
|
154
|
+
_write_console(tail)
|
|
155
|
+
text = str(exc).lower()
|
|
156
|
+
announce("")
|
|
157
|
+
if any(w in text for w in ("access is denied", "permissionerror", "busy", "denied")):
|
|
158
|
+
announce(f"Serial viewer could not open {port}: the port is in use.")
|
|
159
|
+
announce("Close the Arduino IDE / PictoBlox Serial Monitor (only one program can use the port), then run again.")
|
|
160
|
+
elif connection is not None:
|
|
161
|
+
announce(f"Serial viewer stopped: the connection to {port} was lost (USB unplugged?).")
|
|
162
|
+
announce("The Arduino keeps running if it has power. Reconnect USB and run the program again to view it.")
|
|
163
|
+
else:
|
|
164
|
+
announce(f"Serial viewer could not open {port}: {exc}")
|
|
165
|
+
announce("The upload succeeded and the Arduino is running. Check the USB cable and COM port.")
|
|
166
|
+
return 1
|
|
167
|
+
finally:
|
|
168
|
+
if connection is not None:
|
|
169
|
+
try:
|
|
170
|
+
connection.close()
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import io
|
|
5
|
+
import tokenize
|
|
6
|
+
import warnings
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .errors import SourceEncodingError, SourceSyntaxError
|
|
12
|
+
|
|
13
|
+
# Barmaja 0.6.0 deliberately accepts one common Python grammar across every
|
|
14
|
+
# supported host interpreter (3.10 and every newer version). Newer host-only syntax must
|
|
15
|
+
# not silently become valid just because a teacher upgraded Python.
|
|
16
|
+
COMMON_PYTHON_FEATURE_VERSION = 10
|
|
17
|
+
|
|
18
|
+
def _parser_feature_version(version_info=None):
|
|
19
|
+
"""Return the grammar minor version usable by this host.
|
|
20
|
+
|
|
21
|
+
Normal Barmaja hosts are Python 3.10 or newer and therefore always parse the
|
|
22
|
+
common Python-3.10 grammar. PictoBlox V1 currently embeds CPython 3.8.10;
|
|
23
|
+
that host cannot request a future grammar from ast.parse(), so use its own
|
|
24
|
+
3.8 grammar. The supported Barmaja student subset does not depend on
|
|
25
|
+
Python-3.9/3.10-only syntax, so this does not change supported-language
|
|
26
|
+
semantics; it only lets the same compiler run inside the mandatory
|
|
27
|
+
PictoBlox host.
|
|
28
|
+
"""
|
|
29
|
+
vi = version_info or sys.version_info
|
|
30
|
+
major = int(vi[0])
|
|
31
|
+
minor = int(vi[1])
|
|
32
|
+
if major != 3:
|
|
33
|
+
return COMMON_PYTHON_FEATURE_VERSION
|
|
34
|
+
return min(COMMON_PYTHON_FEATURE_VERSION, minor)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class SourceSpan:
|
|
39
|
+
"""A source location using Python's 1-based lines / 0-based columns."""
|
|
40
|
+
|
|
41
|
+
line: int
|
|
42
|
+
column: int
|
|
43
|
+
end_line: int
|
|
44
|
+
end_column: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ParsedSource:
|
|
49
|
+
"""Phase-A output: decoded source, parsed AST, tokens, and source identity."""
|
|
50
|
+
|
|
51
|
+
filename: str
|
|
52
|
+
text: str
|
|
53
|
+
tree: ast.Module
|
|
54
|
+
tokens: tuple[tokenize.TokenInfo, ...]
|
|
55
|
+
|
|
56
|
+
def span_for(self, node: ast.AST) -> SourceSpan | None:
|
|
57
|
+
line = getattr(node, "lineno", None)
|
|
58
|
+
column = getattr(node, "col_offset", None)
|
|
59
|
+
if line is None or column is None:
|
|
60
|
+
return None
|
|
61
|
+
end_line = getattr(node, "end_lineno", None) or line
|
|
62
|
+
end_column = getattr(node, "end_col_offset", None)
|
|
63
|
+
if end_column is None:
|
|
64
|
+
end_column = column
|
|
65
|
+
return SourceSpan(line, column, end_line, end_column)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _syntax_error_from(exc: SyntaxError, filename: str) -> SourceSyntaxError:
|
|
69
|
+
# SyntaxError.offset is 1-based while ast node col_offset is 0-based. The
|
|
70
|
+
# existing student-facing CompileError contract also uses the syntax-style
|
|
71
|
+
# 1-based column, so preserve that here.
|
|
72
|
+
return SourceSyntaxError(
|
|
73
|
+
exc.msg or "Invalid Python syntax.",
|
|
74
|
+
line=exc.lineno,
|
|
75
|
+
column=exc.offset,
|
|
76
|
+
end_line=getattr(exc, "end_lineno", None),
|
|
77
|
+
end_column=getattr(exc, "end_offset", None),
|
|
78
|
+
source=filename,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _Py38SubscriptNormalizer(ast.NodeTransformer):
|
|
84
|
+
"""Rewrite Python 3.8 ast.Index/ast.ExtSlice to the Python 3.9+ shape."""
|
|
85
|
+
|
|
86
|
+
def visit_Index(self, node): # Python 3.8 only
|
|
87
|
+
return self.visit(node.value)
|
|
88
|
+
|
|
89
|
+
def visit_ExtSlice(self, node): # Python 3.8 only
|
|
90
|
+
dims = [self.visit(d) for d in node.dims]
|
|
91
|
+
return ast.Tuple(elts=dims, ctx=ast.Load())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _normalize_py38_subscripts(tree: ast.Module) -> ast.Module:
|
|
95
|
+
tree = _Py38SubscriptNormalizer().visit(tree)
|
|
96
|
+
return ast.fix_missing_locations(tree)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_source_text(source: str, source_path: str | Path | None = None) -> ParsedSource:
|
|
100
|
+
"""Parse text using the common Barmaja Python-3.10 grammar profile."""
|
|
101
|
+
|
|
102
|
+
filename = str(source_path or "<student.py>")
|
|
103
|
+
|
|
104
|
+
# compile_text() may receive text copied from a UTF-8-with-BOM file. The
|
|
105
|
+
# file loader already strips it through utf-8-sig, but accepting it here
|
|
106
|
+
# keeps the public text API deterministic too.
|
|
107
|
+
if source.startswith("\ufeff"):
|
|
108
|
+
source = source[1:]
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Python-level SyntaxWarning/DeprecationWarning text (for example an
|
|
112
|
+
# invalid escape such as "C:\path") must not leak to the student's
|
|
113
|
+
# console as a raw interpreter message during compilation.
|
|
114
|
+
with warnings.catch_warnings():
|
|
115
|
+
warnings.simplefilter("ignore")
|
|
116
|
+
tree = ast.parse(
|
|
117
|
+
source,
|
|
118
|
+
filename=filename,
|
|
119
|
+
mode="exec",
|
|
120
|
+
feature_version=_parser_feature_version(),
|
|
121
|
+
)
|
|
122
|
+
except SyntaxError as exc:
|
|
123
|
+
raise _syntax_error_from(exc, filename) from None
|
|
124
|
+
except UnicodeEncodeError: # (a ValueError subclass; must precede it)
|
|
125
|
+
# Only reachable through the text API: a lone surrogate cannot come from a UTF-8 file.
|
|
126
|
+
raise SourceEncodingError(
|
|
127
|
+
"Source text contains characters that cannot be saved as UTF-8. Save the Python file as UTF-8 and try again.",
|
|
128
|
+
source=filename,
|
|
129
|
+
) from None
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
# Python 3.10/3.11 report NUL bytes as ValueError instead of SyntaxError.
|
|
132
|
+
raise SourceSyntaxError(
|
|
133
|
+
str(exc) or "Invalid Python source.",
|
|
134
|
+
source=filename,
|
|
135
|
+
) from None
|
|
136
|
+
except (RecursionError, MemoryError):
|
|
137
|
+
# CPython 3.11-3.13 raise RecursionError while building very deep ASTs
|
|
138
|
+
# (for example thousands of chained '+' terms) instead of SyntaxError.
|
|
139
|
+
raise SourceSyntaxError(
|
|
140
|
+
"This expression or statement is nested too deeply for Barmaja PyArduino. Split it into smaller steps.",
|
|
141
|
+
source=filename,
|
|
142
|
+
) from None
|
|
143
|
+
|
|
144
|
+
if sys.version_info < (3, 9):
|
|
145
|
+
# PictoBlox embeds CPython 3.8, whose AST wraps subscripts in
|
|
146
|
+
# ast.Index / ast.ExtSlice. Normalize to the 3.9+ shape so every later
|
|
147
|
+
# phase sees one AST on every supported host.
|
|
148
|
+
tree = _normalize_py38_subscripts(tree)
|
|
149
|
+
|
|
150
|
+
# Token data is part of the Phase-A contract and is intentionally captured
|
|
151
|
+
# now so later diagnostics do not need to re-tokenize a possibly transformed
|
|
152
|
+
# representation of the source.
|
|
153
|
+
try:
|
|
154
|
+
tokens = tuple(tokenize.generate_tokens(io.StringIO(source).readline))
|
|
155
|
+
except (RecursionError, MemoryError):
|
|
156
|
+
raise SourceSyntaxError(
|
|
157
|
+
"This expression or statement is nested too deeply for Barmaja PyArduino. Split it into smaller steps.",
|
|
158
|
+
source=filename,
|
|
159
|
+
) from None
|
|
160
|
+
except (tokenize.TokenError, IndentationError) as exc:
|
|
161
|
+
# ast.parse normally catches these first, but keep the source boundary
|
|
162
|
+
# closed and student-facing if tokenization ever disagrees.
|
|
163
|
+
message = exc.args[0] if exc.args else "Invalid Python source."
|
|
164
|
+
location = exc.args[1] if len(exc.args) > 1 and isinstance(exc.args[1], tuple) else None
|
|
165
|
+
line = location[0] if location else None
|
|
166
|
+
column = (location[1] + 1) if location else None
|
|
167
|
+
raise SourceSyntaxError(str(message), line=line, column=column, source=filename) from None
|
|
168
|
+
|
|
169
|
+
return ParsedSource(filename=filename, text=source, tree=tree, tokens=tokens)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def load_source_file(path: str | Path) -> ParsedSource:
|
|
173
|
+
"""Read a student source file as UTF-8/UTF-8-BOM and parse it."""
|
|
174
|
+
|
|
175
|
+
p = Path(path)
|
|
176
|
+
raw = p.read_bytes()
|
|
177
|
+
try:
|
|
178
|
+
text = raw.decode("utf-8-sig")
|
|
179
|
+
except UnicodeDecodeError as exc:
|
|
180
|
+
raise SourceEncodingError(
|
|
181
|
+
"Source file is not valid UTF-8. Save the Python file as UTF-8 and try again.",
|
|
182
|
+
source=str(p),
|
|
183
|
+
) from None
|
|
184
|
+
return parse_source_text(text, p)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Generate the public typing stub directly from the formal API registry.
|
|
4
|
+
|
|
5
|
+
The generated stub is editor-neutral. Any Python IDE/type checker that understands
|
|
6
|
+
PEP 484/561 can use it. PictoBlox remains a mandatory supported host, but the
|
|
7
|
+
stub itself contains no IDE-specific integration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .api_registry import CONSTANT_VALUES, CORE_FUNCTIONS, HARDWARE_CLASSES, SINGLETONS, SignatureSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_ARG_NAMES = {
|
|
16
|
+
"pinMode": ("pin", "mode"),
|
|
17
|
+
"digitalWrite": ("pin", "value"),
|
|
18
|
+
"digitalRead": ("pin",),
|
|
19
|
+
"analogRead": ("pin",),
|
|
20
|
+
"analogWrite": ("pin", "value"),
|
|
21
|
+
"delay": ("milliseconds",),
|
|
22
|
+
"tone": ("pin", "frequency", "duration"),
|
|
23
|
+
"noTone": ("pin",),
|
|
24
|
+
"delayMicroseconds": ("microseconds",),
|
|
25
|
+
"map": ("value", "in_min", "in_max", "out_min", "out_max"),
|
|
26
|
+
"constrain": ("value", "lower", "upper"),
|
|
27
|
+
"Servo.__init__": (),
|
|
28
|
+
"Servo.attach": ("pin", "min_us", "max_us"),
|
|
29
|
+
"Servo.write": ("angle",),
|
|
30
|
+
"Servo.writeMicroseconds": ("microseconds",),
|
|
31
|
+
"Ultrasonic.__init__": ("trigger_pin", "echo_pin"),
|
|
32
|
+
"Ultrasonic.read": ("unit",),
|
|
33
|
+
"Ultrasonic.setTimeout": ("timeout",),
|
|
34
|
+
"DHT.__init__": ("pin", "sensor_type"),
|
|
35
|
+
"LiquidCrystal_I2C.__init__": ("address", "columns", "rows"),
|
|
36
|
+
"LiquidCrystal_I2C.begin": ("columns", "rows", "charsize"),
|
|
37
|
+
"LiquidCrystal_I2C.setCursor": ("column", "row"),
|
|
38
|
+
"LiquidCrystal_I2C.print": ("value",),
|
|
39
|
+
"Serial.begin": ("baud",),
|
|
40
|
+
"Serial.print": ("value", "format"),
|
|
41
|
+
"Serial.println": ("value", "format"),
|
|
42
|
+
"Wire.beginTransmission": ("address",),
|
|
43
|
+
"Wire.write": ("value",),
|
|
44
|
+
"Wire.requestFrom": ("address", "count"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_TYPE_MAP = {
|
|
48
|
+
"Void": "None",
|
|
49
|
+
"Bool": "bool",
|
|
50
|
+
"Int": "int",
|
|
51
|
+
"Float": "float",
|
|
52
|
+
"String": "str",
|
|
53
|
+
"TimeU32": "TimeU32",
|
|
54
|
+
"IntOrBool": "IntOrBool",
|
|
55
|
+
"NumericOrBool": "NumericOrBool",
|
|
56
|
+
"Scalar": "Scalar",
|
|
57
|
+
"UltrasonicUnit": "UltrasonicUnit",
|
|
58
|
+
"DHTType": "DHTType",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _py_type(token: str) -> str:
|
|
63
|
+
if token.startswith("Hardware["):
|
|
64
|
+
return token[len("Hardware["):-1]
|
|
65
|
+
return _TYPE_MAP.get(token, "int")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _params(sig: SignatureSpec, *, method: bool = False, key: str = "") -> str:
|
|
69
|
+
parts = ["self"] if method else []
|
|
70
|
+
names = _ARG_NAMES.get(key, ())
|
|
71
|
+
for i, token in enumerate(sig.args):
|
|
72
|
+
name = names[i] if i < len(names) else f"arg{i}"
|
|
73
|
+
parts.append(f"{name}: {_py_type(token)}")
|
|
74
|
+
if not sig.args and method:
|
|
75
|
+
return "self"
|
|
76
|
+
return ", ".join(parts)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _emit_callable(name: str, signatures: tuple[SignatureSpec, ...], *, indent: str = "", method: bool = False, key: str = "") -> list[str]:
|
|
80
|
+
lines: list[str] = []
|
|
81
|
+
if len(signatures) > 1:
|
|
82
|
+
for sig in signatures:
|
|
83
|
+
lines.append(f"{indent}@overload")
|
|
84
|
+
lines.append(f"{indent}def {name}({_params(sig, method=method, key=key)}) -> {_py_type(sig.returns)}: ...")
|
|
85
|
+
else:
|
|
86
|
+
sig = signatures[0]
|
|
87
|
+
lines.append(f"{indent}def {name}({_params(sig, method=method, key=key)}) -> {_py_type(sig.returns)}: ...")
|
|
88
|
+
return lines
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def render_stub() -> str:
|
|
92
|
+
lines = [
|
|
93
|
+
"# AUTO-GENERATED by Barmaja_PyArduino.stubgen from api_registry.py.",
|
|
94
|
+
"# Do not edit manually; change the registry and regenerate.",
|
|
95
|
+
"from typing import Final, TypeAlias, overload",
|
|
96
|
+
"",
|
|
97
|
+
"TimeU32: TypeAlias = int",
|
|
98
|
+
"IntOrBool: TypeAlias = int | bool",
|
|
99
|
+
"NumericOrBool: TypeAlias = int | float | bool",
|
|
100
|
+
"Scalar: TypeAlias = int | float | bool | str",
|
|
101
|
+
"UltrasonicUnit: TypeAlias = int",
|
|
102
|
+
"DHTType: TypeAlias = int",
|
|
103
|
+
"",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for name in CONSTANT_VALUES:
|
|
107
|
+
lines.append(f"{name}: Final[int]")
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
for name, spec in CORE_FUNCTIONS.items():
|
|
111
|
+
lines.extend(_emit_callable(name, spec.signatures, key=name))
|
|
112
|
+
lines.append("")
|
|
113
|
+
|
|
114
|
+
for class_name, spec in HARDWARE_CLASSES.items():
|
|
115
|
+
lines.append(f"class {class_name}:")
|
|
116
|
+
ctor_sigs = tuple(SignatureSpec(s.args, "Void", s.constraints) for s in spec.constructors)
|
|
117
|
+
lines.extend(_emit_callable("__init__", ctor_sigs, indent=" ", method=True, key=f"{class_name}.__init__"))
|
|
118
|
+
for method_name, method_spec in spec.methods.items():
|
|
119
|
+
lines.extend(_emit_callable(method_name, method_spec.signatures, indent=" ", method=True, key=f"{class_name}.{method_name}"))
|
|
120
|
+
lines.append("")
|
|
121
|
+
|
|
122
|
+
for singleton_name, spec in SINGLETONS.items():
|
|
123
|
+
cls = f"_{singleton_name}API"
|
|
124
|
+
lines.append(f"class {cls}:")
|
|
125
|
+
for method_name, method_spec in spec.methods.items():
|
|
126
|
+
lines.extend(_emit_callable(method_name, method_spec.signatures, indent=" ", method=True, key=f"{singleton_name}.{method_name}"))
|
|
127
|
+
lines.append(f"{singleton_name}: Final[{cls}]")
|
|
128
|
+
lines.append("")
|
|
129
|
+
|
|
130
|
+
lines.append("def upload(path: str | None = None, *, port: str | None = None, monitor: bool | None = None) -> None: ...")
|
|
131
|
+
lines.append("def verify(path: str | None = None) -> None: ...")
|
|
132
|
+
lines.append("def show_code(path: str | None = None) -> None: ...")
|
|
133
|
+
lines.append("def LiveRun(port: str | None = None, *, auto_firmware: bool = True, debug: bool = False): ...")
|
|
134
|
+
lines.append("def LiveStop() -> None: ...")
|
|
135
|
+
lines.append("")
|
|
136
|
+
lines.append("def doctor_pictoblox(*, details: bool = False) -> str: ...")
|
|
137
|
+
lines.append("")
|
|
138
|
+
return "\n".join(lines)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def write_stub(path: str | Path | None = None) -> Path:
|
|
142
|
+
target = Path(path) if path is not None else Path(__file__).with_name("__init__.pyi")
|
|
143
|
+
target.write_text(render_stub(), encoding="utf-8", newline="\n")
|
|
144
|
+
return target
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
print(write_stub())
|