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.
Files changed (87) hide show
  1. Barmaja_PyArduino/BARMAJA_PyArduino.ico +0 -0
  2. Barmaja_PyArduino/__init__.py +528 -0
  3. Barmaja_PyArduino/__init__.pyi +331 -0
  4. Barmaja_PyArduino/__main__.py +2 -0
  5. Barmaja_PyArduino/adapters/__init__.py +35 -0
  6. Barmaja_PyArduino/api_registry.py +459 -0
  7. Barmaja_PyArduino/backend/__init__.py +3 -0
  8. Barmaja_PyArduino/backend/uno_cpp.py +1680 -0
  9. Barmaja_PyArduino/board.py +80 -0
  10. Barmaja_PyArduino/cli.py +223 -0
  11. Barmaja_PyArduino/compiler.py +1721 -0
  12. Barmaja_PyArduino/console.py +217 -0
  13. Barmaja_PyArduino/device_packs.py +367 -0
  14. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/library.properties +9 -0
  15. Barmaja_PyArduino/device_packs_builtin/Button/arduino/Barmaja_Button/src/Barmaja_Button.h +18 -0
  16. Barmaja_PyArduino/device_packs_builtin/Button/example.py +10 -0
  17. Barmaja_PyArduino/device_packs_builtin/Button/pack.json +40 -0
  18. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/library.properties +9 -0
  19. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/arduino/Barmaja_ClockDS3231/src/Barmaja_ClockDS3231.h +20 -0
  20. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/example.py +8 -0
  21. Barmaja_PyArduino/device_packs_builtin/ClockDS3231/pack.json +136 -0
  22. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/library.properties +9 -0
  23. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/arduino/Barmaja_DistanceVL53L0X/src/Barmaja_DistanceVL53L0X.h +12 -0
  24. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/example.py +8 -0
  25. Barmaja_PyArduino/device_packs_builtin/DistanceVL53L0X/pack.json +48 -0
  26. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/library.properties +9 -0
  27. Barmaja_PyArduino/device_packs_builtin/LCDParallel/arduino/Barmaja_LCDParallel/src/Barmaja_LCDParallel.h +17 -0
  28. Barmaja_PyArduino/device_packs_builtin/LCDParallel/example.py +10 -0
  29. Barmaja_PyArduino/device_packs_builtin/LCDParallel/pack.json +137 -0
  30. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/library.properties +9 -0
  31. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/arduino/Barmaja_LoadCellHX711/src/Barmaja_LoadCellHX711.h +16 -0
  32. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/example.py +11 -0
  33. Barmaja_PyArduino/device_packs_builtin/LoadCellHX711/pack.json +74 -0
  34. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/library.properties +9 -0
  35. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/arduino/Barmaja_NeoPixelStrip/src/Barmaja_NeoPixelStrip.h +15 -0
  36. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/example.py +13 -0
  37. Barmaja_PyArduino/device_packs_builtin/NeoPixelStrip/pack.json +114 -0
  38. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/library.properties +9 -0
  39. Barmaja_PyArduino/device_packs_builtin/OLEDText/arduino/Barmaja_OLEDText/src/Barmaja_OLEDText.h +21 -0
  40. Barmaja_PyArduino/device_packs_builtin/OLEDText/example.py +9 -0
  41. Barmaja_PyArduino/device_packs_builtin/OLEDText/pack.json +103 -0
  42. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/library.properties +9 -0
  43. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/arduino/Barmaja_PressureBMP280/src/Barmaja_PressureBMP280.h +16 -0
  44. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/example.py +8 -0
  45. Barmaja_PyArduino/device_packs_builtin/PressureBMP280/pack.json +92 -0
  46. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/library.properties +9 -0
  47. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/arduino/Barmaja_RotaryEncoder/src/Barmaja_RotaryEncoder.h +18 -0
  48. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/example.py +10 -0
  49. Barmaja_PyArduino/device_packs_builtin/RotaryEncoder/pack.json +44 -0
  50. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/library.properties +9 -0
  51. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/arduino/Barmaja_ServoDriverPCA9685/src/Barmaja_ServoDriverPCA9685.h +15 -0
  52. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/example.py +10 -0
  53. Barmaja_PyArduino/device_packs_builtin/ServoDriverPCA9685/pack.json +117 -0
  54. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/library.properties +9 -0
  55. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/arduino/Barmaja_ShiftRegister595/src/Barmaja_ShiftRegister595.h +14 -0
  56. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/example.py +9 -0
  57. Barmaja_PyArduino/device_packs_builtin/ShiftRegister595/pack.json +83 -0
  58. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/library.properties +9 -0
  59. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/arduino/Barmaja_TemperatureDS18B20/src/Barmaja_TemperatureDS18B20.h +16 -0
  60. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/example.py +8 -0
  61. Barmaja_PyArduino/device_packs_builtin/TemperatureDS18B20/pack.json +63 -0
  62. Barmaja_PyArduino/doctor.py +406 -0
  63. Barmaja_PyArduino/environment.py +595 -0
  64. Barmaja_PyArduino/errors.py +166 -0
  65. Barmaja_PyArduino/flow.py +754 -0
  66. Barmaja_PyArduino/frontend.py +611 -0
  67. Barmaja_PyArduino/installer.py +151 -0
  68. Barmaja_PyArduino/ir.py +660 -0
  69. Barmaja_PyArduino/library_adder.py +440 -0
  70. Barmaja_PyArduino/live.py +859 -0
  71. Barmaja_PyArduino/lowering.py +596 -0
  72. Barmaja_PyArduino/numeric.py +748 -0
  73. Barmaja_PyArduino/pack_builder.py +544 -0
  74. Barmaja_PyArduino/py.typed +0 -0
  75. Barmaja_PyArduino/serial_monitor.py +172 -0
  76. Barmaja_PyArduino/source.py +184 -0
  77. Barmaja_PyArduino/stubgen.py +148 -0
  78. Barmaja_PyArduino/symbols.py +646 -0
  79. Barmaja_PyArduino/target.py +1631 -0
  80. Barmaja_PyArduino/toolchain.py +506 -0
  81. Barmaja_PyArduino/typesys.py +917 -0
  82. Barmaja_PyArduino/workflow.py +213 -0
  83. barmaja_pyarduino-2.0.0.dist-info/METADATA +39 -0
  84. barmaja_pyarduino-2.0.0.dist-info/RECORD +87 -0
  85. barmaja_pyarduino-2.0.0.dist-info/WHEEL +5 -0
  86. barmaja_pyarduino-2.0.0.dist-info/licenses/LICENSE.txt +7 -0
  87. 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())