ggblab 0.9.3__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 (26) hide show
  1. ggblab/__init__.py +44 -0
  2. ggblab/_version.py +4 -0
  3. ggblab/comm.py +243 -0
  4. ggblab/construction.py +179 -0
  5. ggblab/errors.py +142 -0
  6. ggblab/ggbapplet.py +293 -0
  7. ggblab/parser.py +486 -0
  8. ggblab/persistent_counter.py +175 -0
  9. ggblab/schema.py +114 -0
  10. ggblab/utils.py +109 -0
  11. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/build_log.json +730 -0
  12. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/install.json +5 -0
  13. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/package.json +210 -0
  14. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/schemas/ggblab/package.json.orig +205 -0
  15. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/schemas/ggblab/plugin.json +8 -0
  16. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/lib_index_js.bbfa36bc62ee08eb62b2.js +465 -0
  17. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/lib_index_js.bbfa36bc62ee08eb62b2.js.map +1 -0
  18. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/remoteEntry.2d29364aef8b527d773e.js +568 -0
  19. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/remoteEntry.2d29364aef8b527d773e.js.map +1 -0
  20. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style.js +4 -0
  21. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style_index_js.aab9f5416f41ce79cac3.js +492 -0
  22. ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style_index_js.aab9f5416f41ce79cac3.js.map +1 -0
  23. ggblab-0.9.3.dist-info/METADATA +768 -0
  24. ggblab-0.9.3.dist-info/RECORD +26 -0
  25. ggblab-0.9.3.dist-info/WHEEL +4 -0
  26. ggblab-0.9.3.dist-info/licenses/LICENSE +29 -0
