suprimedev commited on
Commit
00ea332
·
verified ·
1 Parent(s): e6ea078

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -0
app.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import sys
3
+ import time
4
+ import ast
5
+ import traceback
6
+ from multiprocessing import Process, Queue
7
+
8
+ try:
9
+ import resource # Not available on Windows, but Spaces run Linux.
10
+ except Exception: # pragma: no cover
11
+ resource = None
12
+
13
+ import gradio as gr
14
+
15
+ # -----------------------------
16
+ # Sandbox utilities
17
+ # -----------------------------
18
+ SAFE_BUILTINS = {
19
+ "abs": abs,
20
+ "all": all,
21
+ "any": any,
22
+ "bool": bool,
23
+ "bytes": bytes,
24
+ "callable": callable,
25
+ "chr": chr,
26
+ "complex": complex,
27
+ "dict": dict,
28
+ "dir": dir,
29
+ "divmod": divmod,
30
+ "enumerate": enumerate,
31
+ "filter": filter,
32
+ "float": float,
33
+ "format": format,
34
+ "hash": hash,
35
+ "help": help,
36
+ "hex": hex,
37
+ "int": int,
38
+ "isinstance": isinstance,
39
+ "issubclass": issubclass,
40
+ "iter": iter,
41
+ "len": len,
42
+ "list": list,
43
+ "map": map,
44
+ "max": max,
45
+ "min": min,
46
+ "next": next,
47
+ "object": object,
48
+ "oct": oct,
49
+ "ord": ord,
50
+ "pow": pow,
51
+ "print": print,
52
+ "range": range,
53
+ "repr": repr,
54
+ "reversed": reversed,
55
+ "round": round,
56
+ "set": set,
57
+ "slice": slice,
58
+ "sorted": sorted,
59
+ "str": str,
60
+ "sum": sum,
61
+ "tuple": tuple,
62
+ "type": type,
63
+ "zip": zip,
64
+ }
65
+
66
+ DANGEROUS_NAMES = {
67
+ "open", "compile", "eval", "exec", "__import__", "input",
68
+ "globals", "locals", "vars", "exit", "quit",
69
+ }
70
+
71
+ DANGEROUS_MODULE_TOKENS = {
72
+ "os", "sys", "subprocess", "socket", "shutil", "ctypes", "signal", "resource",
73
+ }
74
+
75
+
76
+ def _lint_code_for_danger(code: str, allow_imports: bool) -> None:
77
+ """Lightweight static checks; raises ValueError on disallowed constructs."""
78
+ try:
79
+ tree = ast.parse(code, mode="exec")
80
+ except SyntaxError as e:
81
+ raise ValueError(f"SyntaxError: {e}")
82
+
83
+ for node in ast.walk(tree):
84
+ # Disallow Attribute or Name usage of dunder or dangerous names
85
+ if isinstance(node, ast.Name):
86
+ if node.id in DANGEROUS_NAMES:
87
+ raise ValueError(f"Use of `{node.id}` is not allowed in this sandbox.")
88
+ if not allow_imports and node.id in DANGEROUS_MODULE_TOKENS:
89
+ raise ValueError(
90
+ f"Importing/using `{node.id}` is not allowed in this sandbox. Enable 'Allow imports' to proceed (unsafe)."
91
+ )
92
+ if isinstance(node, ast.Attribute):
93
+ if node.attr.startswith("__"):
94
+ raise ValueError("Access to dunder attributes is blocked in this sandbox.")
95
+ if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
96
+ if not allow_imports:
97
+ raise ValueError("Imports are disabled. Tick 'Allow imports (unsafe)' to enable.")
98
+
99
+
100
+ def _restricted_import(*args, **kwargs):
101
+ raise ImportError("Importing modules is disabled in this sandbox.")
102
+
103
+
104
+ def _apply_limits(mem_mb: int, cpu_seconds: int):
105
+ if resource is None:
106
+ return
107
+ # Limit address space (virtual memory)
108
+ if mem_mb and mem_mb > 0:
109
+ bytes_limit = int(mem_mb) * 1024 * 1024
110
+ resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
111
+ # Limit CPU seconds
112
+ if cpu_seconds and cpu_seconds > 0:
113
+ resource.setrlimit(resource.RLIMIT_CPU, (cpu_seconds, cpu_seconds))
114
+
115
+
116
+ def _worker(code: str, stdin_data: str, allow_imports: bool, mem_limit_mb: int, cpu_limit_s: int, q: Queue):
117
+ stdout_buf, stderr_buf = io.StringIO(), io.StringIO()
118
+ start = time.time()
119
+ try:
120
+ _apply_limits(mem_limit_mb, cpu_limit_s)
121
+
122
+ # Prepare I/O redirection
123
+ orig_stdout, orig_stderr, orig_stdin = sys.stdout, sys.stderr, sys.stdin
124
+ sys.stdout, sys.stderr, sys.stdin = stdout_buf, stderr_buf, io.StringIO(stdin_data or "")
125
+
126
+ # Lint for dangerous constructs
127
+ _lint_code_for_danger(code, allow_imports)
128
+
129
+ # Prepare restricted globals
130
+ safe_globals = {"__builtins__": SAFE_BUILTINS.copy()}
131
+ if not allow_imports:
132
+ safe_globals["__builtins__"]["__import__"] = _restricted_import
133
+
134
+ # Execute user code
135
+ exec(compile(code, filename="<user_code>", mode="exec"), safe_globals, None)
136
+
137
+ except Exception:
138
+ traceback.print_exc(file=stderr_buf)
139
+ finally:
140
+ # Restore std streams
141
+ try:
142
+ sys.stdout, sys.stderr, sys.stdin = orig_stdout, orig_stderr, orig_stdin
143
+ except Exception:
144
+ pass
145
+ elapsed = time.time() - start
146
+ q.put({
147
+ "stdout": stdout_buf.getvalue(),
148
+ "stderr": stderr_buf.getvalue(),
149
+ "elapsed": elapsed,
150
+ })
151
+
152
+
153
+ def run_code(code: str, stdin_data: str = "", timeout_s: float = 3.0, allow_imports: bool = False, mem_limit_mb: int = 256):
154
+ """Execute code in a separate process with time & memory limits."""
155
+ q: Queue = Queue()
156
+ p = Process(target=_worker, args=(code, stdin_data, allow_imports, int(mem_limit_mb), int(max(1, timeout_s)), q))
157
+ p.start()
158
+ p.join(timeout=timeout_s)
159
+
160
+ if p.is_alive():
161
+ p.terminate()
162
+ try:
163
+ p.join(1)
164
+ except Exception:
165
+ pass
166
+ return "", "Execution timed out.", timeout_s
167
+
168
+ try:
169
+ result = q.get_nowait()
170
+ except Exception:
171
+ return "", "No output captured (possibly killed).", 0.0
172
+
173
+ return result.get("stdout", ""), result.get("stderr", ""), float(result.get("elapsed", 0.0))
174
+
175
+
176
+ # -----------------------------
177
+ # Gradio UI
178
+ # -----------------------------
179
+ EXAMPLE_CODE = """
180
+ # Example: Fibonacci numbers under 200
181
+ fib = [0, 1]
182
+ while fib[-1] + fib[-2] < 200:
183
+ fib.append(fib[-1] + fib[-2])
184
+ print("Fibonacci:", fib)
185
+ """.strip()
186
+
187
+
188
+ def interface_run(code, stdin_text, timeout_s, allow_imports, mem_limit):
189
+ stdout, stderr, elapsed = run_code(code or "", stdin_text or "", timeout_s or 3.0, allow_imports, mem_limit or 256)
190
+ status = f"Finished in {elapsed:.3f}s"
191
+ return stdout, stderr, status
192
+
193
+
194
+ with gr.Blocks(title="Python Code Runner (Sandbox)") as demo:
195
+ gr.Markdown("""
196
+ # 🐍 Python Code Runner (Sandbox)
197
+ A lightweight sandbox to test Python snippets inside your Hugging Face Space.
198
+
199
+ **Notes**
200
+ - Imports are **disabled by default** for safety. You can enable them (unsafe) via the checkbox.
201
+ - CPU and memory limits help protect the Space from heavy or infinite computations.
202
+ - `input()` is disabled; use the *stdin* box instead.
203
+ """)
204
+
205
+ with gr.Row():
206
+ code = gr.Code(language="python", value=EXAMPLE_CODE, lines=18, label="Python code")
207
+ with gr.Row():
208
+ stdin_tb = gr.Textbox(lines=3, label="stdin (optional)")
209
+ with gr.Row():
210
+ timeout = gr.Slider(1, 20, value=3, step=1, label="Time limit (seconds)")
211
+ mem_limit = gr.Slider(64, 2048, value=256, step=64, label="Memory limit (MB)")
212
+ allow_imports = gr.Checkbox(False, label="Allow imports (unsafe)")
213
+ run_btn = gr.Button("Run", variant="primary")
214
+
215
+ with gr.Row():
216
+ stdout = gr.Textbox(lines=12, label="stdout", interactive=False)
217
+ with gr.Row():
218
+ stderr = gr.Textbox(lines=8, label="stderr", interactive=False)
219
+ status = gr.Label(value="Idle", label="Status")
220
+
221
+ run_btn.click(interface_run, inputs=[code, stdin_tb, timeout, allow_imports, mem_limit], outputs=[stdout, stderr, status])
222
+
223
+ gr.Examples(
224
+ examples=[
225
+ ["print('Hello from Space!')"],
226
+ ["for i in range(5):\n print(i*i)"] ,
227
+ ["# Using stdin:\nname = input('Name: ')\nprint('Hi', name)"]
228
+ ],
229
+ inputs=[code],
230
+ label="Quick examples"
231
+ )
232
+
233
+ if __name__ == "__main__":
234
+ demo.launch()