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