honestcode 0.2.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.
Potentially problematic release.
This version of honestcode might be problematic. Click here for more details.
- honestcode/__init__.py +19 -0
- honestcode/cli.py +338 -0
- honestcode/graph/__init__.py +13 -0
- honestcode/graph/graph_store.py +229 -0
- honestcode/graph/watcher.py +116 -0
- honestcode/honest/__init__.py +19 -0
- honestcode/honest/project_binding.py +157 -0
- honestcode/honest/router.py +200 -0
- honestcode/honest/symbol_index.py +291 -0
- honestcode/mcp/__init__.py +46 -0
- honestcode/mcp/knowledge_base.py +141 -0
- honestcode/mcp/server.py +305 -0
- honestcode/mcp/tools.py +1267 -0
- honestcode/py.typed +0 -0
- honestcode/sandbox/__init__.py +195 -0
- honestcode/structure/__init__.py +6 -0
- honestcode/structure/extractor.py +296 -0
- honestcode/structure/multi_lang.py +180 -0
- honestcode/structure/relations.py +43 -0
- honestcode/structure/utils.py +31 -0
- honestcode-0.2.0.dist-info/METADATA +811 -0
- honestcode-0.2.0.dist-info/RECORD +26 -0
- honestcode-0.2.0.dist-info/WHEEL +5 -0
- honestcode-0.2.0.dist-info/entry_points.txt +3 -0
- honestcode-0.2.0.dist-info/licenses/LICENSE +21 -0
- honestcode-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project-wide symbol index.
|
|
3
|
+
|
|
4
|
+
Builds a unified view of every function, class, and variable defined in a
|
|
5
|
+
project, and caches the result to disk.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from honestcode.structure.extractor import StructureExtractor
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
__all__ = ["SymbolInfo", "SymbolIndex", "ProjectIndex", "get_project_index"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SymbolInfo:
|
|
26
|
+
file: str
|
|
27
|
+
line: int
|
|
28
|
+
kind: str
|
|
29
|
+
exported: bool = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SymbolIndex:
|
|
34
|
+
symbols: dict[str, SymbolInfo] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def exported_symbols(self) -> dict[str, SymbolInfo]:
|
|
38
|
+
return {n: i for n, i in self.symbols.items() if i.exported}
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def local_symbols(self) -> dict[str, SymbolInfo]:
|
|
42
|
+
return {n: i for n, i in self.symbols.items() if not i.exported}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ProjectIndex:
|
|
47
|
+
root: str
|
|
48
|
+
symbols: dict[str, SymbolInfo]
|
|
49
|
+
files: list[str]
|
|
50
|
+
cache_key: str = ""
|
|
51
|
+
cached: bool = False
|
|
52
|
+
|
|
53
|
+
def to_sqlite(self, db_path: str | Path) -> None:
|
|
54
|
+
import sqlite3
|
|
55
|
+
|
|
56
|
+
conn = sqlite3.connect(str(db_path))
|
|
57
|
+
conn.execute("DROP TABLE IF EXISTS symbols")
|
|
58
|
+
conn.execute(
|
|
59
|
+
"CREATE TABLE symbols (name TEXT PRIMARY KEY, file TEXT, line INTEGER, kind TEXT, exported INTEGER)" # noqa: E501
|
|
60
|
+
)
|
|
61
|
+
for name, info in self.symbols.items():
|
|
62
|
+
conn.execute(
|
|
63
|
+
"INSERT OR REPLACE INTO symbols VALUES (?, ?, ?, ?, ?)",
|
|
64
|
+
(name, info.file, info.line, info.kind, int(info.exported)),
|
|
65
|
+
)
|
|
66
|
+
conn.execute("DROP TABLE IF EXISTS meta")
|
|
67
|
+
conn.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)")
|
|
68
|
+
conn.execute("INSERT INTO meta VALUES (?, ?)", ("root", self.root))
|
|
69
|
+
conn.execute("INSERT INTO meta VALUES (?, ?)", ("files", json.dumps(self.files)))
|
|
70
|
+
conn.commit()
|
|
71
|
+
conn.close()
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_sqlite(cls, db_path: str | Path) -> ProjectIndex | None:
|
|
75
|
+
import sqlite3
|
|
76
|
+
|
|
77
|
+
p = Path(db_path)
|
|
78
|
+
if not p.exists():
|
|
79
|
+
return None
|
|
80
|
+
try:
|
|
81
|
+
conn = sqlite3.connect(str(p))
|
|
82
|
+
cur = conn.cursor()
|
|
83
|
+
cur.execute("SELECT name, file, line, kind, exported FROM symbols")
|
|
84
|
+
symbols = {
|
|
85
|
+
name: SymbolInfo(file=f, line=line, kind=kind, exported=bool(exported))
|
|
86
|
+
for name, f, line, kind, exported in cur.fetchall()
|
|
87
|
+
}
|
|
88
|
+
cur.execute("SELECT key, value FROM meta")
|
|
89
|
+
meta = dict(cur.fetchall())
|
|
90
|
+
conn.close()
|
|
91
|
+
return cls(
|
|
92
|
+
root=meta.get("root", ""),
|
|
93
|
+
symbols=symbols,
|
|
94
|
+
files=json.loads(meta.get("files", "[]")),
|
|
95
|
+
)
|
|
96
|
+
except Exception as e: # noqa: BLE001
|
|
97
|
+
logger.warning("Failed to load SQLite index from %s: %s", db_path, e)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _should_rebuild(root: Path, cache_path: Path) -> bool:
|
|
102
|
+
"""Determine whether the on-disk cache is stale.
|
|
103
|
+
|
|
104
|
+
Considers file list, content hashes, and mtime for robust invalidation.
|
|
105
|
+
"""
|
|
106
|
+
if not cache_path.exists():
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
cache = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
111
|
+
except Exception: # noqa: BLE001
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
current_files = sorted(
|
|
115
|
+
str(p.relative_to(root))
|
|
116
|
+
for p in root.rglob("*.py")
|
|
117
|
+
if not p.name.startswith(".") and "__pycache__" not in p.parts
|
|
118
|
+
)
|
|
119
|
+
if cache.get("files") != current_files:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
old_hashes = cache.get("hashes", {})
|
|
123
|
+
for rel in current_files:
|
|
124
|
+
p = root / rel
|
|
125
|
+
try:
|
|
126
|
+
digest = hashlib.sha256(p.read_bytes()).hexdigest()
|
|
127
|
+
except Exception: # noqa: BLE001
|
|
128
|
+
return True
|
|
129
|
+
if old_hashes.get(rel) != digest:
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _build_cache_key(root: Path) -> str:
|
|
136
|
+
"""Build a deterministic cache key from file contents."""
|
|
137
|
+
h = hashlib.sha256()
|
|
138
|
+
for p in sorted(root.rglob("*.py")):
|
|
139
|
+
if p.name.startswith(".") or "__pycache__" in p.parts:
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
h.update(p.read_bytes())
|
|
143
|
+
except Exception: # noqa: BLE001
|
|
144
|
+
continue
|
|
145
|
+
return h.hexdigest()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _index_directory(root: Path, extractor: StructureExtractor) -> ProjectIndex:
|
|
149
|
+
symbols: dict[str, SymbolInfo] = {}
|
|
150
|
+
files: list[str] = []
|
|
151
|
+
for p in sorted(root.rglob("*.py")):
|
|
152
|
+
if p.name.startswith(".") or "__pycache__" in p.parts:
|
|
153
|
+
continue
|
|
154
|
+
rel = str(p.relative_to(root))
|
|
155
|
+
files.append(rel)
|
|
156
|
+
try:
|
|
157
|
+
res = extractor.parse_file(p)
|
|
158
|
+
except Exception as e: # noqa: BLE001
|
|
159
|
+
logger.warning("Failed to parse %s: %s", p, e)
|
|
160
|
+
continue
|
|
161
|
+
_merge(p, root, res, symbols)
|
|
162
|
+
|
|
163
|
+
return ProjectIndex(
|
|
164
|
+
root=str(root),
|
|
165
|
+
symbols=symbols,
|
|
166
|
+
files=files,
|
|
167
|
+
cache_key=_build_cache_key(root),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _module_name(file: Path, root: Path) -> str:
|
|
172
|
+
"""Infer a dotted module name from *file* relative to *root*.
|
|
173
|
+
|
|
174
|
+
Only strips the ``src`` prefix when it is a standalone directory component
|
|
175
|
+
(i.e. the standard ``src`` layout) and there are further path segments.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
rel = file.resolve().relative_to(root.resolve())
|
|
179
|
+
except ValueError:
|
|
180
|
+
rel = file
|
|
181
|
+
parts = list(rel.with_suffix("").parts)
|
|
182
|
+
# Only strip "src" if it is a real layout prefix (has sub-packages after it).
|
|
183
|
+
if len(parts) > 1 and parts[0] == "src":
|
|
184
|
+
parts = parts[1:]
|
|
185
|
+
if parts and parts[-1] == "__init__":
|
|
186
|
+
parts.pop()
|
|
187
|
+
return ".".join(parts)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _merge(file: Path, root: Path, res, symbols: dict[str, SymbolInfo]) -> None:
|
|
191
|
+
"""Merge a ParseResult's definitions into the global symbol table."""
|
|
192
|
+
rel = "/".join(file.relative_to(root).parts)
|
|
193
|
+
module = _module_name(file, root)
|
|
194
|
+
|
|
195
|
+
def _add(name: str, line: int, kind: str) -> None:
|
|
196
|
+
full_name = f"{module}.{name}" if module and name else (name or module)
|
|
197
|
+
exported = not name.split(".")[-1].startswith("_") if name else True
|
|
198
|
+
symbols[full_name] = SymbolInfo(file=rel, line=line, kind=kind, exported=exported)
|
|
199
|
+
|
|
200
|
+
for fn, (s, _e) in res.func_defs.items():
|
|
201
|
+
_add(fn, s + 1, "function")
|
|
202
|
+
for cn, (s, _e) in res.class_defs.items():
|
|
203
|
+
_add(cn, s + 1, "class")
|
|
204
|
+
for vn, (s, _e) in res.var_defs.items():
|
|
205
|
+
_add(vn, s + 1, "variable")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Module-level cache to avoid re-indexing within a single process.
|
|
209
|
+
_index_cache: dict[str, ProjectIndex] = {}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_project_index(
|
|
213
|
+
root: str | Path,
|
|
214
|
+
force_rebuild: bool = False,
|
|
215
|
+
cache_dir: str | Path | None = None,
|
|
216
|
+
) -> ProjectIndex:
|
|
217
|
+
"""Return (and cache) the project index for *root*.
|
|
218
|
+
|
|
219
|
+
``ProjectIndex.cached`` reports whether the result came from the in-memory
|
|
220
|
+
or on-disk cache (``False`` when it was rebuilt from source).
|
|
221
|
+
"""
|
|
222
|
+
root = Path(root)
|
|
223
|
+
resolved = root.resolve()
|
|
224
|
+
cache_dir = Path(
|
|
225
|
+
cache_dir
|
|
226
|
+
or os.environ.get("HONESTCODE_INDEX_DIR")
|
|
227
|
+
or os.path.expanduser("~/.cache/honestcode")
|
|
228
|
+
)
|
|
229
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
cache_path = cache_dir / f"{resolved.name}_{_stable_hash(str(resolved))}.json"
|
|
231
|
+
|
|
232
|
+
if not force_rebuild:
|
|
233
|
+
cached = _index_cache.get(str(resolved))
|
|
234
|
+
if cached is not None:
|
|
235
|
+
return cached
|
|
236
|
+
|
|
237
|
+
if not _should_rebuild(resolved, cache_path):
|
|
238
|
+
try:
|
|
239
|
+
data = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
240
|
+
symbols = {n: SymbolInfo(**i) for n, i in data.get("symbols", {}).items()}
|
|
241
|
+
idx = ProjectIndex(
|
|
242
|
+
root=data.get("root", str(resolved)),
|
|
243
|
+
symbols=symbols,
|
|
244
|
+
files=data.get("files", []),
|
|
245
|
+
cache_key=data.get("cache_key", ""),
|
|
246
|
+
cached=True,
|
|
247
|
+
)
|
|
248
|
+
_index_cache[str(resolved)] = idx
|
|
249
|
+
return idx
|
|
250
|
+
except Exception as e: # noqa: BLE001
|
|
251
|
+
logger.warning("Failed to load cache %s: %s", cache_path, e)
|
|
252
|
+
|
|
253
|
+
extractor = StructureExtractor()
|
|
254
|
+
idx = _index_directory(resolved, extractor)
|
|
255
|
+
idx.cached = False
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
cache_path.write_text(
|
|
259
|
+
json.dumps(
|
|
260
|
+
{
|
|
261
|
+
"root": idx.root,
|
|
262
|
+
"symbols": {n: asdict(i) for n, i in idx.symbols.items()},
|
|
263
|
+
"files": idx.files,
|
|
264
|
+
"cache_key": idx.cache_key,
|
|
265
|
+
"hashes": _file_hashes(resolved),
|
|
266
|
+
},
|
|
267
|
+
indent=2,
|
|
268
|
+
),
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
)
|
|
271
|
+
except Exception as e: # noqa: BLE001
|
|
272
|
+
logger.warning("Failed to write cache %s: %s", cache_path, e)
|
|
273
|
+
|
|
274
|
+
_index_cache[str(resolved)] = idx
|
|
275
|
+
return idx
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _stable_hash(text: str) -> str:
|
|
279
|
+
return hashlib.sha256(text.encode()).hexdigest()[:16]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _file_hashes(root: Path) -> dict[str, str]:
|
|
283
|
+
hashes: dict[str, str] = {}
|
|
284
|
+
for p in sorted(root.rglob("*.py")):
|
|
285
|
+
if p.name.startswith(".") or "__pycache__" in p.parts:
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
hashes[str(p.relative_to(root))] = hashlib.sha256(p.read_bytes()).hexdigest()
|
|
289
|
+
except Exception: # noqa: BLE001
|
|
290
|
+
continue
|
|
291
|
+
return hashes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""MCP server integration for HonestCode.
|
|
2
|
+
|
|
3
|
+
Import directly from submodules to avoid loading the MCP server eagerly:
|
|
4
|
+
|
|
5
|
+
from honestcode.mcp.tools import scan_file
|
|
6
|
+
from honestcode.mcp.server import main
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"APIKnowledgeBase",
|
|
13
|
+
"APISignature",
|
|
14
|
+
"check_api",
|
|
15
|
+
"check_symbol",
|
|
16
|
+
"choose_tool",
|
|
17
|
+
"execute_code",
|
|
18
|
+
"explore_call_graph",
|
|
19
|
+
"find_dead_code",
|
|
20
|
+
"find_similar_code",
|
|
21
|
+
"get_project_stats",
|
|
22
|
+
"index_project",
|
|
23
|
+
"load_package_apis",
|
|
24
|
+
"load_project_deps",
|
|
25
|
+
"main",
|
|
26
|
+
"mcp",
|
|
27
|
+
"scan_file",
|
|
28
|
+
"search_code",
|
|
29
|
+
"validate_types",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def __getattr__(name: str):
|
|
34
|
+
if name in ("main", "mcp"):
|
|
35
|
+
from honestcode.mcp.server import main, mcp
|
|
36
|
+
|
|
37
|
+
return {"main": main, "mcp": mcp}[name]
|
|
38
|
+
if name in ("APIKnowledgeBase", "APISignature"):
|
|
39
|
+
from honestcode.mcp.knowledge_base import APIKnowledgeBase, APISignature
|
|
40
|
+
|
|
41
|
+
return {"APIKnowledgeBase": APIKnowledgeBase, "APISignature": APISignature}[name]
|
|
42
|
+
if name in __all__:
|
|
43
|
+
from honestcode.mcp import tools
|
|
44
|
+
|
|
45
|
+
return getattr(tools, name)
|
|
46
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge base of dependency APIs for HonestCode.
|
|
3
|
+
|
|
4
|
+
Loads API signatures from installed packages via ``pydoc`` / AST inspection
|
|
5
|
+
and stores them for fast lookup during hallucination checks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import logging
|
|
12
|
+
import pkgutil
|
|
13
|
+
import threading
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
__all__ = ["APISignature", "APIKnowledgeBase"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class APISignature:
|
|
23
|
+
name: str
|
|
24
|
+
signature: str = ""
|
|
25
|
+
doc: str = ""
|
|
26
|
+
module: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class APIKnowledgeBase:
|
|
30
|
+
"""Lightweight cache of public API signatures for known packages."""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._lock = threading.RLock()
|
|
34
|
+
self.apis: dict[str, APISignature] = {}
|
|
35
|
+
self._loaded_packages: set[str] = set()
|
|
36
|
+
|
|
37
|
+
def load_package(self, package_name: str) -> int:
|
|
38
|
+
"""Extract public API signatures from an installed package.
|
|
39
|
+
|
|
40
|
+
Imports happen outside the lock to avoid blocking concurrent readers,
|
|
41
|
+
but the shared cache is updated under the lock.
|
|
42
|
+
"""
|
|
43
|
+
with self._lock:
|
|
44
|
+
if package_name in self._loaded_packages:
|
|
45
|
+
return sum(1 for v in self.apis.values() if v.module.startswith(package_name))
|
|
46
|
+
self._loaded_packages.add(package_name)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
mod = importlib.import_module(package_name)
|
|
50
|
+
except Exception as e: # noqa: BLE001
|
|
51
|
+
logger.warning("Cannot import %s: %s", package_name, e)
|
|
52
|
+
with self._lock:
|
|
53
|
+
self._loaded_packages.discard(package_name)
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
new_apis: dict[str, APISignature] = {}
|
|
57
|
+
seen: set[str] = set()
|
|
58
|
+
count = self._collect_from_module(mod, package_name, package_name, new_apis, seen)
|
|
59
|
+
|
|
60
|
+
# Recurse into submodules (one level) to enrich the base.
|
|
61
|
+
try:
|
|
62
|
+
paths = getattr(mod, "__path__", None)
|
|
63
|
+
if paths is not None:
|
|
64
|
+
for info in pkgutil.iter_modules(paths):
|
|
65
|
+
if info.name.startswith("_"):
|
|
66
|
+
continue
|
|
67
|
+
sub = f"{package_name}.{info.name}"
|
|
68
|
+
try:
|
|
69
|
+
submod = importlib.import_module(sub)
|
|
70
|
+
except Exception as e: # noqa: BLE001
|
|
71
|
+
logger.debug("Cannot import submodule %s: %s", sub, e)
|
|
72
|
+
continue
|
|
73
|
+
count += self._collect_from_module(submod, sub, sub, new_apis, seen)
|
|
74
|
+
except Exception as e: # noqa: BLE001
|
|
75
|
+
logger.warning("Failed to enumerate submodules of %s: %s", package_name, e)
|
|
76
|
+
|
|
77
|
+
with self._lock:
|
|
78
|
+
self.apis.update(new_apis)
|
|
79
|
+
|
|
80
|
+
logger.info("Loaded %d APIs from %s", count, package_name)
|
|
81
|
+
return count
|
|
82
|
+
|
|
83
|
+
def _collect_from_module(
|
|
84
|
+
self,
|
|
85
|
+
mod,
|
|
86
|
+
module_name: str,
|
|
87
|
+
api_module: str,
|
|
88
|
+
out: dict[str, APISignature],
|
|
89
|
+
seen: set[str],
|
|
90
|
+
) -> int:
|
|
91
|
+
"""Collect public attributes from a module into *out*.
|
|
92
|
+
|
|
93
|
+
Returns the number of newly added signatures.
|
|
94
|
+
"""
|
|
95
|
+
count = 0
|
|
96
|
+
try:
|
|
97
|
+
names = dir(mod)
|
|
98
|
+
except Exception as e: # noqa: BLE001
|
|
99
|
+
logger.warning("dir(%s) failed: %s", module_name, e)
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
for name in names:
|
|
103
|
+
if name.startswith("_"):
|
|
104
|
+
continue
|
|
105
|
+
full = f"{api_module}.{name}"
|
|
106
|
+
if full in seen:
|
|
107
|
+
continue
|
|
108
|
+
seen.add(full)
|
|
109
|
+
try:
|
|
110
|
+
obj = getattr(mod, name)
|
|
111
|
+
sig = self._signature(obj)
|
|
112
|
+
except Exception: # noqa: BLE001
|
|
113
|
+
sig = ""
|
|
114
|
+
out[full] = APISignature(name=full, signature=sig, module=api_module)
|
|
115
|
+
count += 1
|
|
116
|
+
return count
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _signature(obj) -> str:
|
|
120
|
+
try:
|
|
121
|
+
import inspect
|
|
122
|
+
|
|
123
|
+
return str(inspect.signature(obj))
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
def get(self, name: str) -> APISignature | None:
|
|
128
|
+
with self._lock:
|
|
129
|
+
return self.apis.get(name)
|
|
130
|
+
|
|
131
|
+
def has(self, name: str) -> bool:
|
|
132
|
+
with self._lock:
|
|
133
|
+
return name in self.apis
|
|
134
|
+
|
|
135
|
+
def search(self, prefix: str, limit: int = 10) -> list[str]:
|
|
136
|
+
with self._lock:
|
|
137
|
+
return [k for k in self.apis if k.startswith(prefix)][:limit]
|
|
138
|
+
|
|
139
|
+
def all_names(self) -> set[str]:
|
|
140
|
+
with self._lock:
|
|
141
|
+
return set(self.apis.keys())
|