import asyncio
import re
import ipykernel.connect
from IPython.core.getipython import get_ipython
from ipylab import JupyterFrontEnd
from .comm import ggb_comm
from .file import ggb_file
from .parser import ggb_parser
from .errors import (
GeoGebraError,
GeoGebraCommandError,
GeoGebraSyntaxError,
GeoGebraSemanticsError,
GeoGebraAppletError
)
from .utils import flatten
# Exception hierarchy is defined in errors.py and imported above
[docs]
class GeoGebra:
"""Main interface for controlling GeoGebra applets from Python.
This class implements a singleton pattern to ensure only one GeoGebra
instance per kernel session. It provides async methods for sending
commands and calling GeoGebra API functions.
The communication uses a dual-channel architecture:
- IPython Comm: Primary control channel
- Unix socket/TCP WebSocket: Out-of-band response delivery during cell execution
Semantic Validation:
- check_syntax: Validates command strings can be tokenized
- check_semantics: Validates referenced objects exist in applet
- Future: Type checking, scope/visibility validation
Attributes:
file (ggb_file): GeoGebra file (.ggb) loader and saver
construction: Backward compatibility alias for file attribute
parser: Dependency graph parser with command learning
comm (ggb_comm): Communication layer (initialized after init())
kernel_id (str): Current Jupyter kernel ID
app (JupyterFrontEnd): ipylab frontend interface
check_syntax (bool): Enable syntax validation (default: False)
check_semantics (bool): Enable semantic validation (default: False)
_applet_objects (set): Cached object names from applet (updated by command/function)
Note:
The parser attribute lives in this package and provides tokenization
and command-cache features used for syntax/semantics checks.
Example:
>>> ggb = GeoGebra()
>>> await ggb.init()
>>> await ggb.command("A=(0,0)")
>>> result = await ggb.function("getValue", ["A"])
>>> # With validation
>>> ggb.check_syntax = True
>>> ggb.check_semantics = True
>>> await ggb.command("Circle(A, B)")
"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.initialized = False
self.file = ggb_file() # .ggb file I/O
self.construction = self.file # Backward compatibility alias
self.parser = ggb_parser()
self.check_syntax = False
self.check_semantics = False
self._applet_objects = set() # Cache of known objects
[docs]
async def init(self):
"""Initialize the GeoGebra widget and communication channels.
This method:
1. Starts the out-of-band socket server (Unix socket on POSIX, TCP WebSocket on Windows)
2. Registers the IPython Comm target ('ggblab-comm')
3. Opens the GeoGebra widget panel via ipylab with communication settings
4. Initializes the object cache
The widget is launched programmatically to pass kernel-specific settings
(Comm target, socket path) before initialization, avoiding the limitations
of fixed arguments from Launcher/Command Palette.
Returns:
GeoGebra: Self reference for method chaining.
Example:
>>> ggb = await GeoGebra().init()
>>> # GeoGebra panel opens in split-right position
"""
if not self.initialized:
self.comm = ggb_comm()
self.comm.start()
while self.comm.socketPath is None:
await asyncio.sleep(.01)
self.comm.register_target()
_connection_file = ipykernel.connect.get_connection_file()
self.kernel_id = re.search(r'kernel-(.*)\.json', _connection_file).group(1)
self.app = JupyterFrontEnd()
self.app.commands.execute('ggblab:create', {
'kernelId': self.kernel_id,
'commTarget': 'ggblab-comm',
'insertMode': 'split-right',
'socketPath': self.comm.socketPath,
# 'wsPort': self.comm.wsPort,
})
# Initialize object cache
# await self.refresh_object_cache()
self._applet_objects = set()
self._initialized = True
return self
def _is_literal(self, token):
"""Check if token is a literal value (number, string, boolean, math function).
Literals should not be validated as object references. This includes:
- Numeric literals: 2, 3.14, -5, 1e-3
- String literals: "text", 'string'
- Boolean constants: true, false
- Math functions: sin, cos, sqrt, etc.
Args:
token: Token to check
Returns:
bool: True if token is a literal, False if it could be an object reference
"""
if not isinstance(token, str) or not token:
return True
# Numeric literals (integers, decimals, scientific notation)
try:
float(token)
return True
except ValueError:
pass
# String literals (quoted)
if token[0] in ('"', "'"):
return True
# Boolean constants
if token in ('true', 'false'):
return True
# Common GeoGebra/math functions
math_functions = {
'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
'sinh', 'cosh', 'tanh',
'sqrt', 'abs', 'log', 'ln', 'log10', 'exp',
'floor', 'ceil', 'round', 'sgn',
'random', 'min', 'max', 'sum', 'mean',
}
if token in math_functions:
return True
return False
[docs]
async def refresh_object_cache(self):
"""Refresh the cached set of known objects from the applet.
Called automatically during init() and can be called manually to
synchronize the object cache with current applet state.
"""
try:
objects = await self.function("getAllObjectNames")
self._applet_objects = set(objects) if objects else set()
except Exception as e:
print(f"Warning: Could not refresh object cache: {type(e).__name__} {e}")
[docs]
async def function(self, f, args=None):
"""Call a GeoGebra API function.
Args:
f (str): GeoGebra API function name (e.g., "getValue", "getXML").
args (list, optional): Function arguments. Defaults to None.
Returns:
Any: Function return value from GeoGebra.
Example:
>>> value = await ggb.function("getValue", ["A"])
>>> xml = await ggb.function("getXML", ["A"])
>>> all_objs = await ggb.function("getAllObjectNames")
"""
r = await self.comm.send_recv({
"type": "function",
"payload": {
"name": f,
"args": args
}
})
return r['value']
[docs]
async def command(self, c):
"""Execute a GeoGebra command with optional validation.
Args:
c (str): GeoGebra command string (e.g., "A=(0,0)", "Circle(A, 2)").
Returns:
dict: Response from GeoGebra (typically includes object label).
Raises:
GeoGebraSyntaxError: If syntax check is enabled and command has syntax errors.
GeoGebraSemanticsError: If semantics check is enabled and validation fails.
GeoGebraAppletError: If GeoGebra applet produces error events during execution.
Example:
>>> await ggb.command("A=(0,0)")
>>> await ggb.command("B=(3,4)")
>>> await ggb.command("Circle(A, Distance(A, B))")
>>> # With validation
>>> ggb.check_syntax = True
>>> ggb.check_semantics = True
>>> await ggb.command("Circle(A, B)") # Validates syntax and references
>>> # Error handling
>>> try:
... await ggb.command("Unbalanced(")
... except GeoGebraAppletError as e:
... print(f"Applet error: {e.error_message}")
"""
# Syntax check: validate command can be tokenized
if self.check_syntax:
try:
self.parser.tokenize_with_commas(c)
except Exception as e:
raise GeoGebraSyntaxError(c, str(e))
# Semantics check: validate referenced objects exist in applet
if self.check_semantics:
try:
# Refresh object cache before checking
await self.refresh_object_cache()
# Extract object tokens: tokens in the flattened structure that are
# not commands (not in command_cache), not commas, and not literals
t = self.parser.tokenize_with_commas(c)
object_tokens = [o for o in flatten(t)
if o not in self.parser.command_cache
and o != ","
and not self._is_literal(o)]
# Check if referenced objects exist
missing_objects = [obj for obj in object_tokens
if obj not in self._applet_objects]
if missing_objects:
raise GeoGebraSemanticsError(
c,
f"Referenced object(s) do not exist in applet: {missing_objects}",
missing_objects
)
except GeoGebraSemanticsError:
raise
except Exception as e:
raise GeoGebraSemanticsError(c, f"Validation error: {e}")
result = await self.comm.send_recv({
"type": "command",
"payload": c
})
# FUTURE: Error event queue processing for enhanced scope learning
# After command execution, GeoGebra appends error events to self.comm.recv_events.queue:
# {'type': 'Error', 'payload': 'Unbalanced brackets'}
# {'type': 'Error', 'payload': 'Circle(A, 1 '}
#
# This enables:
# 1. Real-time error capture: Complement pre-flight validation with actual GeoGebra errors
# 2. Dynamic scope updates: Track which objects were created despite errors
# 3. Cross-domain learning: Correlate error patterns with domain-specific semantics
# 4. Validation refinement: Use GeoGebra's error feedback to improve check_semantics logic
#
# Implementation strategy:
# - Drain error queue: while self.comm.recv_events.queue: event = popleft()
# - Classify errors: syntax vs semantic vs type errors
# - Update validation rules based on error patterns
# - Store error context for cross-session learning via parser.command_cache
# Update object cache on successful command
if result and 'label' in result:
self._applet_objects.add(result['label'])
return result