1use crate::glue::{PyGlue, placed, python_arguments};
24use pyo3::prelude::*;
25use pyo3::types::PyDict;
26use quarb::Value;
27use quarb::library::{InputSlot, LibCall, LibFailure, OperandSpec, OptionSpec, Pipe, Synopsis};
28use std::collections::HashMap;
29use std::sync::{Mutex, OnceLock};
30
31const GLUE: &str = r#"
32import contextlib, importlib, inspect, math, sys
33import quarb_foreign as _fg
34
35# What a method returns, where the signature does not say and the
36# next segment of a path needs it: pandas' intermediate objects,
37# the estimators that return themselves.
38_RETURNS = {
39 "pandas": {
40 "groupby": "pandas.core.groupby.generic.DataFrameGroupBy",
41 "rolling": "pandas.core.window.rolling.Rolling",
42 "expanding": "pandas.core.window.expanding.Expanding",
43 "ewm": "pandas.core.window.ewm.ExponentialMovingWindow",
44 "resample": "pandas.core.resample.Resampler",
45 },
46 "sklearn": {"fit": "self", "set_params": "self", "partial_fit": "self", "set_output": "self"},
47}
48
49def _import(name):
50 try:
51 return importlib.import_module(name)
52 except ImportError:
53 return None
54
55def _attr(cur, name):
56 try:
57 return getattr(cur, name)
58 except AttributeError:
59 if inspect.ismodule(cur):
60 m = _import(cur.__name__ + "." + name)
61 if m is not None:
62 return m
63 raise
64
65def _class_of(path):
66 mod, _, name = path.rpartition(".")
67 return getattr(importlib.import_module(mod), name)
68
69def _qual(o):
70 if inspect.ismodule(o):
71 return o.__name__
72 if inspect.isclass(o):
73 return o.__qualname__
74 return type(o).__qualname__
75
76def _nearest(o, name, limit=6):
77 import difflib
78 pool = [n for n in dir(o) if not n.startswith("_")]
79 close = difflib.get_close_matches(name, pool, n=limit, cutoff=0.6)
80 return f"; nearest: {', '.join(close)}" if close else ""
81
82def _routine(a):
83 """A function, method or ufunc: a segment that is called. A
84 callable instance (`stats.norm`) is an attribute, walked into."""
85 return inspect.isroutine(a) or (type(a).__module__ == "numpy" and type(a).__name__ == "ufunc")
86
87# A library command is pure (spec: Command Libraries): a reflected
88# name that reads or writes outside the query is withheld, by its
89# module (the packages' I/O trees), its name, or a parameter that
90# takes a file, a buffer or a connection.
91_IO_MODULES = {
92 "io", "_io", "npyio", "_npyio_impl", "_datasource", "memmap", "_memmap",
93 "datasets", "_datasets", "externals", "pickle", "_pickle", "pytables",
94 "clipboard", "sql", "parsers", "excel", "json", "formats", "feather_format",
95 "parquet", "orc", "stata", "sas", "spss", "xml", "html", "gbq",
96}
97_IO_NAMES = ("read_", "load", "save", "fetch", "dump", "open", "tofile", "fromfile",
98 "memmap", "to_clipboard", "to_sql", "to_gbq")
99_IO_PARAMS = {
100 "path", "path_or_buf", "path_or_buffer", "filepath_or_buffer", "file", "fname",
101 "filename", "file_name", "fid", "buf", "excel_writer", "con", "url", "io",
102 "data_home", "destination_table",
103}
104
105class Withheld(AttributeError):
106 pass
107
108def _withheld(a, name):
109 """Why `a` (reached as `name`) is withheld, or None."""
110 mod = a.__name__ if inspect.ismodule(a) else (getattr(a, "__module__", None) or "")
111 if any(p in _IO_MODULES for p in mod.split(".")[1:]):
112 return f"its module {mod}"
113 if name.startswith(_IO_NAMES):
114 return "its name"
115 if _routine(a) or inspect.isclass(a):
116 ps, _ = _params(a, False)
117 if ps and any(p.name in _IO_PARAMS for p in ps):
118 return "a file or connection parameter"
119 return None
120
121def _withhold(a, name, path, root):
122 why = _withheld(a, name)
123 if why is not None:
124 qual = f"{root}.{path}" if not path.startswith(root) else path
125 raise Withheld(f"'{path}' is withheld: {qual} reads or writes outside the query ({why}); a library command is pure, a file is a mount")
126
127def _params(f, drop_self):
128 try:
129 sig = inspect.signature(f)
130 except (TypeError, ValueError):
131 return None, None
132 ps = list(sig.parameters.values())
133 if drop_self and ps and ps[0].name in ("self", "cls"):
134 ps = ps[1:]
135 return ps, sig.return_annotation
136
137class Seg:
138 __slots__ = ("name", "kind", "params", "obj")
139
140def _plan(root, classes, path, dynamic):
141 """Resolve a dotted name from the root package, or from one of
142 the method classes, as far as the signatures allow. Where the
143 receiver's attributes are its data (`dynamic`: a frame's columns),
144 a name found nowhere is the receiver's, resolved during the run."""
145 rootmod = importlib.import_module(root)
146 names = path.split(".")
147 start = "module"
148 cur = rootmod
149 standing = False # cur is a class standing for the receiver's type
150 try:
151 _attr(rootmod, names[0])
152 except AttributeError:
153 for c in classes:
154 cls = _class_of(c)
155 if hasattr(cls, names[0]):
156 cur, standing, start = cls, True, "input"
157 break
158 else:
159 if dynamic:
160 cur, start = None, "input"
161 else:
162 where = f"{root} and {', '.join(_class_of(c).__name__ for c in classes)} have" if classes else f"{root} has"
163 raise AttributeError(f"{where} no '{names[0]}'{_nearest(rootmod, names[0])}")
164 segs = []
165 for n in names:
166 s = Seg(); s.name = n; s.params = None; s.obj = None
167 if cur is None:
168 s.kind = "unknown"; segs.append(s); continue
169 try:
170 a = _attr(cur, n)
171 except AttributeError:
172 if standing:
173 # An attribute of the result (a fitted model's
174 # `components_`): resolved during the run.
175 s.kind = "unknown"; cur = None; segs.append(s); continue
176 raise AttributeError(f"{_qual(cur)} has no '{n}'{_nearest(cur, n)}")
177 s.obj = a
178 _withhold(a, n, path, root)
179 if inspect.ismodule(a):
180 s.kind = "module"; cur = a; standing = False
181 elif inspect.isclass(a):
182 s.kind = "class"; s.params, _ = _params(a, False); cur = a; standing = True
183 elif _routine(a):
184 unbound = standing and inspect.isclass(cur)
185 s.kind = "method" if unbound else "function"
186 s.params, ret = _params(a, unbound)
187 nxt = None
188 r = _RETURNS.get(root, {}).get(n)
189 if r == "self":
190 nxt = cur if inspect.isclass(cur) else type(cur)
191 elif r:
192 nxt = _class_of(r)
193 elif ret in ("Self", "'Self'") and inspect.isclass(cur):
194 nxt = cur
195 elif inspect.isclass(ret):
196 nxt = ret
197 cur, standing = nxt, nxt is not None
198 else:
199 s.kind = "attr"
200 if standing:
201 # A descriptor on the class: the instance's value is
202 # the run's (`DataFrame.index`).
203 cur, standing = None, False
204 else:
205 cur = a
206 segs.append(s)
207 return start, segs
208
209def _first_line(o):
210 doc = inspect.getdoc(o) if o is not None else None
211 if not doc:
212 return ""
213 line = doc.strip().splitlines()[0].strip()
214 return line.rstrip(".")
215
216def _synopsis(root, classes, path, dynamic):
217 start, segs = _plan(root, classes, path, dynamic)
218 operands, options, seen = [], [], set()
219 variadic = open_ = False
220 first = True
221 if start == "input":
222 operands.append(("self", "the input: the receiver of the method", True))
223 for s in segs:
224 if s.kind in ("class", "function", "method"):
225 if s.params is None:
226 variadic = open_ = True
227 first = False
228 continue
229 for p in s.params:
230 if p.kind == p.VAR_POSITIONAL:
231 variadic = True
232 elif p.kind == p.VAR_KEYWORD:
233 open_ = True
234 elif p.default is p.empty and p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD):
235 operands.append((p.name, "", True))
236 elif p.kind == p.POSITIONAL_ONLY:
237 if first:
238 variadic = True
239 else:
240 if p.kind == p.POSITIONAL_OR_KEYWORD and first:
241 variadic = True
242 long = p.name.replace("_", "-")
243 if long not in seen:
244 seen.add(long)
245 d = p.default
246 shown = "" if d is p.empty or repr(d).startswith("<") else repr(d)
247 options.append((long, f"default {shown}" if shown else ""))
248 first = False
249 elif s.kind == "unknown":
250 variadic = open_ = True
251 last = segs[-1]
252 if last.kind == "module":
253 raise AttributeError(
254 f"'{path}' is a module of {root}, not a command: name a function or a class in it ({path}.<name>)")
255 doc = _first_line(last.obj)
256 returns = ""
257 if last.kind in ("function", "method") and last.params is not None:
258 try:
259 ret = inspect.signature(last.obj).return_annotation
260 if ret is not inspect.Signature.empty:
261 returns = ret if isinstance(ret, str) else getattr(ret, "__name__", str(ret))
262 except (TypeError, ValueError):
263 pass
264 elif last.kind == "class":
265 returns = last.obj.__name__
266 # The slot the input fills: the receiver of a method; else the
267 # first required parameter named as data (`X` in
268 # `cross_val_score(estimator, X, y)`), else the first required.
269 input_at = 0
270 if start != "input":
271 input_at = next((i for i, (n, _, _) in enumerate(operands) if n in _DATA_NAMES), 0)
272 # A path that opens with a class may take the input as the
273 # instance itself (a fitted estimator before `.predict(X)`).
274 calls = [s for s in segs if s.kind in ("class", "function", "method", "unknown")]
275 instance = start != "input" and bool(calls) and calls[0].kind == "class"
276 return {"operands": operands, "options": options, "variadic": variadic,
277 "open": open_, "doc": doc, "returns": returns.strip("'"),
278 "input": input_at, "instance": instance}
279
280def _numeric(v):
281 # A boolean reads 1 for true, 0 for false (the table rule), so a
282 # boolean field is a column.
283 if isinstance(v, bool):
284 return 1.0 if v else 0.0
285 if isinstance(v, (int, float)):
286 return float(v)
287 if hasattr(v, "magnitude"):
288 return float(v.magnitude)
289 if isinstance(v, str):
290 try:
291 return float(v.replace(",", ""))
292 except ValueError:
293 return None
294 return None
295
296def _read(v):
297 """A value by its numeric reading (the table rule): numeric text
298 as the number it reads as (an integer form exact), a quantity by
299 its magnitude, a boolean as 1 or 0, anything else as it is."""
300 if isinstance(v, bool):
301 return 1 if v else 0
302 if isinstance(v, str):
303 t = v.strip()
304 try:
305 return int(t)
306 except ValueError:
307 pass
308 try:
309 return float(t)
310 except ValueError:
311 return v
312 if hasattr(v, "magnitude"):
313 return v.magnitude
314 return v
315
316def _isnum(v):
317 return isinstance(v, (int, float)) and not isinstance(v, bool)
318
319_scalar = _read
320
321def _array(x):
322 import numpy as np
323 import datetime as dt
324 if isinstance(x, list):
325 if x and all(isinstance(v, dt.datetime) for v in x):
326 return np.array([v.replace(tzinfo=None) if v.tzinfo else v for v in x], dtype="datetime64[ns]")
327 if x and all(isinstance(v, dt.timedelta) for v in x):
328 return np.array(x, dtype="timedelta64[ns]")
329 if x and all(isinstance(v, list) for v in x):
330 return np.array([_array(v) for v in x])
331 read = [_read(v) for v in x]
332 if read and all(v is None or _isnum(v) for v in read):
333 if any(v is None for v in read):
334 return np.array([np.nan if v is None else float(v) for v in read], dtype=float)
335 return np.array(read)
336 return np.array(read)
337 return x
338
339def _matrix(records):
340 """The table rule for an array: the numeric fields of the first
341 record are the columns, the first text field the label (dropped:
342 an array has no names; pd: keeps them)."""
343 import numpy as np
344 first = records[0]
345 cols = [k for k, v in first.items() if _numeric(v) is not None]
346 label = next((k for k, v in first.items() if isinstance(v, str) and _numeric(v) is None), None)
347 if not cols:
348 raise TypeError("no numeric field in the first record; nothing to make a matrix of")
349 out = []
350 for i, r in enumerate(records):
351 row = []
352 for c in cols:
353 v = _numeric(r.get(c))
354 if v is None:
355 who = r.get(label, i + 1) if label else i + 1
356 raise TypeError(f"row {who}: field '{c}' is {'null (a hole in the matrix)' if r.get(c) is None else 'not numeric'}")
357 row.append(v)
358 out.append(row)
359 return np.array(out, dtype=float)
360
361def _series(values, index=None):
362 """A column: numeric when every non-null value reads as a number
363 (the table rule), text otherwise. A null crosses as pd.NA in a
364 nullable dtype (Float64, Int64, boolean, string), never as NaN,
365 so it comes back as null; a nan is a float like any other."""
366 import pandas as pd
367 read = [_read(v) for v in values]
368 present = [v for v in read if v is not None]
369 if any(v is None for v in read):
370 if present and all(_isnum(v) for v in present):
371 dtype = "Int64" if all(isinstance(v, int) for v in present) else "Float64"
372 return pd.array([pd.NA if v is None else v for v in read], dtype=dtype)
373 return pd.array([pd.NA if v is None else str(v) for v in read], dtype="string")
374 if present and all(_isnum(v) for v in present):
375 return pd.Series(read, index=index)
376 return pd.Series(read, index=index, dtype=object) if index is not None else pd.Series(read, dtype=object)
377
378def _numeric_columns(df):
379 """Every object column through `_series`: numeric readings,
380 nulls as pd.NA in the nullable dtypes."""
381 import pandas as pd
382 for c in df.columns:
383 col = df[c]
384 # a boolean column reads 1 and 0 (the table rule)
385 if str(col.dtype) == "bool":
386 df[c] = col.astype("int64")
387 continue
388 if str(col.dtype) == "boolean":
389 df[c] = col.astype("Int64")
390 continue
391 if col.dtype != object:
392 continue
393 s = _series(col.tolist())
394 df[c] = s if not isinstance(s, pd.Series) else s.values
395 return df
396
397def _frame(x):
398 import pandas as pd
399 if isinstance(x, list):
400 if x and all(isinstance(r, dict) for r in x):
401 df = pd.DataFrame(x)
402 label = next((k for k, v in x[0].items() if isinstance(v, str) and _numeric(v) is None), None)
403 if label is not None and label in df.columns:
404 df = df.set_index(label)
405 return _numeric_columns(df)
406 if x and all(isinstance(r, list) for r in x):
407 return _numeric_columns(pd.DataFrame([[_read(v) for v in r] for r in x]))
408 s = _series(x)
409 return s if isinstance(s, pd.Series) else pd.Series(s)
410 if isinstance(x, dict):
411 return pd.Series({k: _read(v) for k, v in x.items()})
412 return x
413
414def _construct(x, mode):
415 if mode == "none" or not isinstance(x, (list, dict)):
416 return x
417 # A record is a one-row table by the table rule (the user's
418 # ruling on constructors as operands): a frame of one row, or
419 # the matrix of its numeric fields.
420 if isinstance(x, dict):
421 x = [x]
422 if mode == "frame":
423 return _frame(x)
424 if x and all(isinstance(r, dict) for r in x):
425 return _matrix(x)
426 return _array(x)
427
428_DATA_NAMES = {"data", "object", "a", "x", "X", "values", "arr", "obj", "shape"}
429
430def _required(s):
431 if s.params is None:
432 return 0
433 return sum(1 for p in s.params if p.default is p.empty and p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD))
434
435_plans = {}
436
437def _first_positional(s):
438 if not s.params:
439 return None
440 return next((p for p in s.params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)), None)
441
442def _takes_data(s):
443 """Whether a class's constructor takes data first (`DataFrame(data)`,
444 `ndarray(shape)`); an estimator's (`GaussianNB(priors)`) does not."""
445 first = _first_positional(s)
446 return s.params is None or (first is not None and first.name in _DATA_NAMES)
447
448def _call(root, classes, path, construct, dynamic, args, kwargs, input_at=None):
449 with contextlib.redirect_stdout(sys.stderr):
450 key = (root, tuple(classes), path)
451 if key not in _plans:
452 _plans[key] = _plan(root, classes, path, dynamic)
453 start, segs = _plans[key]
454 # Every operand keeps the kind its expression yields and a list
455 # is read as the input is (the table rule): a list of records
456 # a matrix or frame, numeric readings a vector, a library
457 # value itself.
458 queue = [_construct(a, construct) for a in args]
459 # Where the input stands in the queue, followed through the
460 # pops; None once a call has taken it.
461 def taken(k):
462 nonlocal input_at
463 if input_at is not None:
464 input_at = None if input_at < k else input_at - k
465 calls = [s for s in segs if s.kind in ("class", "function", "method", "unknown")]
466 receiver = None
467 if start == "input":
468 if not queue:
469 raise TypeError(f"{path}: the receiver is missing (nothing on the pipe)")
470 receiver = queue.pop(0)
471 taken(1)
472 pos = {id(s): [] for s in segs}
473 instance = None
474 if calls and calls[0].kind == "class" and queue:
475 cls = calls[0].obj
476 if inspect.isclass(cls) and isinstance(queue[0], cls):
477 # The input is an instance of the class on the path (a
478 # fitted estimator, a frame): the receiver of its
479 # methods, not constructed again.
480 instance = queue.pop(0)
481 taken(1)
482 else:
483 # A class first on the path whose first parameter is its
484 # data (`pd:Series.rolling.mean(12)`, `np:ndarray`) is
485 # constructed from the input; an estimator's constructor
486 # (KMeans(n_clusters)) is not, and the input reaches its
487 # method.
488 first = _first_positional(calls[0])
489 if first is not None and first.name in _DATA_NAMES and first.default is not first.empty:
490 pos[id(calls[0])].append(queue.pop(0))
491 taken(1)
492 for s in calls:
493 need = 0 if (instance is not None and s is calls[0]) else _required(s)
494 pos[id(s)], queue = pos[id(s)] + queue[:need], queue[need:]
495 taken(need)
496 if queue:
497 if not calls:
498 raise TypeError(f"{path}: {len(queue)} operand(s) given, none taken (an attribute)")
499 # The input passing a bare constructor that takes no data
500 # (an estimator, built from its options alone) is not
501 # handed to it: the instance is the result.
502 if (input_at is not None and input_at < len(queue) and calls[0].kind == "class"
503 and instance is None and not _takes_data(calls[0])):
504 del queue[input_at]
505 pos[id(calls[0])] += queue
506 kw = {id(s): {} for s in segs}
507 for k, v in kwargs.items():
508 target = None
509 for s in calls:
510 if s.params is None or any(p.name == k for p in s.params):
511 target = s
512 break
513 if target is None:
514 for s in calls:
515 if s.params is not None and any(p.kind == p.VAR_KEYWORD for p in s.params):
516 target = s
517 break
518 if target is None:
519 raise TypeError(f"{path}: no parameter named '{k.replace('_', '-')}'")
520 kw[id(target)][k] = v
521 cur = receiver if start == "input" else importlib.import_module(root)
522 for s in segs:
523 a = _attr(cur, s.name)
524 if s.kind == "unknown":
525 _withhold(a, s.name, path, root)
526 if s.kind == "module":
527 cur = a
528 elif s.kind == "class" and instance is not None and s is calls[0]:
529 cur = instance
530 elif s.kind in ("class", "function", "method") or (s.kind == "unknown" and callable(a)):
531 cur = a(*pos[id(s)], **kw[id(s)])
532 elif pos[id(s)] or kw[id(s)]:
533 # An attribute given arguments: a callable instance
534 # (`stats.norm(0; 1)` freezes a distribution).
535 if not callable(a):
536 raise TypeError(f"{path}: '{s.name}' is an attribute and takes no arguments")
537 cur = a(*pos[id(s)], **kw[id(s)])
538 else:
539 cur = a
540 return cur
541"#;
542
543static PY: PyGlue = PyGlue::new(GLUE, "quarb_reflect.py", "quarb_reflect");
544
545pub struct Reflective {
550 root: &'static str,
551 classes: &'static [&'static str],
552 construct: &'static str,
553 dynamic: bool,
557 cache: OnceLock<Mutex<HashMap<String, &'static Synopsis>>>,
558}
559
560fn leak(s: String) -> &'static str {
561 Box::leak(s.into_boxed_str())
562}
563
564impl Reflective {
565 pub const fn new(
566 root: &'static str,
567 classes: &'static [&'static str],
568 construct: &'static str,
569 dynamic: bool,
570 ) -> Reflective {
571 Reflective {
572 root,
573 classes,
574 construct,
575 dynamic,
576 cache: OnceLock::new(),
577 }
578 }
579
580 pub fn root(&self) -> &'static str {
582 self.root
583 }
584
585 pub fn resolve(&self, name: &str) -> Result<&'static Synopsis, String> {
589 let cache = self.cache.get_or_init(|| Mutex::new(HashMap::new()));
590 if let Some(s) = cache.lock().expect("synopsis cache").get(name) {
591 return Ok(s);
592 }
593 let syn = Python::attach(|py| -> Result<Synopsis, String> {
594 let m = PY.module(py).map_err(|e| e.to_string())?;
595 let d = m
596 .getattr("_synopsis")
597 .and_then(|f| f.call1((self.root, self.classes, name, self.dynamic)))
598 .map_err(|e| e.value(py).to_string())?;
599 let d = d.cast::<PyDict>().map_err(|e| e.to_string())?;
600 let get = |k: &str| d.get_item(k).ok().flatten();
601 let operands: Vec<(String, String, bool)> = get("operands")
602 .and_then(|v| v.extract().ok())
603 .unwrap_or_default();
604 let options: Vec<(String, String)> = get("options")
605 .and_then(|v| v.extract().ok())
606 .unwrap_or_default();
607 let variadic: bool = get("variadic").and_then(|v| v.extract().ok()).unwrap_or(false);
608 let open: bool = get("open").and_then(|v| v.extract().ok()).unwrap_or(false);
609 let doc: String = get("doc").and_then(|v| v.extract().ok()).unwrap_or_default();
610 let returns: String = get("returns").and_then(|v| v.extract().ok()).unwrap_or_default();
611 let input_at: usize = get("input").and_then(|v| v.extract().ok()).unwrap_or(0);
612 let instance: bool = get("instance").and_then(|v| v.extract().ok()).unwrap_or(false);
613 let mut ops: Vec<OperandSpec> = operands
614 .into_iter()
615 .map(|(n, d, required)| OperandSpec {
616 name: leak(n),
617 doc: leak(d),
618 values: &[],
619 required,
620 })
621 .collect();
622 if variadic {
623 ops.push(OperandSpec {
624 name: "...",
625 doc: "further positional arguments, as the function takes them",
626 values: &[],
627 required: false,
628 });
629 }
630 let mut opts: Vec<OptionSpec> = options
631 .into_iter()
632 .map(|(long, d)| OptionSpec {
633 short: None,
634 long: leak(long),
635 takes_value: true,
636 doc: leak(d),
637 })
638 .collect();
639 if open {
640 opts.push(OptionSpec {
641 short: None,
642 long: "*",
643 takes_value: true,
644 doc: "any other keyword argument the function takes",
645 });
646 }
647 Ok(Synopsis {
648 name: leak(name.to_string()),
649 doc: leak(doc),
650 pipes: Pipe::Both,
651 operands: Box::leak(ops.into_boxed_slice()),
652 options: Box::leak(opts.into_boxed_slice()),
653 input: if instance {
654 InputSlot::OperandOrSelf(input_at)
655 } else {
656 InputSlot::Operand(input_at)
657 },
658 returns: leak(if returns.is_empty() {
659 "a value, or a library value".to_string()
660 } else {
661 returns
662 }),
663 })
664 })?;
665 let syn: &'static Synopsis = Box::leak(Box::new(syn));
666 cache
667 .lock()
668 .expect("synopsis cache")
669 .insert(name.to_string(), syn);
670 Ok(syn)
671 }
672
673 pub fn call(&self, syn: &Synopsis, name: &str, call: LibCall) -> Result<Value, LibFailure> {
676 let (positional, keyword, input_at) = placed(syn, call);
677 Python::attach(|py| -> Result<Value, LibFailure> {
678 let failed = |e: PyErr| LibFailure::Failed(e.value(py).to_string());
679 let m = PY.module(py).map_err(failed)?;
680 let (args, kwargs) = python_arguments(py, &positional, &keyword).map_err(failed)?;
681 let out = m
682 .getattr("_call")
683 .map_err(failed)?
684 .call1((
685 self.root,
686 self.classes,
687 name,
688 self.construct,
689 self.dynamic,
690 args,
691 kwargs,
692 input_at,
693 ))
694 .map_err(failed)?;
695 crate::py_to_value(&out).map_err(failed)
696 })
697 }
698}