-
Notifications
You must be signed in to change notification settings - Fork 414
Expand file tree
/
Copy patherrors.ts
More file actions
336 lines (310 loc) · 10.5 KB
/
Copy patherrors.ts
File metadata and controls
336 lines (310 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// Error classes thrown by the pool client, mirroring pydantic_monty's
// exception hierarchy: MontyError is the base, with MontySyntaxError /
// MontyRuntimeError / MontyTypingError for sandbox failures and
// MontyCrashedError for worker death. The full Python traceback is rendered
// once, in the worker (monty's `MontyException` Display), and carried across
// as a string — never re-implemented here. Structured frames travel alongside
// it for programmatic access via `MontyRuntimeError.traceback()`.
import type { NativeException, NativeFrame } from './native.js'
/** One frame of a Monty traceback. */
export interface Frame {
filename: string
line: number
column: number
endLine: number
endColumn: number
functionName?: string
sourceLine?: string
}
/** Inner Python exception summary. */
export interface ExceptionInfo {
typeName: string
message: string
}
/**
* Base class for all Monty errors. Catching `MontyError` catches every
* failure originating from the sandbox or its worker process.
*/
export class MontyError extends Error {
protected readonly typeName: string
protected readonly innerMessage: string
constructor(typeName: string, message: string) {
super(message ? `${typeName}: ${message}` : typeName)
this.name = 'MontyError'
this.typeName = typeName
this.innerMessage = message
if (Error.captureStackTrace) {
Error.captureStackTrace(this, new.target)
}
}
/** Information about the inner Python exception. */
get exception(): ExceptionInfo {
return { typeName: this.typeName, message: this.innerMessage }
}
/**
* Formats the exception: `'type-msg'` for `ExceptionType: message`,
* `'msg'` (default) for just the message.
*/
display(format: 'type-msg' | 'msg' = 'msg'): string {
switch (format) {
case 'msg':
return this.innerMessage
case 'type-msg':
return this.innerMessage ? `${this.typeName}: ${this.innerMessage}` : this.typeName
default:
throw new Error(`Invalid display format: '${format}'. Expected 'type-msg' or 'msg'`)
}
}
}
/**
* Raised when the fed code cannot be parsed. The inner exception is always a
* `SyntaxError`.
*/
export class MontySyntaxError extends MontyError {
private readonly tracebackText: string
constructor(message: string, tracebackText = '') {
super('SyntaxError', message)
this.name = 'MontySyntaxError'
this.tracebackText = tracebackText
}
/**
* Formats the exception; `'traceback'` returns the worker-rendered traceback
* (including the source-location frame CPython shows for syntax errors),
* falling back to the `type-msg` summary when none was supplied.
*/
override display(format: 'traceback' | 'type-msg' | 'msg' = 'msg'): string {
if (format === 'traceback') {
return this.tracebackText || super.display('type-msg')
}
return super.display(format)
}
}
/**
* Raised when sandbox code fails during execution. The session survives — the
* worker keeps its globals and later feeds still work.
*/
export class MontyRuntimeError extends MontyError {
private readonly frames: NativeFrame[]
private readonly tracebackText: string
constructor(typeName: string, message: string, frames: NativeFrame[] = [], tracebackText = '') {
super(typeName, message)
this.name = 'MontyRuntimeError'
this.frames = frames
this.tracebackText = tracebackText
}
/** The Monty traceback, outermost frame first. */
traceback(): Frame[] {
return this.frames.map((f) => ({
filename: f.filename,
line: f.line,
column: f.column,
endLine: f.endLine,
endColumn: f.endColumn,
...(f.frameName !== undefined ? { functionName: f.frameName } : {}),
...(f.previewLine !== undefined ? { sourceLine: f.previewLine } : {}),
}))
}
/**
* Formats the exception: `'traceback'` (default) returns the full Python
* traceback rendered by the worker, `'type-msg'` / `'msg'` the summary
* forms.
*/
override display(format: 'traceback' | 'type-msg' | 'msg' = 'traceback'): string {
if (format === 'traceback') {
return this.tracebackText || super.display('type-msg')
}
return super.display(format)
}
}
/**
* Raised when type checking rejects a fed snippet (sessions created with
* `typeCheck: true`). The snippet was not executed and the session survives.
*
* Diagnostics are rendered inside the worker; `display()` returns them
* verbatim, one per line.
*/
export class MontyTypingError extends MontyError {
private readonly diagnostics: string
constructor(diagnostics: string) {
const first = diagnostics.split('\n', 1)[0] ?? ''
super('TypeError', first)
this.name = 'MontyTypingError'
this.diagnostics = diagnostics
}
/** The rendered type-checking diagnostics, one per line. */
override display(): string {
return this.diagnostics
}
}
/**
* Raised when a worker process died: a hard crash (segfault, allocator abort
* — the failure mode subprocess isolation exists to contain) or a watchdog
* kill for exceeding `requestTimeout` / the `maxDurationSecs` backstop. The
* session is lost; the pool replaces the worker, so other sessions and future
* checkouts are unaffected.
*/
export class MontyCrashedError extends MontyError {
/** True when the worker was killed by a watchdog timeout. */
readonly timedOut: boolean
/** Worker exit description (e.g. `signal: 9 (SIGKILL)`), when known. */
readonly exitStatus: string | null
constructor(message: string, options: { timedOut?: boolean; exitStatus?: string | null } = {}) {
super('RuntimeError', message)
this.name = 'MontyCrashedError'
this.timedOut = options.timedOut ?? false
this.exitStatus = options.exitStatus ?? null
}
}
/**
* Raised when the worker (or a caller misusing the session) violated the
* wire protocol. The worker has been discarded; the session is lost.
*/
export class ProtocolError extends Error {
constructor(message: string) {
super(message)
this.name = 'ProtocolError'
}
}
/**
* Every exception type name monty's `ExcType` can parse (the native binding
* parses the name; unknown names fall back to `RuntimeError`). Kept in
* lockstep with `ExcType` in crates/monty/src/exception_private.rs.
*/
export const PYTHON_EXC_NAMES: ReadonlySet<string> = new Set([
'Exception',
'BaseException',
'SystemExit',
'KeyboardInterrupt',
'ArithmeticError',
'OverflowError',
'ZeroDivisionError',
'LookupError',
'IndexError',
'KeyError',
'RuntimeError',
'NotImplementedError',
'RecursionError',
'AttributeError',
'FrozenInstanceError',
'NameError',
'UnboundLocalError',
'ValueError',
'UnicodeDecodeError',
'UnicodeEncodeError',
'json.JSONDecodeError',
'ImportError',
'ModuleNotFoundError',
'OSError',
'FileNotFoundError',
'FileExistsError',
'IsADirectoryError',
'NotADirectoryError',
'PermissionError',
'io.UnsupportedOperation',
'AssertionError',
'MemoryError',
'StopIteration',
'SyntaxError',
'TimeoutError',
'TypeError',
're.PatternError',
// decimal module exception taxonomy — every `decimal.*` class monty's
// `ExcType` can parse. Without these, a host error named e.g.
// `decimal.Underflow` would fall back to `RuntimeError` and `except
// decimal.Underflow:` in the sandbox would not catch it.
'decimal.DecimalException',
'decimal.InvalidOperation',
'decimal.DivisionByZero',
'decimal.Overflow',
'decimal.Inexact',
'decimal.Rounded',
'decimal.Subnormal',
'decimal.Clamped',
'decimal.Underflow',
'decimal.FloatOperation',
'decimal.ConversionSyntax',
'decimal.DivisionImpossible',
'decimal.DivisionUndefined',
'decimal.InvalidContext',
])
/**
* Maps a native exception to the matching error class: `SyntaxError` is a
* parse failure, everything else a runtime exception.
*/
export function montyErrorFromNative(exc: NativeException): MontySyntaxError | MontyRuntimeError {
if (exc.excType === 'SyntaxError') {
return new MontySyntaxError(exc.message, exc.traceback)
}
return new MontyRuntimeError(exc.excType, exc.message, exc.frames, exc.traceback)
}
/**
* CPython-style `TypeError` message for calling a non-callable
* `externalLookup` entry — reachable when a cached function proxy's entry is
* later replaced by a plain value. Mirrors what CPython raises when calling
* that value, matching the Python binding (which really calls the entry).
*/
export function notCallableMessage(value: unknown): string {
return `'${pyTypeName(value)}' object is not callable`
}
/**
* `__monty_type__` marker → the Python type its conversion produces. `Type`
* and `BuiltinFunction` cannot round-trip and convert to reprs; an unknown
* marker converts as a plain dict.
*/
const MARKED_TYPE_NAMES: Readonly<Record<string, string>> = {
Ellipsis: 'ellipsis',
Exception: 'Exception',
Date: 'date',
DateTime: 'datetime',
TimeDelta: 'timedelta',
TimeZone: 'timezone',
Type: 'repr',
BuiltinFunction: 'repr',
Dataclass: 'dataclass',
}
/** Python type name the JS value converts to (mirrors the Rust `js_to_monty`). */
function pyTypeName(value: unknown): string {
if (value === null || value === undefined) {
return 'NoneType'
}
switch (typeof value) {
case 'boolean':
return 'bool'
case 'number':
return Number.isInteger(value) ? 'int' : 'float'
case 'bigint':
return 'int'
case 'string':
return 'str'
case 'function':
return 'function'
case 'object': {
if (value instanceof Uint8Array) return 'bytes'
if (value instanceof Map) return 'dict'
if (value instanceof Set) return 'set'
if (Array.isArray(value)) {
return readMarker(value, '__tuple__') ? 'tuple' : 'list'
}
const marker = readMarker(value, '__monty_type__')
return typeof marker === 'string' ? (MARKED_TYPE_NAMES[marker] ?? 'dict') : 'dict'
}
default:
// symbols and other exotic values have no Monty equivalent
return 'object'
}
}
/**
* Reads a marker property off a host-provided value without letting a throwing
* getter or `Proxy` trap escape. `pyTypeName` runs *while formatting a
* TypeError message*, and the drive loop treats any throw from a call handler as
* fatal (it marks the session broken), so an exotic `externalLookup` entry must
* still degrade to a plain type rather than poison the turn — mirroring the Rust
* `js_to_monty`, which falls back to `object`/`dict` on any conversion failure.
*/
function readMarker(value: object, key: '__tuple__' | '__monty_type__'): unknown {
try {
return (value as Record<string, unknown>)[key]
} catch {
return undefined
}
}