Repository navigation
Expand file tree
/
Copy pathinstance.go
More file actions
294 lines (236 loc) · 8 KB
/
Copy pathinstance.go
File metadata and controls
294 lines (236 loc) · 8 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
package micropython
import (
"context"
"errors"
"maps"
"slices"
"github.com/gregfurman/micropython-go/internal/api"
"github.com/gregfurman/micropython-go/internal/host"
"github.com/gregfurman/micropython-go/internal/value"
)
// HostFunc is a Go function callable from Python through [WithHostFunc] or
// [Instance.DefineFunction]. Errors and panics become Python exceptions;
// use [Raise] to choose the exception class.
// A callback must not synchronously call or close its own Instance.
type HostFunc func(ctx context.Context, args []Value) (Value, error)
var (
// ErrClosed reports a closed interpreter or pool.
ErrClosed = api.ErrClosed
// ErrInterrupted identifies an interrupted call.
ErrInterrupted = api.ErrInterrupted
// ErrInstanceNotInitialised reports an Instance that was not initialized.
ErrInstanceNotInitialised = errors.New("cannot perform operation on Instance that has not been initialised")
)
// Instance is a MicroPython interpreter whose state persists between calls.
// It is safe for concurrent use, but executes one operation at a time.
// Use [Program] or [Instance.Clone] for parallel execution, and call Close when done.
type Instance struct {
wrapped *api.Instance
}
// NewInstance creates an interpreter and applies its initialization options.
// The caller must close it when done.
func NewInstance(ctx context.Context, opts ...Option) (*Instance, error) {
opt := newOptions(opts)
return newInstance(ctx, opt)
}
func newInstance(ctx context.Context, opt *options) (*Instance, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if err := opt.validate(); err != nil {
return nil, err
}
networkConfig, err := opt.network()
if err != nil {
return nil, err
}
in, err := api.New(int32(opt.heapBytes), opt.stdout, networkConfig, opt.filesystem, opt.vars)
if err != nil {
return nil, err
}
for _, key := range slices.Sorted(maps.Keys(opt.globals)) {
if err := in.Set(ctx, key, unwrapAny(opt.globals[key])); err != nil {
return nil, errors.Join(err, in.Close())
}
}
for _, name := range slices.Sorted(maps.Keys(opt.hostFuncs)) {
if err := in.DefineFunction(ctx, name, hostFunc(opt.hostFuncs[name])); err != nil {
return nil, errors.Join(err, in.Close())
}
}
if src := opt.sourceScript; src != "" {
if err := in.Exec(ctx, src); err != nil {
return nil, errors.Join(err, in.Close())
}
}
return &Instance{wrapped: in}, nil
}
// Set binds v to a Python global. It accepts Go values and [Value] builders,
// using the same conversion rules as [Instance.Call].
func (i *Instance) Set(ctx context.Context, name string, v any) error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
return i.wrapped.Set(ctx, name, unwrapAny(v))
}
// Get reads a Python global without evaluating an expression.
// It returns a Python NameError if the name is unbound.
func (i *Instance) Get(ctx context.Context, name string) (Value, error) {
if i.wrapped == nil {
return Value{}, ErrInstanceNotInitialised
}
out, err := i.wrapped.Get(ctx, name)
if err != nil {
return Value{}, err
}
return wrapValue(out), nil
}
// Resolve reads an object handle using the same conversion rules as Eval.
// Containers are copied; cyclic or deeply nested parts remain handles.
// Other objects return another handle to the same object.
func (i *Instance) Resolve(ctx context.Context, v Value) (Value, error) {
if i.wrapped == nil {
return Value{}, ErrInstanceNotInitialised
}
obj, ok := v.val.(value.Object)
if !ok {
return Value{}, conversionError(v, "object")
}
out, err := i.wrapped.Resolve(ctx, obj)
if err != nil {
return Value{}, err
}
return wrapValue(out), nil
}
// Release unpins guest objects, including handles nested in containers.
// It invalidates all handles to each object, even separately acquired ones.
// Non-handles, already-released handles, and foreign handles are ignored.
//
// Release does not run GC or remove Python-owned references. It is optional:
// Go cleanup also queues releases, applied by subsequent interpreter operations.
func (i *Instance) Release(ctx context.Context, vals ...Value) error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
var refs []*value.Ref
for _, val := range vals {
walk(val, func(v Value) bool {
if obj, err := v.AsObject(); err == nil {
refs = append(refs, obj.handle())
}
return true
})
}
if len(refs) == 0 {
return nil
}
return i.wrapped.Release(ctx, refs...)
}
// DefineFunction binds fn to a Python global, replacing any existing binding.
// The binding persists across calls and is inherited by clones.
// See [HostFunc] for callback behavior.
func (i *Instance) DefineFunction(ctx context.Context, name string, fn HostFunc) error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
return i.wrapped.DefineFunction(ctx, name, hostFunc(fn))
}
// hostFunc adapts a public HostFunc to the value model the guest speaks. The
// result is unwrapped here rather than encoded as the Value wrapper, which the
// codec would otherwise take for an ordinary Go struct.
func hostFunc(fn HostFunc) host.HostFunc {
if fn == nil {
return nil
}
return func(ctx context.Context, args []value.Value) (value.Value, error) {
result, err := fn(ctx, wrapValues(args))
if err != nil {
return nil, err
}
return result.val, nil
}
}
// Cancel requests a KeyboardInterrupt in the current Python execution.
// It is safe from any goroutine and does not affect the next operation.
// Interruption is best effort: long C operations and blocking host I/O may delay it.
func (i *Instance) Cancel() error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
i.wrapped.Cancel()
return nil
}
// Call invokes a Python global function. Arguments may be Go values or [Value]
// builders; the result is a Value. Python state changes persist after the call.
func (i *Instance) Call(ctx context.Context, name string, args ...any) (Value, error) {
if i.wrapped == nil {
return Value{}, ErrInstanceNotInitialised
}
out, err := i.wrapped.Call(ctx, name, unwrapArgs(args)...)
if err != nil {
return Value{}, err
}
return wrapValue(out), nil
}
// Clone copies the current Python state into a new, caller-owned Instance.
// It locks the source while copying. Go callbacks, output writers, and filesystem
// backends are shared; guest handles cannot be transferred between instances.
// Close Python sockets, files, and directory iterators before cloning.
func (i *Instance) Clone(ctx context.Context) (*Instance, error) {
if i.wrapped == nil {
return nil, ErrInstanceNotInitialised
}
snap, err := i.wrapped.Snapshot(ctx)
if err != nil {
return nil, err
}
return fromSnapshot(snap)
}
// Close interrupts active execution and closes the interpreter.
// Later execution attempts return [ErrClosed].
func (i *Instance) Close() error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
return i.wrapped.Close()
}
// Eval evaluates a Python expression and returns its Value.
// Use [Instance.Exec] for statements and assignments.
func (i *Instance) Eval(ctx context.Context, expr string) (Value, error) {
if i.wrapped == nil {
return Value{}, ErrInstanceNotInitialised
}
out, err := i.wrapped.Eval(ctx, expr)
if err != nil {
return Value{}, err
}
return wrapValue(out), nil
}
// Exec runs Python statements, preserving their state changes.
// Output goes to [WithStdout], or is discarded by default.
func (i *Instance) Exec(ctx context.Context, src string) error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
return i.wrapped.Exec(ctx, src)
}
// Err returns the interpreter's fatal error or [ErrClosed], or nil if healthy.
func (i *Instance) Err() error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
return i.wrapped.Err()
}
func fromSnapshot(s *api.Snapshot) (*Instance, error) {
instance, err := api.FromSnapshot(s)
if err != nil {
return nil, err
}
return &Instance{wrapped: instance}, nil
}
func (i *Instance) restore(s *api.Snapshot) error {
if i.wrapped == nil {
return ErrInstanceNotInitialised
}
return i.wrapped.Restore(context.Background(), s)
}