shell-lite 0.3.5__py3-none-any.whl → 0.4.1__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.
- shell_lite/ast_nodes.py +24 -1
- shell_lite/compiler.py +14 -2
- shell_lite/interpreter.py +212 -17
- shell_lite/js_compiler.py +17 -3
- shell_lite/lexer.py +4 -1
- shell_lite/main.py +263 -81
- shell_lite/parser.py +227 -23
- shell_lite-0.4.1.dist-info/METADATA +77 -0
- shell_lite-0.4.1.dist-info/RECORD +17 -0
- shell_lite-0.3.5.dist-info/METADATA +0 -40
- shell_lite-0.3.5.dist-info/RECORD +0 -17
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/LICENSE +0 -0
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/WHEEL +0 -0
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/entry_points.txt +0 -0
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/top_level.txt +0 -0
shell_lite/ast_nodes.py
CHANGED
|
@@ -83,7 +83,7 @@ class Return(Node):
|
|
|
83
83
|
@dataclass
|
|
84
84
|
class ClassDef(Node):
|
|
85
85
|
name: str
|
|
86
|
-
properties: List[str]
|
|
86
|
+
properties: List[tuple[str, Optional[Node]]]
|
|
87
87
|
methods: List[FunctionDef]
|
|
88
88
|
parent: Optional[str] = None
|
|
89
89
|
@dataclass
|
|
@@ -272,3 +272,26 @@ class FileRead(Node):
|
|
|
272
272
|
class DatabaseOp(Node):
|
|
273
273
|
op: str
|
|
274
274
|
args: List[Node]
|
|
275
|
+
@dataclass
|
|
276
|
+
class PythonImport(Node):
|
|
277
|
+
module_name: str
|
|
278
|
+
alias: Optional[str]
|
|
279
|
+
|
|
280
|
+
@dataclass
|
|
281
|
+
class App(Node):
|
|
282
|
+
title: str
|
|
283
|
+
width: int
|
|
284
|
+
height: int
|
|
285
|
+
body: List[Node]
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class Widget(Node):
|
|
289
|
+
widget_type: str # 'button', 'input', 'heading', 'text'
|
|
290
|
+
label: str
|
|
291
|
+
var_name: Optional[str] = None
|
|
292
|
+
event_handler: Optional[List[Node]] = None # For buttons with 'do:'
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class Layout(Node):
|
|
296
|
+
layout_type: str # 'column', 'row'
|
|
297
|
+
body: List[Node]
|
shell_lite/compiler.py
CHANGED
|
@@ -360,8 +360,20 @@ class Compiler:
|
|
|
360
360
|
parent = node.parent if node.parent else "Instance"
|
|
361
361
|
code = f"class {node.name}({parent}):\n"
|
|
362
362
|
self.indentation += 1
|
|
363
|
-
args = ["self"]
|
|
364
|
-
assigns = [
|
|
363
|
+
args = ["self"]
|
|
364
|
+
assigns = []
|
|
365
|
+
for prop in node.properties:
|
|
366
|
+
if isinstance(prop, tuple):
|
|
367
|
+
name, default = prop
|
|
368
|
+
if default:
|
|
369
|
+
args.append(f"{name}={self.visit(default)}")
|
|
370
|
+
else:
|
|
371
|
+
args.append(name)
|
|
372
|
+
assigns.append(f"self.{name} = {name}")
|
|
373
|
+
else:
|
|
374
|
+
args.append(prop)
|
|
375
|
+
assigns.append(f"self.{prop} = {prop}")
|
|
376
|
+
|
|
365
377
|
if not assigns:
|
|
366
378
|
assigns = ["pass"]
|
|
367
379
|
code += f"{self.indent()}def __init__({', '.join(args)}):\n"
|
shell_lite/interpreter.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import Any, Dict, List, Callable
|
|
|
2
2
|
from .ast_nodes import *
|
|
3
3
|
from .lexer import Token, Lexer
|
|
4
4
|
from .parser import Parser
|
|
5
|
+
import importlib
|
|
5
6
|
import operator
|
|
6
7
|
import re
|
|
7
8
|
import os
|
|
@@ -618,12 +619,31 @@ class Interpreter:
|
|
|
618
619
|
raise NameError(f"Class '{node.class_name}' not defined.")
|
|
619
620
|
class_def = self.classes[node.class_name]
|
|
620
621
|
all_properties = self._get_class_properties(class_def)
|
|
621
|
-
|
|
622
|
-
|
|
622
|
+
|
|
623
|
+
# Check args length
|
|
624
|
+
# First count how many properties are required (no default)
|
|
625
|
+
required_count = 0
|
|
626
|
+
for name, default_val in all_properties:
|
|
627
|
+
if default_val is None:
|
|
628
|
+
required_count += 1
|
|
629
|
+
|
|
630
|
+
if len(node.args) < required_count:
|
|
631
|
+
raise TypeError(f"Structure '{node.class_name}' expects at least {required_count} args, got {len(node.args)}")
|
|
632
|
+
|
|
623
633
|
instance = Instance(class_def)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
634
|
+
|
|
635
|
+
for i, (prop_name, default_val) in enumerate(all_properties):
|
|
636
|
+
val = None
|
|
637
|
+
if i < len(node.args):
|
|
638
|
+
val = self.visit(node.args[i])
|
|
639
|
+
elif default_val is not None:
|
|
640
|
+
val = self.visit(default_val)
|
|
641
|
+
else:
|
|
642
|
+
# Should be caught by required_count check, but safety fallback
|
|
643
|
+
raise TypeError(f"Missing argument for property '{prop_name}' in '{node.class_name}'")
|
|
644
|
+
|
|
645
|
+
instance.data[prop_name] = val
|
|
646
|
+
|
|
627
647
|
self.current_env.set(node.var_name, instance)
|
|
628
648
|
return instance
|
|
629
649
|
def visit_MethodCall(self, node: MethodCall):
|
|
@@ -675,7 +695,7 @@ class Interpreter:
|
|
|
675
695
|
new_env.set(k, v)
|
|
676
696
|
if len(node.args) > len(method_node.args):
|
|
677
697
|
raise TypeError(f"Method '{node.method_name}' expects max {len(method_node.args)} arguments.")
|
|
678
|
-
for i, (arg_name, default_node) in enumerate(method_node.args):
|
|
698
|
+
for i, (arg_name, default_node, type_hint) in enumerate(method_node.args):
|
|
679
699
|
if i < len(node.args):
|
|
680
700
|
val = self.visit(node.args[i])
|
|
681
701
|
elif default_node is not None:
|
|
@@ -698,15 +718,39 @@ class Interpreter:
|
|
|
698
718
|
return ret_val
|
|
699
719
|
def visit_PropertyAccess(self, node: PropertyAccess):
|
|
700
720
|
instance = self.current_env.get(node.instance_name)
|
|
721
|
+
|
|
722
|
+
# 1. ShellLite Instance
|
|
701
723
|
if isinstance(instance, Instance):
|
|
702
724
|
if node.property_name not in instance.data:
|
|
703
|
-
|
|
725
|
+
# Check for methods? PropertyAccess usually implies data.
|
|
726
|
+
# But in some cases we might want method reference?
|
|
727
|
+
raise AttributeError(f"Structure '{instance.class_def.name}' has no property '{node.property_name}'")
|
|
704
728
|
return instance.data[node.property_name]
|
|
729
|
+
|
|
730
|
+
# 2. Dictionary
|
|
705
731
|
elif isinstance(instance, dict):
|
|
706
732
|
if node.property_name in instance:
|
|
707
733
|
return instance[node.property_name]
|
|
708
734
|
raise AttributeError(f"Dictionary has no key '{node.property_name}'")
|
|
709
|
-
|
|
735
|
+
|
|
736
|
+
# 3. List
|
|
737
|
+
elif isinstance(instance, list):
|
|
738
|
+
if node.property_name == 'length':
|
|
739
|
+
return len(instance)
|
|
740
|
+
|
|
741
|
+
# 4. String
|
|
742
|
+
elif isinstance(instance, str):
|
|
743
|
+
if node.property_name == 'length':
|
|
744
|
+
return len(instance)
|
|
745
|
+
|
|
746
|
+
# 5. Python Object / Module Interop
|
|
747
|
+
# If the instance has the attribute natively, return it.
|
|
748
|
+
# This handles 'math.pi', 'os.name', etc.
|
|
749
|
+
if hasattr(instance, node.property_name):
|
|
750
|
+
return getattr(instance, node.property_name)
|
|
751
|
+
|
|
752
|
+
raise TypeError(f"Object '{node.instance_name}' (type {type(instance).__name__}) has no property '{node.property_name}'")
|
|
753
|
+
|
|
710
754
|
def visit_Import(self, node: Import):
|
|
711
755
|
if node.path in self.std_modules:
|
|
712
756
|
self.current_env.set(node.path, self.std_modules[node.path])
|
|
@@ -750,8 +794,16 @@ class Interpreter:
|
|
|
750
794
|
statements = parser.parse()
|
|
751
795
|
for stmt in statements:
|
|
752
796
|
self.visit(stmt)
|
|
753
|
-
def _get_class_properties(self, class_def: ClassDef) -> List[str]:
|
|
754
|
-
|
|
797
|
+
def _get_class_properties(self, class_def: ClassDef) -> List[tuple[str, Optional[Node]]]:
|
|
798
|
+
if not hasattr(class_def, 'properties'): return []
|
|
799
|
+
# Support both old string list and new tuple list for backward compat if needed, though we updated AST
|
|
800
|
+
props = []
|
|
801
|
+
for p in class_def.properties:
|
|
802
|
+
if isinstance(p, tuple):
|
|
803
|
+
props.append(p)
|
|
804
|
+
else:
|
|
805
|
+
props.append((p, None))
|
|
806
|
+
|
|
755
807
|
if class_def.parent:
|
|
756
808
|
if class_def.parent not in self.classes:
|
|
757
809
|
raise NameError(f"Parent class '{class_def.parent}' not defined.")
|
|
@@ -950,6 +1002,14 @@ class Interpreter:
|
|
|
950
1002
|
raise StopException()
|
|
951
1003
|
def visit_Skip(self, node: Skip):
|
|
952
1004
|
raise SkipException()
|
|
1005
|
+
def visit_PythonImport(self, node: PythonImport):
|
|
1006
|
+
try:
|
|
1007
|
+
mod = importlib.import_module(node.module_name)
|
|
1008
|
+
name = node.alias if node.alias else node.module_name.split('.')[0]
|
|
1009
|
+
self.global_env.set(name, mod)
|
|
1010
|
+
except ImportError as e:
|
|
1011
|
+
raise RuntimeError(f"Could not import python module '{node.module_name}': {e}")
|
|
1012
|
+
|
|
953
1013
|
def visit_Throw(self, node: Throw):
|
|
954
1014
|
message = self.visit(node.message)
|
|
955
1015
|
raise ShellLiteError(str(message))
|
|
@@ -1009,6 +1069,7 @@ class Interpreter:
|
|
|
1009
1069
|
result = None
|
|
1010
1070
|
for stmt in statements:
|
|
1011
1071
|
result = self.visit(stmt)
|
|
1072
|
+
self.current_env.set('__exec_result__', result)
|
|
1012
1073
|
return result
|
|
1013
1074
|
def visit_ImportAs(self, node: ImportAs):
|
|
1014
1075
|
if node.path in self.std_modules:
|
|
@@ -1088,18 +1149,152 @@ class Interpreter:
|
|
|
1088
1149
|
code = 0
|
|
1089
1150
|
if node.code:
|
|
1090
1151
|
code = self.visit(node.code)
|
|
1091
|
-
|
|
1092
|
-
sys.exit(
|
|
1152
|
+
sys.exit(int(code))
|
|
1153
|
+
sys.exit(0)
|
|
1154
|
+
|
|
1155
|
+
# -------------------------------------------------------------------------
|
|
1156
|
+
# Project Polaris: Phase 2 (The Canvas - Native UI)
|
|
1157
|
+
# -------------------------------------------------------------------------
|
|
1158
|
+
def visit_App(self, node: App):
|
|
1159
|
+
# We need a root for the app
|
|
1160
|
+
import tkinter as tk
|
|
1161
|
+
from tkinter import messagebox
|
|
1162
|
+
root = tk.Tk()
|
|
1163
|
+
root.title(node.title)
|
|
1164
|
+
root.geometry(f"{node.width}x{node.height}")
|
|
1165
|
+
|
|
1166
|
+
# Store root for potential access, though mostly we use 'master' passed down
|
|
1167
|
+
# Ideally we pass 'parent' to visits, but we don't have that signature.
|
|
1168
|
+
# So we'll use a stack or a temporary context.
|
|
1169
|
+
self.ui_parent_stack = [root]
|
|
1170
|
+
|
|
1171
|
+
# Define a helpful alert function available in UI context
|
|
1172
|
+
def ui_alert(msg):
|
|
1173
|
+
messagebox.showinfo("Message", str(msg))
|
|
1174
|
+
self.current_env.set("alert", ui_alert)
|
|
1175
|
+
|
|
1176
|
+
try:
|
|
1177
|
+
for child in node.body:
|
|
1178
|
+
self.visit(child)
|
|
1179
|
+
finally:
|
|
1180
|
+
self.ui_parent_stack.pop()
|
|
1181
|
+
|
|
1182
|
+
root.mainloop()
|
|
1183
|
+
|
|
1184
|
+
def visit_Layout(self, node: Layout):
|
|
1185
|
+
parent = self.ui_parent_stack[-1]
|
|
1186
|
+
|
|
1187
|
+
# Create a frame for the layout
|
|
1188
|
+
frame = tk.Frame(parent)
|
|
1189
|
+
|
|
1190
|
+
# Pack options based on layout type of THIS container relative to parent??
|
|
1191
|
+
# Usually Layout implies how CHILDREN are arranged.
|
|
1192
|
+
# But here 'column' means "I am a column" -> children stacked vertically.
|
|
1193
|
+
# 'row' means "I am a row" -> children stacked horizontally.
|
|
1194
|
+
|
|
1195
|
+
# In Tkinter, pack() defaults to vertical (column).
|
|
1196
|
+
# side=LEFT makes it horizontal (row).
|
|
1197
|
+
|
|
1198
|
+
# We start by adding the frame to the parent.
|
|
1199
|
+
# If parent is a Column, we pack(side=TOP). If Row, pack(side=LEFT).
|
|
1200
|
+
# But simplified: Just use pack(fill=X) or something.
|
|
1201
|
+
frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
1202
|
+
|
|
1203
|
+
self.ui_parent_stack.append((frame, node.layout_type))
|
|
1204
|
+
try:
|
|
1205
|
+
for child in node.body:
|
|
1206
|
+
self.visit(child)
|
|
1207
|
+
finally:
|
|
1208
|
+
self.ui_parent_stack.pop()
|
|
1209
|
+
|
|
1210
|
+
def visit_Widget(self, node: Widget):
|
|
1211
|
+
from tkinter import messagebox
|
|
1212
|
+
parent_ctx = self.ui_parent_stack[-1]
|
|
1213
|
+
if isinstance(parent_ctx, tuple):
|
|
1214
|
+
parent, layout_mode = parent_ctx
|
|
1215
|
+
else:
|
|
1216
|
+
parent = parent_ctx
|
|
1217
|
+
layout_mode = 'column' # Default to column
|
|
1218
|
+
|
|
1219
|
+
widget = None
|
|
1220
|
+
if node.widget_type == 'button':
|
|
1221
|
+
# Handle event
|
|
1222
|
+
def on_click():
|
|
1223
|
+
if node.event_handler:
|
|
1224
|
+
try:
|
|
1225
|
+
for stmt in node.event_handler:
|
|
1226
|
+
self.visit(stmt)
|
|
1227
|
+
except Exception as e:
|
|
1228
|
+
messagebox.showerror("Error", str(e))
|
|
1229
|
+
|
|
1230
|
+
widget = tk.Button(parent, text=node.label, command=on_click)
|
|
1231
|
+
|
|
1232
|
+
elif node.widget_type == 'input':
|
|
1233
|
+
lbl = tk.Label(parent, text=node.label)
|
|
1234
|
+
pack_opts = {'side': tk.TOP, 'anchor': 'w'} if layout_mode == 'column' else {'side': tk.LEFT}
|
|
1235
|
+
lbl.pack(**pack_opts)
|
|
1236
|
+
|
|
1237
|
+
widget = tk.Entry(parent)
|
|
1238
|
+
|
|
1239
|
+
# Store accessor in Env so we can read .value
|
|
1240
|
+
if node.var_name:
|
|
1241
|
+
# We can't store the widget directly because .value access visits PropertyAccess
|
|
1242
|
+
# which expects dict, list, or python object.
|
|
1243
|
+
# Tkinter Entry has get().
|
|
1244
|
+
# We wrap it or just rely on 'visit_PropertyAccess' (Phase 1)
|
|
1245
|
+
# By default Tkinter widgets store config.
|
|
1246
|
+
# Let's verify if widget.value works natively? No.
|
|
1247
|
+
# So we wrap it.
|
|
1248
|
+
class InputWrapper:
|
|
1249
|
+
def __init__(self, w): self.w = w
|
|
1250
|
+
@property
|
|
1251
|
+
def value(self): return self.w.get()
|
|
1252
|
+
@property
|
|
1253
|
+
def text(self): return self.w.get()
|
|
1254
|
+
|
|
1255
|
+
self.current_env.set(node.var_name, InputWrapper(widget))
|
|
1256
|
+
|
|
1257
|
+
elif node.widget_type == 'heading':
|
|
1258
|
+
widget = tk.Label(parent, text=node.label, font=("Helvetica", 16, "bold"))
|
|
1259
|
+
|
|
1260
|
+
elif node.widget_type == 'text':
|
|
1261
|
+
widget = tk.Label(parent, text=node.label)
|
|
1262
|
+
|
|
1263
|
+
if widget:
|
|
1264
|
+
# Layout the widget
|
|
1265
|
+
if layout_mode == 'column':
|
|
1266
|
+
widget.pack(side=tk.TOP, pady=5, fill=tk.X)
|
|
1267
|
+
else:
|
|
1268
|
+
widget.pack(side=tk.LEFT, padx=5)
|
|
1269
|
+
|
|
1093
1270
|
def visit_Make(self, node: Make):
|
|
1094
1271
|
if node.class_name not in self.classes:
|
|
1095
1272
|
raise NameError(f"Thing '{node.class_name}' not defined.")
|
|
1096
1273
|
class_def = self.classes[node.class_name]
|
|
1097
1274
|
props = self._get_class_properties(class_def)
|
|
1098
|
-
|
|
1099
|
-
|
|
1275
|
+
|
|
1276
|
+
# Check args length
|
|
1277
|
+
required_count = 0
|
|
1278
|
+
for name, default_val in props:
|
|
1279
|
+
if default_val is None:
|
|
1280
|
+
required_count += 1
|
|
1281
|
+
|
|
1282
|
+
if len(node.args) < required_count:
|
|
1283
|
+
raise TypeError(f"Thing '{node.class_name}' expects at least {required_count} values, got {len(node.args)}")
|
|
1284
|
+
|
|
1100
1285
|
instance = Instance(class_def)
|
|
1101
|
-
|
|
1102
|
-
|
|
1286
|
+
|
|
1287
|
+
for i, (prop_name, default_val) in enumerate(props):
|
|
1288
|
+
val = None
|
|
1289
|
+
if i < len(node.args):
|
|
1290
|
+
val = self.visit(node.args[i])
|
|
1291
|
+
elif default_val is not None:
|
|
1292
|
+
val = self.visit(default_val)
|
|
1293
|
+
else:
|
|
1294
|
+
raise TypeError(f"Missing argument for property '{prop_name}' in '{node.class_name}'")
|
|
1295
|
+
|
|
1296
|
+
instance.data[prop_name] = val
|
|
1297
|
+
|
|
1103
1298
|
return instance
|
|
1104
1299
|
def visit_Convert(self, node: Convert):
|
|
1105
1300
|
val = self.visit(node.expression)
|
|
@@ -1324,7 +1519,7 @@ class Interpreter:
|
|
|
1324
1519
|
self.wfile.write(str(e).encode())
|
|
1325
1520
|
except: pass
|
|
1326
1521
|
server = HTTPServer(('0.0.0.0', port_val), ShellLiteHandler)
|
|
1327
|
-
print(f"\n ShellLite Server v0.
|
|
1522
|
+
print(f"\n ShellLite Server v0.04.1 is running!")
|
|
1328
1523
|
print(f" \u001b[1;36m➜\u001b[0m Local: \u001b[1;4;36mhttp://localhost:{port_val}/\u001b[0m\n")
|
|
1329
1524
|
try: server.serve_forever()
|
|
1330
1525
|
except KeyboardInterrupt:
|
shell_lite/js_compiler.py
CHANGED
|
@@ -149,12 +149,26 @@ class JSCompiler:
|
|
|
149
149
|
code = f"class {node.name}{extends} {{\n"
|
|
150
150
|
self.indentation += 1
|
|
151
151
|
if node.properties:
|
|
152
|
-
props =
|
|
152
|
+
props = []
|
|
153
|
+
assigns = []
|
|
154
|
+
for p in node.properties:
|
|
155
|
+
if isinstance(p, tuple):
|
|
156
|
+
name, default = p
|
|
157
|
+
if default:
|
|
158
|
+
# JS 6 supports defaults in args
|
|
159
|
+
props.append(f"{name} = {self.visit(default)}")
|
|
160
|
+
else:
|
|
161
|
+
props.append(name)
|
|
162
|
+
assigns.append(f"self.{name} = {name};")
|
|
163
|
+
else:
|
|
164
|
+
props.append(p)
|
|
165
|
+
assigns.append(f"self.{p} = {p};")
|
|
166
|
+
|
|
153
167
|
code += f"{self.indent()}constructor({', '.join(props)}) {{\n"
|
|
154
168
|
self.indentation += 1
|
|
155
169
|
if parent: code += f"{self.indent()}super();\n"
|
|
156
|
-
for
|
|
157
|
-
|
|
170
|
+
for assign in assigns:
|
|
171
|
+
code += f"{self.indent()}{assign}\n"
|
|
158
172
|
self.indentation -= 1
|
|
159
173
|
code += f"{self.indent()}}}\n"
|
|
160
174
|
for m in node.methods:
|
shell_lite/lexer.py
CHANGED
|
@@ -195,7 +195,7 @@ class Lexer:
|
|
|
195
195
|
'copy': 'COPY', 'paste': 'PASTE', 'clipboard': 'CLIPBOARD',
|
|
196
196
|
'press': 'PRESS', 'type': 'TYPE', 'click': 'CLICK', 'at': 'AT',
|
|
197
197
|
'notify': 'NOTIFY',
|
|
198
|
-
'date': '
|
|
198
|
+
'date': 'ID', 'today': 'ID', 'after': 'AFTER', 'before': 'BEFORE',
|
|
199
199
|
'list': 'LIST', 'set': 'SET', 'unique': 'UNIQUE', 'of': 'OF',
|
|
200
200
|
'wait': 'WAIT',
|
|
201
201
|
'convert': 'CONVERT', 'json': 'JSON',
|
|
@@ -223,6 +223,9 @@ class Lexer:
|
|
|
223
223
|
'count': 'COUNT', 'many': 'MANY', 'how': 'HOW',
|
|
224
224
|
'field': 'FIELD', 'submit': 'SUBMIT', 'named': 'NAMED',
|
|
225
225
|
'placeholder': 'PLACEHOLDER',
|
|
226
|
+
'app': 'APP', 'title': 'ID', 'size': 'ID',
|
|
227
|
+
'column': 'ID', 'row': 'ID',
|
|
228
|
+
'button': 'ID', 'heading': 'HEADING', 'text': 'ID',
|
|
226
229
|
}
|
|
227
230
|
token_type = keywords.get(value, 'ID')
|
|
228
231
|
self.tokens.append(Token(token_type, value, self.line_number, current_col))
|