File size: 2,704 Bytes
380a7fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"""Ancrage pour du code : les modules et les noms importés existent-ils vraiment dans l'environnement ?

L'hallucination typique d'un petit modèle de code n'est pas une fausse citation mais une API inventée
(`from math import sqrtt`). La vérification se fait dans le bac à sable, jamais dans le processus du Space.
"""
from __future__ import annotations

import ast
import json

from mouh.skills.sandbox import SandboxLimits, run_in_sandbox

_PROBE = (
    "import importlib, json, sys\n"
    "bad = []\n"
    "for mod, names in json.loads(sys.argv[1]):\n"
    "    try:\n"
    "        m = importlib.import_module(mod)\n"
    "    except Exception:\n"
    "        bad.append(mod)\n"
    "        continue\n"
    "    for n in names:\n"
    "        if not hasattr(m, n):\n"
    "            try:\n"
    "                importlib.import_module(mod + '.' + n)\n"
    "            except Exception:\n"
    "                bad.append(mod + '.' + n)\n"
    "print(json.dumps(bad))\n"
)


def imported_names(files: dict[str, str]) -> list[tuple[str, list[str]]]:
    """[(module, [noms])] des imports absolus, hors modules fournis par la proposition elle-même."""
    local = {name[:-3].replace("/", ".") for name in files if name.endswith(".py")}
    wanted: list[tuple[str, list[str]]] = []
    for code in files.values():
        try:
            tree = ast.parse(code)
        except SyntaxError:
            return []  # la porte de compilation et pytest le signalent mieux
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                wanted += [(a.name, []) for a in node.names]
            elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
                wanted.append((node.module, [a.name for a in node.names if a.name != "*"]))
    return [(m, n) for m, n in wanted if m.split(".")[0] not in local]


class ApiCheck:
    def __init__(self, timeout_s: float = 10.0) -> None:
        self.timeout_s = timeout_s
        self._memo: dict[str, list[str]] = {}

    def check(self, files, notes=None) -> list[str]:
        wanted = imported_names(files)
        if not wanted:
            return []
        key = json.dumps(wanted, sort_keys=True)
        if key not in self._memo:
            run = run_in_sandbox(["probe.py", key], {"probe.py": _PROBE}, timeout_s=self.timeout_s,
                                 limits=SandboxLimits(cpu_seconds=int(self.timeout_s)))
            try:
                bad = json.loads(run.stdout.strip().splitlines()[-1])
            except (ValueError, IndexError):
                bad = []
            self._memo[key] = [f"API introuvable : {b}" for b in bad]
        return list(self._memo[key])