agenticml-lite 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agenticml/__init__.py +63 -0
- agenticml/__main__.py +5 -0
- agenticml/_frame_inspect.py +499 -0
- agenticml/_globals.py +308 -0
- agenticml/_import_hooks.py +127 -0
- agenticml/_source_inspect.py +253 -0
- agenticml/auth.py +314 -0
- agenticml/auto_metrics.py +160 -0
- agenticml/auto_tracking.py +998 -0
- agenticml/cli.py +611 -0
- agenticml/client.py +382 -0
- agenticml/code_tracking.py +614 -0
- agenticml/component_tracking.py +640 -0
- agenticml/distributed.py +35 -0
- agenticml/env.py +51 -0
- agenticml/ignore.py +118 -0
- agenticml/integrations/__init__.py +1 -0
- agenticml/integrations/hyperparams.py +265 -0
- agenticml/integrations/tensorboard.py +245 -0
- agenticml/launch.py +343 -0
- agenticml/lineage.py +522 -0
- agenticml/media.py +141 -0
- agenticml/model_tracking.py +245 -0
- agenticml/run.py +1386 -0
- agenticml/store/__init__.py +54 -0
- agenticml/store/gc.py +648 -0
- agenticml/store/layout.py +463 -0
- agenticml/store/notes.py +700 -0
- agenticml/store/objects.py +115 -0
- agenticml/store/overview.py +440 -0
- agenticml/store/reader.py +882 -0
- agenticml/store/writer.py +1203 -0
- agenticml/sync.py +1113 -0
- agenticml/system_metrics.py +151 -0
- agenticml/ui/__init__.py +7 -0
- agenticml/ui/server.py +1739 -0
- agenticml/ui/static/InstrumentSans.OFL.txt +93 -0
- agenticml/ui/static/InstrumentSans.woff2 +0 -0
- agenticml/ui/static/JetBrainsMono.OFL.txt +93 -0
- agenticml/ui/static/JetBrainsMono.woff2 +0 -0
- agenticml/ui/static/app.css +3109 -0
- agenticml/ui/static/app.js +3570 -0
- agenticml/ui/static/charts.js +427 -0
- agenticml/ui/static/code.js +777 -0
- agenticml/ui/static/codediff.js +471 -0
- agenticml/ui/static/index.html +29 -0
- agenticml_lite-1.0.0.dist-info/METADATA +283 -0
- agenticml_lite-1.0.0.dist-info/RECORD +52 -0
- agenticml_lite-1.0.0.dist-info/WHEEL +5 -0
- agenticml_lite-1.0.0.dist-info/entry_points.txt +2 -0
- agenticml_lite-1.0.0.dist-info/licenses/LICENSE +21 -0
- agenticml_lite-1.0.0.dist-info/top_level.txt +1 -0
agenticml/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""agenticml — Python SDK for the AgenticML platform.
|
|
2
|
+
|
|
3
|
+
Install as ``agenticml-lite``, import as ``agenticml``.
|
|
4
|
+
|
|
5
|
+
Quickstart::
|
|
6
|
+
|
|
7
|
+
import agenticml
|
|
8
|
+
|
|
9
|
+
agenticml.init(project="demo", name="exp1", config={"lr": 0.01})
|
|
10
|
+
for step in range(10):
|
|
11
|
+
agenticml.log({"loss": 1 / (step + 1)})
|
|
12
|
+
agenticml.summary["best_loss"] = 0.1
|
|
13
|
+
agenticml.finish()
|
|
14
|
+
|
|
15
|
+
See https://agenticml.xyz for the full reference.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from ._globals import (
|
|
19
|
+
config,
|
|
20
|
+
finish,
|
|
21
|
+
get_active_run,
|
|
22
|
+
init,
|
|
23
|
+
log,
|
|
24
|
+
log_artifact,
|
|
25
|
+
summary,
|
|
26
|
+
track_dataloader,
|
|
27
|
+
track_files,
|
|
28
|
+
track_loss,
|
|
29
|
+
track_model,
|
|
30
|
+
track_optimizer,
|
|
31
|
+
)
|
|
32
|
+
from .distributed import get_rank, is_main_process
|
|
33
|
+
from .media import Image
|
|
34
|
+
from .run import Run
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.0"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Image",
|
|
40
|
+
"Run",
|
|
41
|
+
"__version__",
|
|
42
|
+
"config",
|
|
43
|
+
"finish",
|
|
44
|
+
"get_active_run",
|
|
45
|
+
"get_rank",
|
|
46
|
+
"init",
|
|
47
|
+
"is_main_process",
|
|
48
|
+
"log",
|
|
49
|
+
"log_artifact",
|
|
50
|
+
"summary",
|
|
51
|
+
"track_dataloader",
|
|
52
|
+
"track_files",
|
|
53
|
+
"track_loss",
|
|
54
|
+
"track_model",
|
|
55
|
+
"track_optimizer",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name):
|
|
60
|
+
if name == "Client":
|
|
61
|
+
from .client import Client
|
|
62
|
+
return Client
|
|
63
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
agenticml/__main__.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Resolve caller variable names and loss expressions without importing torch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import linecache
|
|
9
|
+
import sys
|
|
10
|
+
import textwrap
|
|
11
|
+
import types
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .component_tracking import sanitize_component_name
|
|
15
|
+
|
|
16
|
+
# Longest first so "_optimizer" is removed before "_optim".
|
|
17
|
+
_TRAILING_SUFFIXES = (
|
|
18
|
+
"_optimizer",
|
|
19
|
+
"_criterion",
|
|
20
|
+
"_dataset",
|
|
21
|
+
"_loader",
|
|
22
|
+
"_model",
|
|
23
|
+
"_optim",
|
|
24
|
+
"_loss",
|
|
25
|
+
)
|
|
26
|
+
_SCOPE_NODES = (
|
|
27
|
+
ast.FunctionDef,
|
|
28
|
+
ast.AsyncFunctionDef,
|
|
29
|
+
ast.Lambda,
|
|
30
|
+
ast.ClassDef,
|
|
31
|
+
)
|
|
32
|
+
_MISSING = object()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def infer_variable_name(
|
|
36
|
+
obj: Any, start_frame: types.FrameType | None = None
|
|
37
|
+
) -> str | None:
|
|
38
|
+
"""Return the nearest non-agenticml variable name bound to ``obj``."""
|
|
39
|
+
try:
|
|
40
|
+
frame: Any = start_frame if start_frame is not None else sys._getframe(1)
|
|
41
|
+
except ValueError:
|
|
42
|
+
return None
|
|
43
|
+
seen: set[int] = set()
|
|
44
|
+
while frame is not None and id(frame) not in seen:
|
|
45
|
+
seen.add(id(frame))
|
|
46
|
+
try:
|
|
47
|
+
internal = _is_agenticml_frame(frame)
|
|
48
|
+
name = None if internal else _bound_name(frame, obj)
|
|
49
|
+
nxt = getattr(frame, "f_back", None)
|
|
50
|
+
except Exception:
|
|
51
|
+
break
|
|
52
|
+
if name is not None:
|
|
53
|
+
return name
|
|
54
|
+
frame = nxt
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_component_name(raw: str | None) -> str:
|
|
59
|
+
"""Strip one trailing component suffix and sanitize the name."""
|
|
60
|
+
if not isinstance(raw, str):
|
|
61
|
+
return "default"
|
|
62
|
+
text = raw.strip()
|
|
63
|
+
for suffix in _TRAILING_SUFFIXES:
|
|
64
|
+
if text.endswith(suffix):
|
|
65
|
+
text = text[: -len(suffix)]
|
|
66
|
+
break
|
|
67
|
+
if not text.strip():
|
|
68
|
+
return "default"
|
|
69
|
+
return sanitize_component_name(text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_loss_from_backward_frame(
|
|
73
|
+
frame: types.FrameType,
|
|
74
|
+
loss_tensor_name: str | None = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""Find the loss callable assigned in the frame that called ``backward()``."""
|
|
77
|
+
try:
|
|
78
|
+
return _resolve_loss(frame, loss_tensor_name)
|
|
79
|
+
except Exception:
|
|
80
|
+
return _failure_result(frame)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_agenticml_frame(frame: Any) -> bool:
|
|
84
|
+
try:
|
|
85
|
+
module = frame.f_globals.get("__name__", "")
|
|
86
|
+
except Exception:
|
|
87
|
+
return False
|
|
88
|
+
return isinstance(module, str) and module.startswith("agenticml")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _bound_name(frame: Any, obj: Any) -> str | None:
|
|
92
|
+
for attr in ("f_locals", "f_globals"):
|
|
93
|
+
name = _mapping_name(getattr(frame, attr, None), obj)
|
|
94
|
+
if name is not None:
|
|
95
|
+
return name
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _mapping_name(mapping: Any, obj: Any) -> str | None:
|
|
100
|
+
if mapping is None:
|
|
101
|
+
return None
|
|
102
|
+
try:
|
|
103
|
+
items = list(mapping.items())
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
for key, value in items:
|
|
107
|
+
if isinstance(key, str) and value is obj:
|
|
108
|
+
return key
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _resolve_loss(frame: Any, loss_tensor_name: str | None) -> dict[str, Any]:
|
|
113
|
+
filename, lineno = _frame_file_line(frame)
|
|
114
|
+
parsed = _parse_frame(frame)
|
|
115
|
+
if parsed is None or lineno is None:
|
|
116
|
+
return _result(
|
|
117
|
+
"inline_expression",
|
|
118
|
+
None,
|
|
119
|
+
_line_text(filename, lineno, None, 0),
|
|
120
|
+
filename,
|
|
121
|
+
lineno,
|
|
122
|
+
)
|
|
123
|
+
source, tree, offset = parsed
|
|
124
|
+
found = _find_loss_expr(tree, lineno, offset, loss_tensor_name)
|
|
125
|
+
if found is None:
|
|
126
|
+
return _result(
|
|
127
|
+
"inline_expression",
|
|
128
|
+
None,
|
|
129
|
+
_line_text(filename, lineno, source, offset),
|
|
130
|
+
filename,
|
|
131
|
+
lineno,
|
|
132
|
+
)
|
|
133
|
+
expr, line = found
|
|
134
|
+
expression = _segment(source, expr)
|
|
135
|
+
if expression is None:
|
|
136
|
+
expression = _line_text(filename, line, source, offset)
|
|
137
|
+
resolved = _callable_from_expr(expr, frame)
|
|
138
|
+
if resolved is not None:
|
|
139
|
+
return _result("ast_assignment", resolved, expression, filename, line)
|
|
140
|
+
return _result("inline_expression", None, expression, filename, line)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _failure_result(frame: Any) -> dict[str, Any]:
|
|
144
|
+
filename, lineno = _frame_file_line(frame)
|
|
145
|
+
return _result(
|
|
146
|
+
"inline_expression",
|
|
147
|
+
None,
|
|
148
|
+
_line_text(filename, lineno, None, 0),
|
|
149
|
+
filename,
|
|
150
|
+
lineno,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _result(
|
|
155
|
+
evidence: str,
|
|
156
|
+
resolved: Any,
|
|
157
|
+
expression: str | None,
|
|
158
|
+
filename: str | None,
|
|
159
|
+
line: int | None,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
return {
|
|
162
|
+
"evidence": evidence,
|
|
163
|
+
"callable": resolved,
|
|
164
|
+
"expression": expression,
|
|
165
|
+
"file": filename,
|
|
166
|
+
"line": line,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _frame_file_line(frame: Any) -> tuple[str | None, int | None]:
|
|
171
|
+
filename = None
|
|
172
|
+
lineno = None
|
|
173
|
+
try:
|
|
174
|
+
raw_name = getattr(getattr(frame, "f_code", None), "co_filename", None)
|
|
175
|
+
if isinstance(raw_name, str) and raw_name:
|
|
176
|
+
filename = raw_name
|
|
177
|
+
except Exception:
|
|
178
|
+
filename = None
|
|
179
|
+
try:
|
|
180
|
+
raw_line = getattr(frame, "f_lineno", None)
|
|
181
|
+
if isinstance(raw_line, int) and raw_line >= 1:
|
|
182
|
+
lineno = raw_line
|
|
183
|
+
except Exception:
|
|
184
|
+
lineno = None
|
|
185
|
+
return filename, lineno
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_frame(frame: Any) -> tuple[str, ast.AST, int] | None:
|
|
189
|
+
filename, _lineno = _frame_file_line(frame)
|
|
190
|
+
if filename:
|
|
191
|
+
source = _cached_source(filename)
|
|
192
|
+
if source:
|
|
193
|
+
try:
|
|
194
|
+
return source, ast.parse(source), 0
|
|
195
|
+
except (SyntaxError, ValueError):
|
|
196
|
+
pass
|
|
197
|
+
code = None
|
|
198
|
+
try:
|
|
199
|
+
code = getattr(frame, "f_code", None)
|
|
200
|
+
except Exception:
|
|
201
|
+
return None
|
|
202
|
+
if code is None:
|
|
203
|
+
return None
|
|
204
|
+
try:
|
|
205
|
+
lines, start = inspect.getsourcelines(code)
|
|
206
|
+
except (OSError, TypeError):
|
|
207
|
+
return None
|
|
208
|
+
source = textwrap.dedent("".join(lines))
|
|
209
|
+
try:
|
|
210
|
+
tree = ast.parse(source)
|
|
211
|
+
except (SyntaxError, ValueError):
|
|
212
|
+
return None
|
|
213
|
+
if not isinstance(start, int) or start < 1:
|
|
214
|
+
start = 1
|
|
215
|
+
return source, tree, start - 1
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _cached_source(filename: str) -> str | None:
|
|
219
|
+
try:
|
|
220
|
+
linecache.checkcache(filename)
|
|
221
|
+
lines = linecache.getlines(filename)
|
|
222
|
+
except Exception:
|
|
223
|
+
return None
|
|
224
|
+
if not lines:
|
|
225
|
+
return None
|
|
226
|
+
return "".join(lines)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _find_loss_expr(
|
|
230
|
+
tree: ast.AST,
|
|
231
|
+
file_lineno: int,
|
|
232
|
+
offset: int,
|
|
233
|
+
loss_tensor_name: str | None,
|
|
234
|
+
) -> tuple[ast.expr, int] | None:
|
|
235
|
+
scope = _enclosing_scope(tree, file_lineno, offset)
|
|
236
|
+
if loss_tensor_name is not None:
|
|
237
|
+
return _last_binding(scope, loss_tensor_name, file_lineno, offset)
|
|
238
|
+
call = _backward_call(scope, file_lineno, offset)
|
|
239
|
+
if call is None or not isinstance(call.func, ast.Attribute):
|
|
240
|
+
return None
|
|
241
|
+
receiver = call.func.value
|
|
242
|
+
if isinstance(receiver, ast.Name):
|
|
243
|
+
return _last_binding(scope, receiver.id, file_lineno, offset)
|
|
244
|
+
if isinstance(receiver, ast.NamedExpr):
|
|
245
|
+
line = _file_line(receiver, offset)
|
|
246
|
+
if line is None:
|
|
247
|
+
return None
|
|
248
|
+
return receiver.value, line
|
|
249
|
+
line = _file_line(receiver, offset)
|
|
250
|
+
if line is None:
|
|
251
|
+
return None
|
|
252
|
+
return receiver, line
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _enclosing_scope(tree: ast.AST, file_lineno: int, offset: int) -> ast.AST:
|
|
256
|
+
best: ast.AST = tree
|
|
257
|
+
best_span: int | None = None
|
|
258
|
+
for node in ast.walk(tree):
|
|
259
|
+
if not isinstance(node, _SCOPE_NODES):
|
|
260
|
+
continue
|
|
261
|
+
start = _file_line(node, offset)
|
|
262
|
+
end = _file_end(node, offset)
|
|
263
|
+
if start is None or end is None or not (start <= file_lineno <= end):
|
|
264
|
+
continue
|
|
265
|
+
span = end - start
|
|
266
|
+
if best_span is None or span <= best_span:
|
|
267
|
+
best = node
|
|
268
|
+
best_span = span
|
|
269
|
+
return best
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _backward_call(scope: ast.AST, file_lineno: int, offset: int) -> ast.Call | None:
|
|
273
|
+
best: ast.Call | None = None
|
|
274
|
+
best_span: int | None = None
|
|
275
|
+
for node in _iter_nodes(scope):
|
|
276
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
277
|
+
continue
|
|
278
|
+
if node.func.attr != "backward":
|
|
279
|
+
continue
|
|
280
|
+
start = _file_line(node, offset)
|
|
281
|
+
end = _file_end(node, offset)
|
|
282
|
+
if start is None or end is None or not (start <= file_lineno <= end):
|
|
283
|
+
continue
|
|
284
|
+
span = end - start
|
|
285
|
+
if best_span is None or span < best_span:
|
|
286
|
+
best = node
|
|
287
|
+
best_span = span
|
|
288
|
+
return best
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _last_binding(
|
|
292
|
+
scope: ast.AST, name: str, file_lineno: int, offset: int
|
|
293
|
+
) -> tuple[ast.expr, int] | None:
|
|
294
|
+
found: tuple[ast.expr, int] | None = None
|
|
295
|
+
for node in _iter_nodes(scope):
|
|
296
|
+
value = _binding_value(node, name)
|
|
297
|
+
if value is None:
|
|
298
|
+
continue
|
|
299
|
+
line = _file_line(node, offset)
|
|
300
|
+
if line is None or line > file_lineno:
|
|
301
|
+
continue
|
|
302
|
+
if found is None or line >= found[1]:
|
|
303
|
+
found = (value, line)
|
|
304
|
+
return found
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _binding_value(node: ast.AST, name: str) -> ast.expr | None:
|
|
308
|
+
if isinstance(node, ast.Assign):
|
|
309
|
+
for target in node.targets:
|
|
310
|
+
if _target_binds(target, name):
|
|
311
|
+
return node.value
|
|
312
|
+
return None
|
|
313
|
+
if isinstance(node, ast.AnnAssign) and node.value is not None:
|
|
314
|
+
if _target_binds(node.target, name):
|
|
315
|
+
return node.value
|
|
316
|
+
return None
|
|
317
|
+
if (
|
|
318
|
+
isinstance(node, ast.NamedExpr)
|
|
319
|
+
and isinstance(node.target, ast.Name)
|
|
320
|
+
and node.target.id == name
|
|
321
|
+
):
|
|
322
|
+
return node.value
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _target_binds(target: ast.AST, name: str) -> bool:
|
|
327
|
+
if isinstance(target, ast.Name):
|
|
328
|
+
return target.id == name
|
|
329
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
330
|
+
return any(_target_binds(elt, name) for elt in target.elts)
|
|
331
|
+
if isinstance(target, ast.Starred):
|
|
332
|
+
return _target_binds(target.value, name)
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _iter_nodes(root: ast.AST):
|
|
337
|
+
stack = [root]
|
|
338
|
+
while stack:
|
|
339
|
+
node = stack.pop()
|
|
340
|
+
yield node
|
|
341
|
+
children = [
|
|
342
|
+
child
|
|
343
|
+
for child in ast.iter_child_nodes(node)
|
|
344
|
+
if not isinstance(child, _SCOPE_NODES)
|
|
345
|
+
]
|
|
346
|
+
stack.extend(reversed(children))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _file_line(node: ast.AST, offset: int) -> int | None:
|
|
350
|
+
lineno = getattr(node, "lineno", None)
|
|
351
|
+
if not isinstance(lineno, int):
|
|
352
|
+
return None
|
|
353
|
+
return lineno + offset
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _file_end(node: ast.AST, offset: int) -> int | None:
|
|
357
|
+
end = getattr(node, "end_lineno", None)
|
|
358
|
+
if not isinstance(end, int):
|
|
359
|
+
return _file_line(node, offset)
|
|
360
|
+
return end + offset
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _segment(source: str, node: ast.AST) -> str | None:
|
|
364
|
+
try:
|
|
365
|
+
text = ast.get_source_segment(source, node)
|
|
366
|
+
except Exception:
|
|
367
|
+
return None
|
|
368
|
+
if not text:
|
|
369
|
+
return None
|
|
370
|
+
stripped = text.strip()
|
|
371
|
+
return stripped or None
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _line_text(
|
|
375
|
+
filename: str | None,
|
|
376
|
+
lineno: int | None,
|
|
377
|
+
source: str | None,
|
|
378
|
+
offset: int,
|
|
379
|
+
) -> str | None:
|
|
380
|
+
if source is not None and lineno is not None:
|
|
381
|
+
index = lineno - offset - 1
|
|
382
|
+
lines = source.splitlines()
|
|
383
|
+
if 0 <= index < len(lines):
|
|
384
|
+
text = lines[index].strip()
|
|
385
|
+
if text:
|
|
386
|
+
return text
|
|
387
|
+
if not filename or lineno is None:
|
|
388
|
+
return None
|
|
389
|
+
try:
|
|
390
|
+
text = linecache.getline(filename, lineno).strip()
|
|
391
|
+
except Exception:
|
|
392
|
+
return None
|
|
393
|
+
return text or None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _callable_from_expr(expr: ast.expr, frame: Any) -> Any:
|
|
397
|
+
try:
|
|
398
|
+
if not isinstance(expr, ast.Call):
|
|
399
|
+
return None
|
|
400
|
+
resolved = _callable_from_call(expr, frame)
|
|
401
|
+
except Exception:
|
|
402
|
+
return None
|
|
403
|
+
if _is_callable(resolved):
|
|
404
|
+
return resolved
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _callable_from_call(call: ast.Call, frame: Any) -> Any:
|
|
409
|
+
func = call.func
|
|
410
|
+
if isinstance(func, ast.Call):
|
|
411
|
+
return _partial_from_call(func, frame)
|
|
412
|
+
return _resolve_callee(func, frame)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _partial_from_call(call: ast.Call, frame: Any) -> Any:
|
|
416
|
+
if _resolve_callee(call.func, frame) is not functools.partial:
|
|
417
|
+
return None
|
|
418
|
+
if not call.args:
|
|
419
|
+
return None
|
|
420
|
+
wrapped = _static_value(call.args[0], frame)
|
|
421
|
+
if not _is_callable(wrapped):
|
|
422
|
+
return None
|
|
423
|
+
extra: list[Any] = []
|
|
424
|
+
for arg in call.args[1:]:
|
|
425
|
+
value = _static_value(arg, frame)
|
|
426
|
+
if value is _MISSING:
|
|
427
|
+
return wrapped
|
|
428
|
+
extra.append(value)
|
|
429
|
+
keywords: dict[str, Any] = {}
|
|
430
|
+
for keyword in call.keywords:
|
|
431
|
+
if keyword.arg is None:
|
|
432
|
+
return wrapped
|
|
433
|
+
value = _static_value(keyword.value, frame)
|
|
434
|
+
if value is _MISSING:
|
|
435
|
+
return wrapped
|
|
436
|
+
keywords[keyword.arg] = value
|
|
437
|
+
try:
|
|
438
|
+
return functools.partial(wrapped, *extra, **keywords)
|
|
439
|
+
except Exception:
|
|
440
|
+
return wrapped
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _resolve_callee(expr: ast.expr, frame: Any) -> Any:
|
|
444
|
+
if isinstance(expr, (ast.Name, ast.Attribute)):
|
|
445
|
+
return _eval_expr(expr, frame)
|
|
446
|
+
return _MISSING
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _eval_expr(expr: ast.expr, frame: Any) -> Any:
|
|
450
|
+
if isinstance(expr, ast.Name):
|
|
451
|
+
return _lookup(expr.id, frame)
|
|
452
|
+
if isinstance(expr, ast.Attribute):
|
|
453
|
+
base = _eval_expr(expr.value, frame)
|
|
454
|
+
if base is _MISSING:
|
|
455
|
+
return _MISSING
|
|
456
|
+
try:
|
|
457
|
+
return getattr(base, expr.attr)
|
|
458
|
+
except Exception:
|
|
459
|
+
return _MISSING
|
|
460
|
+
return _MISSING
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _static_value(expr: ast.expr, frame: Any) -> Any:
|
|
464
|
+
if isinstance(expr, ast.Constant):
|
|
465
|
+
return expr.value
|
|
466
|
+
if isinstance(expr, ast.UnaryOp) and isinstance(expr.operand, ast.Constant):
|
|
467
|
+
value = expr.operand.value
|
|
468
|
+
if isinstance(expr.op, ast.USub):
|
|
469
|
+
try:
|
|
470
|
+
return -value
|
|
471
|
+
except Exception:
|
|
472
|
+
return _MISSING
|
|
473
|
+
if isinstance(expr.op, ast.UAdd):
|
|
474
|
+
return value
|
|
475
|
+
if isinstance(expr, (ast.Name, ast.Attribute)):
|
|
476
|
+
return _eval_expr(expr, frame)
|
|
477
|
+
return _MISSING
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _lookup(name: str, frame: Any) -> Any:
|
|
481
|
+
for attr in ("f_locals", "f_globals"):
|
|
482
|
+
mapping = getattr(frame, attr, None)
|
|
483
|
+
if mapping is None:
|
|
484
|
+
continue
|
|
485
|
+
try:
|
|
486
|
+
if name in mapping:
|
|
487
|
+
return mapping[name]
|
|
488
|
+
except Exception:
|
|
489
|
+
continue
|
|
490
|
+
return _MISSING
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _is_callable(obj: Any) -> bool:
|
|
494
|
+
if obj is None or obj is _MISSING:
|
|
495
|
+
return False
|
|
496
|
+
try:
|
|
497
|
+
return callable(obj)
|
|
498
|
+
except Exception:
|
|
499
|
+
return False
|