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 """A parameter is reachable as Python takes it (spec: "Reflected
218 commands"): operands fill the positional parameters in signature
219 order — the required ones of every call on the path, then the
220 defaulted ones of the first call, where surplus operands go —
221 and `--name=` fills the parameter so named wherever it stands;
222 positional-only is an operand alone, keyword-only an option
223 alone; `*args` admits surplus operands, `**kwargs` any option."""
224 start, segs = _plan(root, classes, path, dynamic)
225 operands, options, seen = [], [], set()
226 deferred = []
227 variadic = open_ = False
228 first = True
229 if start == "input":
230 operands.append(("self", "the input: the receiver of the method", True))
231 for s in segs:
232 if s.kind in ("class", "function", "method"):
233 if s.params is None:
234 variadic = open_ = True
235 first = False
236 continue
237 leading = _first_positional(s)
238 for p in s.params:
239 if p.kind == p.VAR_POSITIONAL:
240 variadic = True
241 continue
242 if p.kind == p.VAR_KEYWORD:
243 open_ = True
244 continue
245 required = p.default is p.empty
246 d = p.default
247 shown = "" if required or repr(d).startswith("<") else repr(d)
248 doc = f"default {shown}" if shown else ""
249 if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD):
250 if required:
251 operands.append((p.name, "", True))
252 elif first and p is leading and p.name in _DATA_NAMES:
253 # A class or function whose first parameter is
254 # its data, defaulted (`DataFrame(data=None)`):
255 # the input's slot, ahead of the rest.
256 operands.append((p.name, doc, False))
257 elif first:
258 deferred.append((p.name, doc, False))
259 if p.kind != p.POSITIONAL_ONLY:
260 long = p.name.replace("_", "-")
261 if long not in seen:
262 seen.add(long)
263 options.append((long, doc))
264 first = False
265 elif s.kind == "unknown":
266 variadic = open_ = True
267 # The slot the input fills: the receiver of a method; else the
268 # first parameter named as data (`X` in `cross_val_score(estimator,
269 # X, y)`, `data` of a constructor), else the first — judged before
270 # the first call's defaulted positionals join the operands.
271 input_at = 0
272 if start != "input":
273 input_at = next((i for i, (n, _, _) in enumerate(operands) if n in _DATA_NAMES), 0)
274 operands += deferred
275 last = segs[-1]
276 if last.kind == "module":
277 raise AttributeError(
278 f"'{path}' is a module of {root}, not a command: name a function or a class in it ({path}.<name>)")
279 doc = _first_line(last.obj)
280 returns = ""
281 if last.kind in ("function", "method") and last.params is not None:
282 try:
283 ret = inspect.signature(last.obj).return_annotation
284 if ret is not inspect.Signature.empty:
285 returns = ret if isinstance(ret, str) else getattr(ret, "__name__", str(ret))
286 except (TypeError, ValueError):
287 pass
288 elif last.kind == "class":
289 returns = last.obj.__name__
290 # A path that opens with a class may take the input as the
291 # instance itself (a fitted estimator before `.predict(X)`).
292 calls = [s for s in segs if s.kind in ("class", "function", "method", "unknown")]
293 instance = start != "input" and bool(calls) and calls[0].kind == "class"
294 return {"operands": operands, "options": options, "variadic": variadic,
295 "open": open_, "doc": doc, "returns": returns.strip("'"),
296 "input": input_at, "instance": instance}
297
298def _numeric(v):
299 # A boolean reads 1 for true, 0 for false (the table rule), so a
300 # boolean field is a column.
301 if isinstance(v, bool):
302 return 1.0 if v else 0.0
303 if isinstance(v, (int, float)):
304 return float(v)
305 if hasattr(v, "magnitude"):
306 return float(v.magnitude)
307 if isinstance(v, str):
308 try:
309 return float(v.replace(",", ""))
310 except ValueError:
311 return None
312 return None
313
314def _read(v):
315 """A value by its numeric reading (the table rule): numeric text
316 as the number it reads as (an integer form exact), a quantity by
317 its magnitude, a boolean as 1 or 0, anything else as it is."""
318 if isinstance(v, bool):
319 return 1 if v else 0
320 if isinstance(v, str):
321 t = v.strip()
322 try:
323 return int(t)
324 except ValueError:
325 pass
326 try:
327 return float(t)
328 except ValueError:
329 return v
330 if hasattr(v, "magnitude"):
331 return v.magnitude
332 return v
333
334def _isnum(v):
335 return isinstance(v, (int, float)) and not isinstance(v, bool)
336
337_scalar = _read
338
339def _array(x):
340 import numpy as np
341 import datetime as dt
342 if isinstance(x, list):
343 if x and all(isinstance(v, dt.datetime) for v in x):
344 return np.array([v.replace(tzinfo=None) if v.tzinfo else v for v in x], dtype="datetime64[ns]")
345 if x and all(isinstance(v, dt.timedelta) for v in x):
346 return np.array(x, dtype="timedelta64[ns]")
347 if x and all(isinstance(v, list) for v in x):
348 return np.array([_array(v) for v in x])
349 read = [_read(v) for v in x]
350 if read and all(v is None or _isnum(v) for v in read):
351 if any(v is None for v in read):
352 return np.array([np.nan if v is None else float(v) for v in read], dtype=float)
353 return np.array(read)
354 return np.array(read)
355 return x
356
357def _matrix(records):
358 """The table rule for an array: the numeric fields of the first
359 record are the columns, the first text field the label (dropped:
360 an array has no names; pd: keeps them)."""
361 import numpy as np
362 first = records[0]
363 cols = [k for k, v in first.items() if _numeric(v) is not None]
364 label = next((k for k, v in first.items() if isinstance(v, str) and _numeric(v) is None), None)
365 if not cols:
366 raise TypeError("no numeric field in the first record; nothing to make a matrix of")
367 out = []
368 for i, r in enumerate(records):
369 row = []
370 for c in cols:
371 v = _numeric(r.get(c))
372 if v is None:
373 who = r.get(label, i + 1) if label else i + 1
374 raise TypeError(f"row {who}: field '{c}' is {'null (a hole in the matrix)' if r.get(c) is None else 'not numeric'}")
375 row.append(v)
376 out.append(row)
377 return np.array(out, dtype=float)
378
379def _series(values, index=None):
380 """A column: numeric when every non-null value reads as a number
381 (the table rule), text otherwise. A null crosses as pd.NA in a
382 nullable dtype (Float64, Int64, boolean, string), never as NaN,
383 so it comes back as null; a nan is a float like any other."""
384 import pandas as pd
385 read = [_read(v) for v in values]
386 present = [v for v in read if v is not None]
387 if any(v is None for v in read):
388 if present and all(_isnum(v) for v in present):
389 dtype = "Int64" if all(isinstance(v, int) for v in present) else "Float64"
390 return pd.array([pd.NA if v is None else v for v in read], dtype=dtype)
391 return pd.array([pd.NA if v is None else str(v) for v in read], dtype="string")
392 if present and all(_isnum(v) for v in present):
393 return pd.Series(read, index=index)
394 return pd.Series(read, index=index, dtype=object) if index is not None else pd.Series(read, dtype=object)
395
396def _numeric_columns(df):
397 """Every object column through `_series`: numeric readings,
398 nulls as pd.NA in the nullable dtypes."""
399 import pandas as pd
400 for c in df.columns:
401 col = df[c]
402 # a boolean column reads 1 and 0 (the table rule)
403 if str(col.dtype) == "bool":
404 df[c] = col.astype("int64")
405 continue
406 if str(col.dtype) == "boolean":
407 df[c] = col.astype("Int64")
408 continue
409 # pandas 3 infers its `str` dtype for text; it reads as
410 # object does.
411 if col.dtype != object and str(col.dtype) not in ("str", "string"):
412 continue
413 s = _series(col.tolist())
414 df[c] = s if not isinstance(s, pd.Series) else s.values
415 return df
416
417def _frame(x):
418 import pandas as pd
419 if isinstance(x, list):
420 if x and all(isinstance(r, dict) for r in x):
421 df = pd.DataFrame(x)
422 label = next((k for k, v in x[0].items() if isinstance(v, str) and _numeric(v) is None), None)
423 if label is not None and label in df.columns:
424 df = df.set_index(label)
425 return _numeric_columns(df)
426 if x and all(isinstance(r, list) for r in x):
427 return _numeric_columns(pd.DataFrame([[_read(v) for v in r] for r in x]))
428 s = _series(x)
429 return s if isinstance(s, pd.Series) else pd.Series(s)
430 if isinstance(x, dict):
431 return pd.Series({k: _read(v) for k, v in x.items()})
432 return x
433
434def _construct(x, mode):
435 if mode == "none" or not isinstance(x, (list, dict)):
436 return x
437 # A record is a one-row table by the table rule (the user's
438 # ruling on constructors as operands): a frame of one row, or
439 # the matrix of its numeric fields.
440 if isinstance(x, dict):
441 x = [x]
442 if mode == "frame":
443 return _frame(x)
444 if x and all(isinstance(r, dict) for r in x):
445 return _matrix(x)
446 return _array(x)
447
448_DATA_NAMES = {"data", "object", "a", "x", "X", "values", "arr", "obj", "shape"}
449
450def _required(s, named=()):
451 """How many operands a call takes as its required positional
452 parameters, those filled by name left out."""
453 if s.params is None:
454 return 0
455 return sum(1 for p in s.params
456 if p.default is p.empty and p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
457 and p.name not in named)
458
459_plans = {}
460
461def _first_positional(s):
462 if not s.params:
463 return None
464 return next((p for p in s.params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)), None)
465
466def _takes_data(s):
467 """Whether a class's constructor takes data first (`DataFrame(data)`,
468 `ndarray(shape)`); an estimator's (`GaussianNB(priors)`) does not."""
469 first = _first_positional(s)
470 return s.params is None or (first is not None and first.name in _DATA_NAMES)
471
472def _call(root, classes, path, construct, dynamic, args, kwargs, input_at=None):
473 with contextlib.redirect_stdout(sys.stderr):
474 key = (root, tuple(classes), path)
475 if key not in _plans:
476 _plans[key] = _plan(root, classes, path, dynamic)
477 start, segs = _plans[key]
478 # Every operand keeps the kind its expression yields and a list
479 # is read as the input is (the table rule): a list of records
480 # a matrix or frame, numeric readings a vector, a library
481 # value itself.
482 queue = [_construct(a, construct) for a in args]
483 # Where the input stands in the queue, followed through the
484 # pops; None once a call has taken it.
485 def taken(k):
486 nonlocal input_at
487 if input_at is not None:
488 input_at = None if input_at < k else input_at - k
489 calls = [s for s in segs if s.kind in ("class", "function", "method", "unknown")]
490 receiver = None
491 if start == "input":
492 if not queue:
493 raise TypeError(f"{path}: the receiver is missing (nothing on the pipe)")
494 receiver = queue.pop(0)
495 taken(1)
496 pos = {id(s): [] for s in segs}
497 instance = None
498 if calls and calls[0].kind == "class" and queue:
499 cls = calls[0].obj
500 if inspect.isclass(cls) and isinstance(queue[0], cls):
501 # The input is an instance of the class on the path (a
502 # fitted estimator, a frame): the receiver of its
503 # methods, not constructed again.
504 instance = queue.pop(0)
505 taken(1)
506 else:
507 # A class first on the path whose first parameter is its
508 # data (`pd:Series.rolling.mean(12)`, `np:ndarray`) is
509 # constructed from the input; an estimator's constructor
510 # (KMeans(n_clusters)) is not, and the input reaches its
511 # method.
512 first = _first_positional(calls[0])
513 if first is not None and first.name in _DATA_NAMES and first.default is not first.empty:
514 pos[id(calls[0])].append(queue.pop(0))
515 taken(1)
516 # An option fills the parameter so named on the first call
517 # that declares it, else one taking **kwargs; a required
518 # parameter it fills is not taken from the operands.
519 kw = {id(s): {} for s in segs}
520 for k, v in kwargs.items():
521 target = None
522 for s in calls:
523 if s.params is None or any(p.name == k for p in s.params):
524 target = s
525 break
526 if target is None:
527 for s in calls:
528 if s.params is not None and any(p.kind == p.VAR_KEYWORD for p in s.params):
529 target = s
530 break
531 if target is None:
532 raise TypeError(f"{path}: no parameter named '{k.replace('_', '-')}'")
533 kw[id(target)][k] = v
534 for s in calls:
535 need = 0 if (instance is not None and s is calls[0]) else _required(s, kw[id(s)])
536 pos[id(s)], queue = pos[id(s)] + queue[:need], queue[need:]
537 taken(need)
538 if queue:
539 if not calls:
540 raise TypeError(f"{path}: {len(queue)} operand(s) given, none taken (an attribute)")
541 # The input passing a bare constructor that takes no data
542 # (an estimator, built from its options alone) is not
543 # handed to it: the instance is the result.
544 if (input_at is not None and input_at < len(queue) and calls[0].kind == "class"
545 and instance is None and not _takes_data(calls[0])):
546 del queue[input_at]
547 pos[id(calls[0])] += queue
548 cur = receiver if start == "input" else importlib.import_module(root)
549 for s in segs:
550 a = _attr(cur, s.name)
551 if s.kind == "unknown":
552 _withhold(a, s.name, path, root)
553 if s.kind == "module":
554 cur = a
555 elif s.kind == "class" and instance is not None and s is calls[0]:
556 cur = instance
557 elif s.kind in ("class", "function", "method") or (s.kind == "unknown" and callable(a)):
558 cur = a(*pos[id(s)], **kw[id(s)])
559 elif pos[id(s)] or kw[id(s)]:
560 # An attribute given arguments: a callable instance
561 # (`stats.norm(0; 1)` freezes a distribution).
562 if not callable(a):
563 raise TypeError(f"{path}: '{s.name}' is an attribute and takes no arguments")
564 cur = a(*pos[id(s)], **kw[id(s)])
565 else:
566 cur = a
567 return cur
568"#;
569
570static PY: PyGlue = PyGlue::new(GLUE, "quarb_reflect.py", "quarb_reflect");
571
572pub struct Reflective {
577 root: &'static str,
578 classes: &'static [&'static str],
579 construct: &'static str,
580 dynamic: bool,
584 cache: OnceLock<Mutex<HashMap<String, &'static Synopsis>>>,
585}
586
587fn leak(s: String) -> &'static str {
588 Box::leak(s.into_boxed_str())
589}
590
591impl Reflective {
592 pub const fn new(
593 root: &'static str,
594 classes: &'static [&'static str],
595 construct: &'static str,
596 dynamic: bool,
597 ) -> Reflective {
598 Reflective {
599 root,
600 classes,
601 construct,
602 dynamic,
603 cache: OnceLock::new(),
604 }
605 }
606
607 pub fn root(&self) -> &'static str {
609 self.root
610 }
611
612 pub fn resolve(&self, name: &str) -> Result<&'static Synopsis, String> {
616 let cache = self.cache.get_or_init(|| Mutex::new(HashMap::new()));
617 if let Some(s) = cache.lock().expect("synopsis cache").get(name) {
618 return Ok(s);
619 }
620 let syn = Python::attach(|py| -> Result<Synopsis, String> {
621 let m = PY.module(py).map_err(|e| e.to_string())?;
622 let d = m
623 .getattr("_synopsis")
624 .and_then(|f| f.call1((self.root, self.classes, name, self.dynamic)))
625 .map_err(|e| e.value(py).to_string())?;
626 let d = d.cast::<PyDict>().map_err(|e| e.to_string())?;
627 let get = |k: &str| d.get_item(k).ok().flatten();
628 let operands: Vec<(String, String, bool)> = get("operands")
629 .and_then(|v| v.extract().ok())
630 .unwrap_or_default();
631 let options: Vec<(String, String)> = get("options")
632 .and_then(|v| v.extract().ok())
633 .unwrap_or_default();
634 let variadic: bool = get("variadic")
635 .and_then(|v| v.extract().ok())
636 .unwrap_or(false);
637 let open: bool = get("open").and_then(|v| v.extract().ok()).unwrap_or(false);
638 let doc: String = get("doc")
639 .and_then(|v| v.extract().ok())
640 .unwrap_or_default();
641 let returns: String = get("returns")
642 .and_then(|v| v.extract().ok())
643 .unwrap_or_default();
644 let input_at: usize = get("input").and_then(|v| v.extract().ok()).unwrap_or(0);
645 let instance: bool = get("instance")
646 .and_then(|v| v.extract().ok())
647 .unwrap_or(false);
648 let mut ops: Vec<OperandSpec> = operands
649 .into_iter()
650 .map(|(n, d, required)| OperandSpec {
651 name: leak(n),
652 doc: leak(d),
653 values: &[],
654 required,
655 })
656 .collect();
657 if variadic {
658 ops.push(OperandSpec {
659 name: "...",
660 doc: "further positional arguments, as the function takes them",
661 values: &[],
662 required: false,
663 });
664 }
665 let mut opts: Vec<OptionSpec> = options
666 .into_iter()
667 .map(|(long, d)| OptionSpec {
668 short: None,
669 long: leak(long),
670 takes_value: true,
671 doc: leak(d),
672 })
673 .collect();
674 if open {
675 opts.push(OptionSpec {
676 short: None,
677 long: "*",
678 takes_value: true,
679 doc: "any other keyword argument the function takes",
680 });
681 }
682 Ok(Synopsis {
683 name: leak(name.to_string()),
684 doc: leak(doc),
685 pipes: Pipe::Both,
686 operands: Box::leak(ops.into_boxed_slice()),
687 options: Box::leak(opts.into_boxed_slice()),
688 input: if instance {
689 InputSlot::OperandOrSelf(input_at)
690 } else {
691 InputSlot::Operand(input_at)
692 },
693 returns: leak(if returns.is_empty() {
694 "a value, or a library value".to_string()
695 } else {
696 returns
697 }),
698 })
699 })?;
700 let syn: &'static Synopsis = Box::leak(Box::new(syn));
701 cache
702 .lock()
703 .expect("synopsis cache")
704 .insert(name.to_string(), syn);
705 Ok(syn)
706 }
707
708 pub fn call(&self, syn: &Synopsis, name: &str, call: LibCall) -> Result<Value, LibFailure> {
711 let (positional, keyword, input_at) = placed(syn, call);
712 Python::attach(|py| -> Result<Value, LibFailure> {
713 let failed = |e: PyErr| LibFailure::Failed(e.value(py).to_string());
714 let m = PY.module(py).map_err(failed)?;
715 let (args, kwargs) = python_arguments(py, &positional, &keyword).map_err(failed)?;
716 let out = m
717 .getattr("_call")
718 .map_err(failed)?
719 .call1((
720 self.root,
721 self.classes,
722 name,
723 self.construct,
724 self.dynamic,
725 args,
726 kwargs,
727 input_at,
728 ))
729 .map_err(failed)?;
730 crate::py_to_value(&out).map_err(failed)
731 })
732 }
733}