stackvm 0.1.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.
stackvm/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from ._api import StackMachine as StackMachine
2
+
3
+ __all__ = ["StackMachine"]
stackvm/__main__.py ADDED
@@ -0,0 +1,173 @@
1
+ import ast
2
+ import tkinter as tk
3
+ from importlib import resources, util
4
+ from pathlib import Path
5
+ from tkinter import filedialog, messagebox, simpledialog, ttk
6
+
7
+ from stackvm._api import StackMachine
8
+ from stackvm._util import OpCode
9
+
10
+
11
+ def get_machine_from_python(file: str, root: tk.Tk) -> StackMachine | None:
12
+ result = messagebox.askokcancel(
13
+ title="Warning",
14
+ message="Python code contained within the selected file will be executed.\nContinue?",
15
+ icon="warning",
16
+ )
17
+
18
+ if not result:
19
+ return None
20
+
21
+ spec = util.spec_from_file_location("<stackvm>", file)
22
+
23
+ if spec is None:
24
+ messagebox.showerror(title="Error", message="Failed to load Python file")
25
+ return None
26
+
27
+ module = util.module_from_spec(spec)
28
+ loader = spec.loader
29
+
30
+ if loader is None:
31
+ messagebox.showerror(title="Error", message="Failed to load Python file")
32
+ return None
33
+
34
+ try:
35
+ loader.exec_module(module)
36
+ except BaseException as e: # noqa: BLE001
37
+ messagebox.showerror(
38
+ title="Error",
39
+ message="Exception raised when importing module",
40
+ detail=f"{type(e).__name__}: {e}",
41
+ )
42
+ return None
43
+
44
+ candidates = {name: machine for name, machine in vars(module).items() if isinstance(machine, StackMachine)}
45
+
46
+ if not candidates:
47
+ messagebox.showerror(title="Error", message="No instances of StackMachine found in module globals")
48
+ return None
49
+
50
+ if len(candidates) >= 2:
51
+ options = sorted(candidates)
52
+
53
+ dialog = tk.Toplevel(root)
54
+ dialog.title("Input")
55
+
56
+ dialog.resizable(False, False)
57
+
58
+ choice = tk.StringVar(value=options[0])
59
+
60
+ tk.Label(dialog, text="Select stack machine to execute:").pack()
61
+
62
+ ttk.Combobox(dialog, textvariable=choice, values=options, state="readonly").pack(padx=20, pady=10)
63
+
64
+ selection: str = ""
65
+
66
+ def submit():
67
+ nonlocal selection
68
+ selection = choice.get()
69
+ dialog.destroy()
70
+
71
+ ttk.Button(dialog, text="Select", command=submit).pack(pady=10)
72
+
73
+ dialog.grab_set()
74
+ root.wait_window(dialog)
75
+
76
+ return candidates[selection]
77
+ else:
78
+ return next(iter(candidates.values()))
79
+
80
+
81
+ def main():
82
+ root = tk.Tk()
83
+
84
+ icon = resources.files("stackvm").joinpath("resources", "icon.ico")
85
+
86
+ with resources.as_file(icon) as path:
87
+ root.iconbitmap(path)
88
+
89
+ try:
90
+ root.withdraw()
91
+
92
+ file = filedialog.askopenfilename(
93
+ title="Select a file...",
94
+ filetypes=[
95
+ ("StackVM files", "*.bin *.svm"),
96
+ ("Python files", "*.py"),
97
+ ("All files", "*.*"),
98
+ ],
99
+ )
100
+
101
+ if not file:
102
+ return
103
+
104
+ path = Path(file)
105
+
106
+ if path.suffix == ".py":
107
+ machine = get_machine_from_python(file, root)
108
+
109
+ if machine is None:
110
+ return
111
+ else:
112
+ machine = StackMachine.from_bytes(path.read_bytes())
113
+
114
+ if any(instr.opcode is OpCode.OPEN for instr in machine.instructions):
115
+ result = messagebox.askyesno(
116
+ title="Warning",
117
+ message="This program performs file I/O. Continue?",
118
+ icon="warning",
119
+ )
120
+ if not result:
121
+ return
122
+
123
+ arg_input = simpledialog.askstring(
124
+ title="Input",
125
+ prompt="Enter initial arguments, separated by commas. Use quotes to indicate characters.",
126
+ )
127
+
128
+ if not arg_input:
129
+ arg_input = "()"
130
+
131
+ try:
132
+ args = ast.literal_eval(arg_input)
133
+ except (ValueError, SyntaxError):
134
+ messagebox.showerror(title="Error", message="Could not parse arguments")
135
+ return
136
+
137
+ if not isinstance(args, tuple):
138
+ args = (args,)
139
+
140
+ for arg in args:
141
+ if not (isinstance(arg, None | int | bool) or isinstance(arg, str) and len(arg) <= 1):
142
+ messagebox.showerror(
143
+ title="Error",
144
+ message=f"Disallowed argument {arg} (only None, int, bool, and chr are permitted)",
145
+ )
146
+ return
147
+
148
+ if any(instr.opcode in {OpCode.OFTEN, OpCode.SELDOM} for instr in machine.instructions):
149
+ seed_input = simpledialog.askstring(
150
+ title="Input",
151
+ prompt="Enter a seed (leave blank for random):",
152
+ )
153
+
154
+ if seed_input is None:
155
+ return
156
+
157
+ if not seed_input:
158
+ seed = None
159
+ else:
160
+ try:
161
+ seed = int(seed_input)
162
+ except ValueError:
163
+ seed = seed_input
164
+ else:
165
+ seed = None
166
+ finally:
167
+ root.destroy()
168
+
169
+ machine.ui(*args, seed=seed)
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()