ggblab/ggbapplet.py ADDED
@@ -0,0 +1,293 @@
1
+ import asyncio
2
+ import re
3
+ import ipykernel.connect
4
+
5
+ from IPython.core.getipython import get_ipython
6
+ from ipylab import JupyterFrontEnd
7
+
8
+ from .comm import ggb_comm
9
+ from .errors import (
10
+ GeoGebraError,
11
+ GeoGebraCommandError,
12
+ GeoGebraSyntaxError,
13
+ GeoGebraSemanticsError,
14
+ GeoGebraAppletError
15
+ )
16
+ from .construction import ggb_construction
17
+ from .parser import ggb_parser
18
+ from .utils import flatten
19
+
20
+
21
+ # Exception hierarchy is defined in errors.py and imported above
22
+ class GeoGebra:
23
+ """Main interface for controlling GeoGebra applets from Python.
24
+
25
+ This class implements a singleton pattern to ensure only one GeoGebra
26
+ instance per kernel session. It provides async methods for sending
27
+ commands and calling GeoGebra API functions.
28
+
29
+ The communication uses a dual-channel architecture:
30
+ - IPython Comm: Primary control channel
31
+ - Unix socket/TCP WebSocket: Out-of-band response delivery during cell execution
32
+
33
+ Semantic Validation:
34
+ - check_syntax: Validates command strings can be tokenized
35
+ - check_semantics: Validates referenced objects exist in applet
36
+ - Future: Type checking, scope/visibility validation
37
+
38
+ Attributes:
39
+ construction (ggb_construction): File loader/saver for .ggb files
40
+ parser (ggb_parser): Dependency graph parser with command learning
41
+ comm (ggb_comm): Communication layer (initialized after init())
42
+ kernel_id (str): Current Jupyter kernel ID
43
+ app (JupyterFrontEnd): ipylab frontend interface
44
+ check_syntax (bool): Enable syntax validation (default: False)
45
+ check_semantics (bool): Enable semantic validation (default: False)
46
+ _applet_objects (set): Cached object names from applet (updated by command/function)
47
+
48
+ Example:
49
+ >>> ggb = GeoGebra()
50
+ >>> await ggb.init()
51
+ >>> await ggb.command("A=(0,0)")
52
+ >>> result = await ggb.function("getValue", ["A"])
53
+
54
+ >>> # With validation
55
+ >>> ggb.check_syntax = True
56
+ >>> ggb.check_semantics = True
57
+ >>> await ggb.command("Circle(A, B)")
58
+ """
59
+ _instance = None
60
+
61
+ def __new__(cls):
62
+ if cls._instance is None:
63
+ cls._instance = super().__new__(cls)
64
+ return cls._instance
65
+
66
+ def __init__(self):
67
+ self.initialized = False
68
+ self.construction = ggb_construction()
69
+ self.parser = ggb_parser()
70
+ self.check_syntax = False
71
+ self.check_semantics = False
72
+ self._applet_objects = set() # Cache of known objects
73
+
74
+ async def init(self):
75
+ """Initialize the GeoGebra widget and communication channels.
76
+
77
+ This method:
78
+ 1. Starts the out-of-band socket server (Unix socket on POSIX, TCP WebSocket on Windows)
79
+ 2. Registers the IPython Comm target ('ggblab-comm')
80
+ 3. Opens the GeoGebra widget panel via ipylab with communication settings
81
+ 4. Initializes the object cache
82
+
83
+ The widget is launched programmatically to pass kernel-specific settings
84
+ (Comm target, socket path) before initialization, avoiding the limitations
85
+ of fixed arguments from Launcher/Command Palette.
86
+
87
+ Returns:
88
+ GeoGebra: Self reference for method chaining.
89
+
90
+ Example:
91
+ >>> ggb = await GeoGebra().init()
92
+ >>> # GeoGebra panel opens in split-right position
93
+ """
94
+ if not self.initialized:
95
+ self.comm = ggb_comm()
96
+ self.comm.start()
97
+ while self.comm.socketPath is None:
98
+ await asyncio.sleep(.01)
99
+ self.comm.register_target()
100
+
101
+ _connection_file = ipykernel.connect.get_connection_file()
102
+ self.kernel_id = re.search(r'kernel-(.*)\.json', _connection_file).group(1)
103
+
104
+ self.app = JupyterFrontEnd()
105
+ self.app.commands.execute('ggblab:create', {
106
+ 'kernelId': self.kernel_id,
107
+ 'commTarget': 'ggblab-comm',
108
+ 'insertMode': 'split-right',
109
+ 'socketPath': self.comm.socketPath,
110
+ # 'wsPort': self.comm.wsPort,
111
+ })
112
+
113
+ # Initialize object cache
114
+ await self.refresh_object_cache()
115
+
116
+ self._initialized = True
117
+ return self
118
+
119
+ def _is_literal(self, token):
120
+ """Check if token is a literal value (number, string, boolean, math function).
121
+
122
+ Literals should not be validated as object references. This includes:
123
+ - Numeric literals: 2, 3.14, -5, 1e-3
124
+ - String literals: "text", 'string'
125
+ - Boolean constants: true, false
126
+ - Math functions: sin, cos, sqrt, etc.
127
+
128
+ Args:
129
+ token: Token to check
130
+
131
+ Returns:
132
+ bool: True if token is a literal, False if it could be an object reference
133
+ """
134
+ if not isinstance(token, str) or not token:
135
+ return True
136
+
137
+ # Numeric literals (integers, decimals, scientific notation)
138
+ try:
139
+ float(token)
140
+ return True
141
+ except ValueError:
142
+ pass
143
+
144
+ # String literals (quoted)
145
+ if token[0] in ('"', "'"):
146
+ return True
147
+
148
+ # Boolean constants
149
+ if token in ('true', 'false'):
150
+ return True
151
+
152
+ # Common GeoGebra/math functions
153
+ math_functions = {
154
+ 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
155
+ 'sinh', 'cosh', 'tanh',
156
+ 'sqrt', 'abs', 'log', 'ln', 'log10', 'exp',
157
+ 'floor', 'ceil', 'round', 'sgn',
158
+ 'random', 'min', 'max', 'sum', 'mean',
159
+ }
160
+ if token in math_functions:
161
+ return True
162
+
163
+ return False
164
+
165
+ async def refresh_object_cache(self):
166
+ """Refresh the cached set of known objects from the applet.
167
+
168
+ Called automatically during init() and can be called manually to
169
+ synchronize the object cache with current applet state.
170
+ """
171
+ try:
172
+ objects = await self.function("getAllObjectNames")
173
+ self._applet_objects = set(objects) if objects else set()
174
+ except Exception as e:
175
+ print(f"Warning: Could not refresh object cache: {e}")
176
+
177
+ async def function(self, f, args=None):
178
+ """Call a GeoGebra API function.
179
+
180
+ Args:
181
+ f (str): GeoGebra API function name (e.g., "getValue", "getXML").
182
+ args (list, optional): Function arguments. Defaults to None.
183
+
184
+ Returns:
185
+ Any: Function return value from GeoGebra.
186
+
187
+ Example:
188
+ >>> value = await ggb.function("getValue", ["A"])
189
+ >>> xml = await ggb.function("getXML", ["A"])
190
+ >>> all_objs = await ggb.function("getAllObjectNames")
191
+ """
192
+ r = await self.comm.send_recv({
193
+ "type": "function",
194
+ "payload": {
195
+ "name": f,
196
+ "args": args
197
+ }
198
+ })
199
+ return r['value']
200
+
201
+ async def command(self, c):
202
+ """Execute a GeoGebra command with optional validation.
203
+
204
+ Args:
205
+ c (str): GeoGebra command string (e.g., "A=(0,0)", "Circle(A, 2)").
206
+
207
+ Returns:
208
+ dict: Response from GeoGebra (typically includes object label).
209
+
210
+ Raises:
211
+ GeoGebraSyntaxError: If syntax check is enabled and command has syntax errors.
212
+ GeoGebraSemanticsError: If semantics check is enabled and validation fails.
213
+ GeoGebraAppletError: If GeoGebra applet produces error events during execution.
214
+
215
+ Example:
216
+ >>> await ggb.command("A=(0,0)")
217
+ >>> await ggb.command("B=(3,4)")
218
+ >>> await ggb.command("Circle(A, Distance(A, B))")
219
+
220
+ >>> # With validation
221
+ >>> ggb.check_syntax = True
222
+ >>> ggb.check_semantics = True
223
+ >>> await ggb.command("Circle(A, B)") # Validates syntax and references
224
+
225
+ >>> # Error handling
226
+ >>> try:
227
+ ... await ggb.command("Unbalanced(")
228
+ ... except GeoGebraAppletError as e:
229
+ ... print(f"Applet error: {e.error_message}")
230
+ """
231
+ # Syntax check: validate command can be tokenized
232
+ if self.check_syntax:
233
+ try:
234
+ self.parser.tokenize_with_commas(c)
235
+ except Exception as e:
236
+ raise GeoGebraSyntaxError(c, str(e))
237
+
238
+ # Semantics check: validate referenced objects exist in applet
239
+ if self.check_semantics:
240
+ try:
241
+ # Refresh object cache before checking
242
+ await self.refresh_object_cache()
243
+
244
+ # Extract object tokens: tokens in the flattened structure that are
245
+ # not commands (not in command_cache), not commas, and not literals
246
+ t = self.parser.tokenize_with_commas(c)
247
+ object_tokens = [o for o in flatten(t)
248
+ if o not in self.parser.command_cache
249
+ and o != ","
250
+ and not self._is_literal(o)]
251
+
252
+ # Check if referenced objects exist
253
+ missing_objects = [obj for obj in object_tokens
254
+ if obj not in self._applet_objects]
255
+
256
+ if missing_objects:
257
+ raise GeoGebraSemanticsError(
258
+ c,
259
+ f"Referenced object(s) do not exist in applet: {missing_objects}",
260
+ missing_objects
261
+ )
262
+ except GeoGebraSemanticsError:
263
+ raise
264
+ except Exception as e:
265
+ raise GeoGebraSemanticsError(c, f"Validation error: {e}")
266
+
267
+ result = await self.comm.send_recv({
268
+ "type": "command",
269
+ "payload": c
270
+ })
271
+
272
+ # FUTURE: Error event queue processing for enhanced scope learning
273
+ # After command execution, GeoGebra appends error events to self.comm.recv_events.queue:
274
+ # {'type': 'Error', 'payload': 'Unbalanced brackets'}
275
+ # {'type': 'Error', 'payload': 'Circle(A, 1 '}
276
+ #
277
+ # This enables:
278
+ # 1. Real-time error capture: Complement pre-flight validation with actual GeoGebra errors
279
+ # 2. Dynamic scope updates: Track which objects were created despite errors
280
+ # 3. Cross-domain learning: Correlate error patterns with domain-specific semantics
281
+ # 4. Validation refinement: Use GeoGebra's error feedback to improve check_semantics logic
282
+ #
283
+ # Implementation strategy:
284
+ # - Drain error queue: while self.comm.recv_events.queue: event = popleft()
285
+ # - Classify errors: syntax vs semantic vs type errors
286
+ # - Update validation rules based on error patterns
287
+ # - Store error context for cross-session learning via parser.command_cache
288
+
289
+ # Update object cache on successful command
290
+ if result and 'label' in result:
291
+ self._applet_objects.add(result['label'])
292
+
293
+ return result