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.
- ggblab/__init__.py +44 -0
- ggblab/_version.py +4 -0
- ggblab/comm.py +243 -0
- ggblab/construction.py +179 -0
- ggblab/errors.py +142 -0
- ggblab/ggbapplet.py +293 -0
- ggblab/parser.py +486 -0
- ggblab/persistent_counter.py +175 -0
- ggblab/schema.py +114 -0
- ggblab/utils.py +109 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/build_log.json +730 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/install.json +5 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/package.json +210 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/schemas/ggblab/package.json.orig +205 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/schemas/ggblab/plugin.json +8 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/lib_index_js.bbfa36bc62ee08eb62b2.js +465 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/lib_index_js.bbfa36bc62ee08eb62b2.js.map +1 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/remoteEntry.2d29364aef8b527d773e.js +568 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/remoteEntry.2d29364aef8b527d773e.js.map +1 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style.js +4 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style_index_js.aab9f5416f41ce79cac3.js +492 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style_index_js.aab9f5416f41ce79cac3.js.map +1 -0
- ggblab-0.9.3.dist-info/METADATA +768 -0
- ggblab-0.9.3.dist-info/RECORD +26 -0
- ggblab-0.9.3.dist-info/WHEEL +4 -0
- 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
|