cg-code-graph 0.10.1__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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Python source roots: the directories whose contents are importable by top-level name (what `sys.path` holds when
|
|
2
|
+
the code runs), each with the reason it was chosen.
|
|
3
|
+
|
|
4
|
+
Detection (no configuration needed), in this order of evidence:
|
|
5
|
+
* the indexed root;
|
|
6
|
+
* packaging config at the root and in nested projects: `pyproject.toml` (`[tool.setuptools] package-dir`,
|
|
7
|
+
`packages.find.where`, `[tool.hatch.build] packages / sources`, `[tool.poetry] packages = [{from = ...}]`,
|
|
8
|
+
`[tool.pdm.build] package-dir`, `[tool.maturin] python-source`), `setup.cfg` (`[options] package_dir`,
|
|
9
|
+
`[options.packages.find] where`) and literal `package_dir=` / `find_packages(where)` in `setup.py`;
|
|
10
|
+
* conventional directories `src/`, `lib/`, `python/` that are not packages themselves;
|
|
11
|
+
* nested projects: directories holding `pyproject.toml`, `setup.py`, `setup.cfg` or `manage.py` (depth <= 4);
|
|
12
|
+
* namespace packages (PEP 420): a directory without `__init__.py` that other files import as a package
|
|
13
|
+
(`import acme.core` -> the parent of `acme/` is a root);
|
|
14
|
+
* the parent of every top-level package (a directory with `__init__.py` whose parent has none).
|
|
15
|
+
Configured roots (`python.source_roots` in `.cg.yaml`, or `cg index --python-root`) replace detection.
|
|
16
|
+
|
|
17
|
+
A file reachable from several roots gets one canonical module name: the one under the root whose top-level name the
|
|
18
|
+
project's own imports use most (ties: the deepest root). The other names stay importable aliases."""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
import configparser
|
|
23
|
+
from collections import Counter
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path, PurePosixPath
|
|
26
|
+
|
|
27
|
+
NESTED_MARKERS = ("pyproject.toml", "setup.py", "setup.cfg", "manage.py")
|
|
28
|
+
CONVENTIONAL = ("src", "lib", "python")
|
|
29
|
+
NESTED_DEPTH = 4
|
|
30
|
+
MAX_RECORDED = 200
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SourceRoot:
|
|
35
|
+
path: str # repo-relative directory, "" = the indexed root
|
|
36
|
+
origin: str # detected | configured | flag
|
|
37
|
+
why: list = field(default_factory=list)
|
|
38
|
+
prefix: str = "" # package name the directory maps to (setuptools package-dir {"pkg": "dir"})
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def key(self) -> tuple:
|
|
42
|
+
return (self.path, self.prefix)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def depth(self) -> int:
|
|
46
|
+
return len(PurePosixPath(self.path).parts) if self.path else 0
|
|
47
|
+
|
|
48
|
+
def label(self) -> str:
|
|
49
|
+
return (self.path or ".") + "/"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _rel(base: str, sub: str) -> str | None:
|
|
53
|
+
"""Join a packaging-config path to the project dir; None when it leaves the indexed root."""
|
|
54
|
+
p = PurePosixPath(base) / PurePosixPath(str(sub).strip().replace("\\", "/") or ".")
|
|
55
|
+
parts = []
|
|
56
|
+
for x in p.parts:
|
|
57
|
+
if x in (".", ""):
|
|
58
|
+
continue
|
|
59
|
+
if x == "..":
|
|
60
|
+
if not parts:
|
|
61
|
+
return None
|
|
62
|
+
parts.pop()
|
|
63
|
+
else:
|
|
64
|
+
parts.append(x)
|
|
65
|
+
return "/".join(parts)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _pyproject_roots(path: Path) -> list[tuple[str, str, str]]:
|
|
69
|
+
"""[(dir, prefix, why)] from a pyproject.toml (dirs relative to its directory)."""
|
|
70
|
+
try:
|
|
71
|
+
import tomllib
|
|
72
|
+
d = tomllib.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
73
|
+
except Exception: # noqa: BLE001 (unreadable / invalid TOML: no packaging evidence)
|
|
74
|
+
return []
|
|
75
|
+
tool = d.get("tool") if isinstance(d.get("tool"), dict) else {}
|
|
76
|
+
out = []
|
|
77
|
+
st = tool.get("setuptools") if isinstance(tool.get("setuptools"), dict) else {}
|
|
78
|
+
pd = st.get("package-dir")
|
|
79
|
+
if isinstance(pd, dict):
|
|
80
|
+
for k, v in pd.items():
|
|
81
|
+
if isinstance(k, str) and isinstance(v, str):
|
|
82
|
+
out.append(_mapping(k, v, "pyproject.toml [tool.setuptools] package-dir"))
|
|
83
|
+
pk = st.get("packages")
|
|
84
|
+
find = pk.get("find") if isinstance(pk, dict) else None
|
|
85
|
+
if isinstance(find, dict):
|
|
86
|
+
where = find.get("where")
|
|
87
|
+
for w in ([where] if isinstance(where, str) else where if isinstance(where, list) else []):
|
|
88
|
+
if isinstance(w, str):
|
|
89
|
+
out.append((w, "", "pyproject.toml [tool.setuptools.packages.find] where"))
|
|
90
|
+
hatch = tool.get("hatch") if isinstance(tool.get("hatch"), dict) else {}
|
|
91
|
+
build = hatch.get("build") if isinstance(hatch.get("build"), dict) else {}
|
|
92
|
+
targets = build.get("targets") if isinstance(build.get("targets"), dict) else {}
|
|
93
|
+
for where, sect in [("[tool.hatch.build]", build)] + [(f"[tool.hatch.build.targets.{t}]", v) for t, v in targets.items()
|
|
94
|
+
if isinstance(v, dict)]:
|
|
95
|
+
for p in sect.get("packages") or []:
|
|
96
|
+
if isinstance(p, str):
|
|
97
|
+
parent = str(PurePosixPath(p).parent)
|
|
98
|
+
out.append((parent, "", f"pyproject.toml {where} packages"))
|
|
99
|
+
src = sect.get("sources")
|
|
100
|
+
if isinstance(src, list):
|
|
101
|
+
out += [(s, "", f"pyproject.toml {where} sources") for s in src if isinstance(s, str)]
|
|
102
|
+
elif isinstance(src, dict):
|
|
103
|
+
for k, v in src.items():
|
|
104
|
+
if isinstance(k, str) and isinstance(v, str):
|
|
105
|
+
out.append((k, v.strip("/").replace("/", "."), f"pyproject.toml {where} sources"))
|
|
106
|
+
poetry = tool.get("poetry") if isinstance(tool.get("poetry"), dict) else {}
|
|
107
|
+
for p in poetry.get("packages") or []:
|
|
108
|
+
if isinstance(p, dict) and isinstance(p.get("from"), str):
|
|
109
|
+
out.append((p["from"], "", "pyproject.toml [tool.poetry] packages from"))
|
|
110
|
+
pdm = tool.get("pdm") if isinstance(tool.get("pdm"), dict) else {}
|
|
111
|
+
for where, sect in (("[tool.pdm.build]", pdm.get("build")), ("[tool.pdm]", pdm)):
|
|
112
|
+
if isinstance(sect, dict) and isinstance(sect.get("package-dir"), str):
|
|
113
|
+
out.append((sect["package-dir"], "", f"pyproject.toml {where} package-dir"))
|
|
114
|
+
mat = tool.get("maturin") if isinstance(tool.get("maturin"), dict) else {}
|
|
115
|
+
if isinstance(mat.get("python-source"), str):
|
|
116
|
+
out.append((mat["python-source"], "", "pyproject.toml [tool.maturin] python-source"))
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _mapping(pkg: str, d: str, why: str) -> tuple[str, str, str]:
|
|
121
|
+
"""setuptools package_dir entry: {"": "src"} -> root src; {"core": "lib/core"} -> root lib; {"core": "lib"} ->
|
|
122
|
+
lib/ holds the contents of package core."""
|
|
123
|
+
pkg = pkg.strip()
|
|
124
|
+
d = d.strip()
|
|
125
|
+
if not pkg:
|
|
126
|
+
return (d, "", why)
|
|
127
|
+
last = pkg.split(".")[-1]
|
|
128
|
+
if PurePosixPath(d).name == last:
|
|
129
|
+
parent = str(PurePosixPath(d).parent)
|
|
130
|
+
return (parent, ".".join(pkg.split(".")[:-1]), why)
|
|
131
|
+
return (d, pkg, why)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _setup_cfg_roots(path: Path) -> list[tuple[str, str, str]]:
|
|
135
|
+
cp = configparser.ConfigParser(interpolation=None)
|
|
136
|
+
try:
|
|
137
|
+
cp.read_string(path.read_text(encoding="utf-8", errors="replace"))
|
|
138
|
+
except Exception: # noqa: BLE001
|
|
139
|
+
return []
|
|
140
|
+
out = []
|
|
141
|
+
if cp.has_option("options", "package_dir"):
|
|
142
|
+
for line in cp.get("options", "package_dir").splitlines():
|
|
143
|
+
if "=" in line:
|
|
144
|
+
k, v = line.split("=", 1)
|
|
145
|
+
if v.strip():
|
|
146
|
+
out.append(_mapping(k, v, "setup.cfg [options] package_dir"))
|
|
147
|
+
if cp.has_option("options.packages.find", "where"):
|
|
148
|
+
for w in cp.get("options.packages.find", "where").split():
|
|
149
|
+
out.append((w, "", "setup.cfg [options.packages.find] where"))
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _setup_py_roots(path: Path) -> list[tuple[str, str, str]]:
|
|
154
|
+
try:
|
|
155
|
+
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
|
|
156
|
+
except (SyntaxError, ValueError, RecursionError, OSError):
|
|
157
|
+
return []
|
|
158
|
+
out = []
|
|
159
|
+
for n in ast.walk(tree):
|
|
160
|
+
if not isinstance(n, ast.Call):
|
|
161
|
+
continue
|
|
162
|
+
fname = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id if isinstance(n.func, ast.Name) else ""
|
|
163
|
+
if fname == "setup":
|
|
164
|
+
for k in n.keywords:
|
|
165
|
+
if k.arg == "package_dir" and isinstance(k.value, ast.Dict):
|
|
166
|
+
for kk, vv in zip(k.value.keys, k.value.values):
|
|
167
|
+
if isinstance(kk, ast.Constant) and isinstance(kk.value, str) and \
|
|
168
|
+
isinstance(vv, ast.Constant) and isinstance(vv.value, str):
|
|
169
|
+
out.append(_mapping(kk.value, vv.value, "setup.py package_dir"))
|
|
170
|
+
elif fname in ("find_packages", "find_namespace_packages"):
|
|
171
|
+
w = n.args[0] if n.args else next((k.value for k in n.keywords if k.arg == "where"), None)
|
|
172
|
+
if isinstance(w, ast.Constant) and isinstance(w.value, str):
|
|
173
|
+
out.append((w.value, "", f"setup.py {fname}({w.value!r})"))
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def packaging_roots(root: Path, project_dir: str) -> list[tuple[str, str, str]]:
|
|
178
|
+
"""Source roots stated by the packaging config of the project at `project_dir` (repo-relative)."""
|
|
179
|
+
base = root / project_dir if project_dir else root
|
|
180
|
+
out = []
|
|
181
|
+
for fn, fnc in (("pyproject.toml", _pyproject_roots), ("setup.cfg", _setup_cfg_roots), ("setup.py", _setup_py_roots)):
|
|
182
|
+
p = base / fn
|
|
183
|
+
if p.is_file():
|
|
184
|
+
for d, prefix, why in fnc(p):
|
|
185
|
+
rel = _rel(project_dir, d)
|
|
186
|
+
if rel is not None:
|
|
187
|
+
where = f"{project_dir}/{why}" if project_dir else why
|
|
188
|
+
out.append((rel, prefix, where))
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def import_names(trees) -> Counter:
|
|
193
|
+
"""Absolute dotted names the given module ASTs import, with how often: `import a.b` -> a.b; `from a.b import c`
|
|
194
|
+
-> a.b and a.b.c (c may be a submodule)."""
|
|
195
|
+
out: Counter = Counter()
|
|
196
|
+
for imps in trees:
|
|
197
|
+
for st in imps:
|
|
198
|
+
if isinstance(st, ast.Import):
|
|
199
|
+
for a in st.names:
|
|
200
|
+
out[a.name] += 1
|
|
201
|
+
elif isinstance(st, ast.ImportFrom) and not st.level and st.module:
|
|
202
|
+
out[st.module] += 1
|
|
203
|
+
for a in st.names:
|
|
204
|
+
if a.name != "*":
|
|
205
|
+
out[f"{st.module}.{a.name}"] += 1
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class RootPlan:
|
|
210
|
+
"""The chosen source roots, plus the canonical module name of every file."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, root: Path, files: list[str], imports: Counter, configured: list[str] | None = None,
|
|
213
|
+
origin: str = "configured", skip_dirs=frozenset()):
|
|
214
|
+
self.root = root
|
|
215
|
+
self.files = files
|
|
216
|
+
self.imports = imports
|
|
217
|
+
self.warnings: list[str] = []
|
|
218
|
+
self.ambiguous: list[dict] = []
|
|
219
|
+
self.collisions: list[dict] = []
|
|
220
|
+
self.n_collisions = 0 # files whose preferred module name another file holds
|
|
221
|
+
self.n_requalified = 0 # files named by their path from the indexed root because their tree clashed
|
|
222
|
+
self.mode = origin if configured is not None else "detected"
|
|
223
|
+
self.roots: dict[tuple, SourceRoot] = {}
|
|
224
|
+
self._skip = skip_dirs
|
|
225
|
+
mods = [f[:-3] for f in files]
|
|
226
|
+
self.file_set = set(files)
|
|
227
|
+
self.dirs: set[str] = set()
|
|
228
|
+
for f in files:
|
|
229
|
+
p = PurePosixPath(f).parent
|
|
230
|
+
while str(p) != ".":
|
|
231
|
+
self.dirs.add(str(p))
|
|
232
|
+
p = p.parent
|
|
233
|
+
self.pkg_dirs = {str(PurePosixPath(m).parent) for m in mods if PurePosixPath(m).name == "__init__"}
|
|
234
|
+
self.pkg_dirs.discard(".")
|
|
235
|
+
self.root_is_pkg = "__init__.py" in self.file_set
|
|
236
|
+
if configured is not None:
|
|
237
|
+
for c in configured:
|
|
238
|
+
if c and not (root / c).is_dir():
|
|
239
|
+
self.warnings.append(f"configured source root {c}/ does not exist")
|
|
240
|
+
continue
|
|
241
|
+
self._add(c, origin, "configured in .cg.yaml" if origin == "configured" else "--python-root")
|
|
242
|
+
if not self.roots:
|
|
243
|
+
self.warnings.append("no source root: none of the configured source roots exists")
|
|
244
|
+
else:
|
|
245
|
+
self._detect()
|
|
246
|
+
|
|
247
|
+
# ---- detection
|
|
248
|
+
def _add(self, path: str, origin: str, why: str, prefix: str = "") -> None:
|
|
249
|
+
if path and path not in self.dirs and origin == "detected":
|
|
250
|
+
return # no .py file below it
|
|
251
|
+
r = self.roots.get((path, prefix))
|
|
252
|
+
if r is None:
|
|
253
|
+
self.roots[(path, prefix)] = SourceRoot(path, origin, [why], prefix)
|
|
254
|
+
elif why not in r.why:
|
|
255
|
+
r.why.append(why)
|
|
256
|
+
|
|
257
|
+
def _nested_projects(self) -> list[str]:
|
|
258
|
+
"""Directories (depth <= NESTED_DEPTH) holding a project marker, not packages themselves, with .py files below."""
|
|
259
|
+
out = []
|
|
260
|
+
for d in sorted(self.dirs):
|
|
261
|
+
if d.count("/") + 1 > NESTED_DEPTH or d in self.pkg_dirs:
|
|
262
|
+
continue
|
|
263
|
+
if any(x in self._skip for x in d.split("/")):
|
|
264
|
+
continue
|
|
265
|
+
if f"{d}/manage.py" in self.file_set or any((self.root / d / m).is_file() for m in NESTED_MARKERS[:3]):
|
|
266
|
+
out.append(d)
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
def _detect(self) -> None:
|
|
270
|
+
self._add("", "detected", "indexed root")
|
|
271
|
+
projects = [""] + self._nested_projects()
|
|
272
|
+
for pd in projects:
|
|
273
|
+
if pd:
|
|
274
|
+
marker = next(m for m in NESTED_MARKERS if (self.root / pd / m).exists())
|
|
275
|
+
self._add(pd, "detected", f"nested project ({pd}/{marker})")
|
|
276
|
+
for d, prefix, why in packaging_roots(self.root, pd):
|
|
277
|
+
self._add(d, "detected", why, prefix)
|
|
278
|
+
for c in CONVENTIONAL:
|
|
279
|
+
d = f"{pd}/{c}" if pd else c
|
|
280
|
+
if d in self.dirs and d not in self.pkg_dirs:
|
|
281
|
+
self._add(d, "detected", f"{c}/ directory")
|
|
282
|
+
# namespace packages: a directory without __init__.py imported as a package (acme.core -> parent of acme/)
|
|
283
|
+
by_name: dict[str, list[str]] = {}
|
|
284
|
+
for d in self.dirs:
|
|
285
|
+
if d not in self.pkg_dirs:
|
|
286
|
+
by_name.setdefault(PurePosixPath(d).name, []).append(d)
|
|
287
|
+
ns_dirs = set()
|
|
288
|
+
for name in sorted(self.imports):
|
|
289
|
+
parts = name.split(".")
|
|
290
|
+
if len(parts) < 2 or parts[0] not in by_name:
|
|
291
|
+
continue
|
|
292
|
+
for d in by_name[parts[0]]:
|
|
293
|
+
sub = f"{d}/{parts[1]}"
|
|
294
|
+
if sub not in self.dirs and f"{sub}.py" not in self.file_set:
|
|
295
|
+
continue
|
|
296
|
+
parent = str(PurePosixPath(d).parent)
|
|
297
|
+
parent = "" if parent == "." else parent
|
|
298
|
+
if parent in self.pkg_dirs or (not parent and self.root_is_pkg):
|
|
299
|
+
continue
|
|
300
|
+
ns_dirs.add(d)
|
|
301
|
+
self._add(parent, "detected", f"namespace package {parts[0]} (imported as {parts[0]}.{parts[1]})")
|
|
302
|
+
# the parent of every top-level package (several package roots in one repo)
|
|
303
|
+
for d in sorted(self.pkg_dirs):
|
|
304
|
+
name = PurePosixPath(d).name
|
|
305
|
+
parent = str(PurePosixPath(d).parent)
|
|
306
|
+
parent = "" if parent == "." else parent
|
|
307
|
+
if not name.isidentifier() or parent in self.pkg_dirs or parent in ns_dirs:
|
|
308
|
+
continue
|
|
309
|
+
if any(parent == n or parent.startswith(n + "/") for n in ns_dirs):
|
|
310
|
+
continue # inside a namespace package: the namespace's parent is the root
|
|
311
|
+
self._add(parent, "detected", f"parent of top-level package {name}")
|
|
312
|
+
|
|
313
|
+
# ---- naming
|
|
314
|
+
def candidates(self, rel: str) -> list[tuple[str, bool, SourceRoot]]:
|
|
315
|
+
"""Importable (name, is_pkg, root) of a file under every root that contains it."""
|
|
316
|
+
p = PurePosixPath(rel)
|
|
317
|
+
mp = list(p.with_suffix("").parts)
|
|
318
|
+
out = []
|
|
319
|
+
for i in range(len(mp)): # i = number of leading path parts that form the root directory
|
|
320
|
+
base = "/".join(mp[:i])
|
|
321
|
+
for prefix in self._prefixes.get(base, ()):
|
|
322
|
+
r = self.roots[(base, prefix)]
|
|
323
|
+
sub = mp[i:]
|
|
324
|
+
is_pkg = sub[-1] == "__init__"
|
|
325
|
+
if is_pkg:
|
|
326
|
+
sub = sub[:-1]
|
|
327
|
+
if prefix:
|
|
328
|
+
sub = prefix.split(".") + sub
|
|
329
|
+
if sub and all(x.isidentifier() for x in sub):
|
|
330
|
+
out.append((".".join(sub), is_pkg, r))
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
def assign(self) -> tuple[dict, list[str]]:
|
|
334
|
+
"""{file: (canonical name, is_pkg, [alias names])} and the files no root maps (unmapped)."""
|
|
335
|
+
self._prefixes: dict[str, list[str]] = {}
|
|
336
|
+
for (path, prefix) in self.roots:
|
|
337
|
+
self._prefixes.setdefault(path, []).append(prefix)
|
|
338
|
+
cands = {f: self.candidates(f) for f in self.files}
|
|
339
|
+
# import evidence per (root, top-level name): how often the project's own imports use names under it
|
|
340
|
+
index: dict[str, list[tuple]] = {}
|
|
341
|
+
for f, cs in cands.items():
|
|
342
|
+
for name, _, r in cs:
|
|
343
|
+
index.setdefault(name, []).append((r.key, name.split(".")[0]))
|
|
344
|
+
score: Counter = Counter()
|
|
345
|
+
for name, n in self.imports.items():
|
|
346
|
+
for k in set(index.get(name, ())):
|
|
347
|
+
score[k] += n
|
|
348
|
+
prefs: dict[str, list] = {}
|
|
349
|
+
out, unmapped = {}, []
|
|
350
|
+
for f, cs in cands.items():
|
|
351
|
+
ranked = sorted(cs, key=lambda c: (-score[(c[2].key, c[0].split(".")[0])], -c[2].depth, len(c[0])))
|
|
352
|
+
prefs[f] = ranked
|
|
353
|
+
used = [c for c in ranked if score[(c[2].key, c[0].split(".")[0])] > 0]
|
|
354
|
+
if len(used) > 1 and len(self.ambiguous) < MAX_RECORDED:
|
|
355
|
+
self.ambiguous.append({"file": f, "chosen": used[0][0], "also": [c[0] for c in used[1:3]],
|
|
356
|
+
"imports": [score[(c[2].key, c[0].split(".")[0])] for c in used[:3]]})
|
|
357
|
+
# one module name per file. A package tree (files preferring the same root and top-level name) keeps its names
|
|
358
|
+
# together: when another tree already holds one of them, the whole tree falls back to its path from the indexed
|
|
359
|
+
# root (unique, and relative imports inside the tree still resolve). Trees with more import evidence go first,
|
|
360
|
+
# then trees without another importable name, then the shallower root.
|
|
361
|
+
groups: dict[tuple, list[str]] = {}
|
|
362
|
+
for f, ranked in prefs.items():
|
|
363
|
+
if ranked:
|
|
364
|
+
groups.setdefault((ranked[0][2].key, ranked[0][0].split(".")[0]), []).append(f)
|
|
365
|
+
else:
|
|
366
|
+
unmapped.append(f)
|
|
367
|
+
|
|
368
|
+
def gkey(item):
|
|
369
|
+
(rk, anchor), fs = item
|
|
370
|
+
alt = any(len(prefs[f]) > 1 for f in fs)
|
|
371
|
+
return (-score[(rk, anchor)], alt, len(PurePosixPath(rk[0]).parts) if rk[0] else 0, rk, anchor)
|
|
372
|
+
|
|
373
|
+
taken: dict[str, str] = {}
|
|
374
|
+
for (rk, anchor), fs in sorted(groups.items(), key=gkey):
|
|
375
|
+
fs = sorted(fs, key=lambda f: (not prefs[f][0][1], f)) # pkg/__init__.py before a pkg.py twin
|
|
376
|
+
clash = next((f for f in fs if prefs[f][0][0] in taken), None)
|
|
377
|
+
for f in fs:
|
|
378
|
+
c = prefs[f][0]
|
|
379
|
+
name, is_pkg, root = c[0], c[1], c[2]
|
|
380
|
+
if clash is not None or name in taken:
|
|
381
|
+
mp = list(PurePosixPath(f).with_suffix("").parts)
|
|
382
|
+
if mp[-1] == "__init__":
|
|
383
|
+
mp = mp[:-1]
|
|
384
|
+
fallback = ".".join(mp)
|
|
385
|
+
ok = bool(fallback) and fallback not in taken
|
|
386
|
+
if name in taken:
|
|
387
|
+
self.n_collisions += 1
|
|
388
|
+
if len(self.collisions) < MAX_RECORDED:
|
|
389
|
+
self.collisions.append({"file": f, "name": name, "with": taken[name],
|
|
390
|
+
**({"named": fallback} if ok else {})})
|
|
391
|
+
if not ok:
|
|
392
|
+
unmapped.append(f)
|
|
393
|
+
continue
|
|
394
|
+
self.n_requalified += 1
|
|
395
|
+
name, root = fallback, None
|
|
396
|
+
taken[name] = f
|
|
397
|
+
out[f] = (name, is_pkg, [x[0] for x in prefs[f] if x[0] != name], root)
|
|
398
|
+
return out, sorted(unmapped)
|
|
399
|
+
|
|
400
|
+
def report(self, owners: Counter) -> list[dict]:
|
|
401
|
+
"""Roots that own at least one canonical module, with origin and reason (for stats / coverage)."""
|
|
402
|
+
out = []
|
|
403
|
+
for r in sorted(self.roots.values(), key=lambda r: (r.path, r.prefix)):
|
|
404
|
+
n = owners.get(r.key, 0)
|
|
405
|
+
if not n and self.mode == "detected":
|
|
406
|
+
continue
|
|
407
|
+
e = {"path": r.label(), "origin": r.origin, "why": "; ".join(r.why[:2]) + (f" (+{len(r.why) - 2})" if len(r.why) > 2 else ""),
|
|
408
|
+
"modules": n}
|
|
409
|
+
if r.prefix:
|
|
410
|
+
e["package"] = r.prefix
|
|
411
|
+
out.append(e)
|
|
412
|
+
return out[:MAX_RECORDED]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""python-socketio / Flask-SocketIO events -> endpoint:socketio:<namespace>#<event> (the #31 protocol model).
|
|
2
|
+
|
|
3
|
+
Objects: module-level `sio = socketio.AsyncServer(...)` / `Server` / `flask_socketio.SocketIO(app)` (server) and
|
|
4
|
+
`socketio.AsyncClient()` / `Client` / `SimpleClient` (client), used in their module or imported by name elsewhere.
|
|
5
|
+
|
|
6
|
+
receive @sio.on('event', namespace='/ns') / @sio.event (the function name is the event) / sio.on('event', handler);
|
|
7
|
+
class namespaces (socketio.Namespace / AsyncNamespace: on_<event> methods, namespace from
|
|
8
|
+
register_namespace(Cls('/ns')))
|
|
9
|
+
send sio.emit('event', data, to=room, namespace='/ns') / sio.call(...) (role request) / sio.send(data) (event
|
|
10
|
+
`message`); f-string event names become `{param}` templates
|
|
11
|
+
guards a server namespace's `connect` handler that rejects (raises ConnectionRefusedError / returns False) is the
|
|
12
|
+
guard of every server receiver in that namespace; without one the receivers record guards [] (`unguarded`)
|
|
13
|
+
connect / disconnect / connect_error are lifecycle callbacks, not endpoints.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
|
|
19
|
+
from ...core.model import EXACT, RESOLVED
|
|
20
|
+
from ...protocols import protocol_receive, protocol_send
|
|
21
|
+
|
|
22
|
+
SERVER = {"socketio.Server", "socketio.AsyncServer", "flask_socketio.SocketIO"}
|
|
23
|
+
CLIENT = {"socketio.Client", "socketio.AsyncClient", "socketio.SimpleClient", "socketio.AsyncSimpleClient"}
|
|
24
|
+
NAMESPACE_BASES = ("socketio.Namespace", "socketio.AsyncNamespace", "socketio.ClientNamespace", "socketio.AsyncClientNamespace",
|
|
25
|
+
"flask_socketio.Namespace")
|
|
26
|
+
LIFECYCLE = {"connect", "disconnect", "connect_error"}
|
|
27
|
+
LIB = "python-socketio"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _full(m, dn: str | None) -> str | None:
|
|
31
|
+
if not dn:
|
|
32
|
+
return None
|
|
33
|
+
head, _, rest = dn.partition(".")
|
|
34
|
+
imp = m.imports.get(head)
|
|
35
|
+
if not imp:
|
|
36
|
+
return None
|
|
37
|
+
if imp[0] in ("mod", "modprefix"):
|
|
38
|
+
base = imp[1]
|
|
39
|
+
elif imp[0] == "sym":
|
|
40
|
+
base = f"{imp[1]}.{imp[2]}"
|
|
41
|
+
else:
|
|
42
|
+
return None
|
|
43
|
+
return f"{base}.{rest}" if rest else base
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _event(e) -> str | None:
|
|
47
|
+
if isinstance(e, ast.Constant) and isinstance(e.value, str):
|
|
48
|
+
return e.value
|
|
49
|
+
if isinstance(e, ast.JoinedStr):
|
|
50
|
+
out = []
|
|
51
|
+
for v in e.values:
|
|
52
|
+
if isinstance(v, ast.Constant):
|
|
53
|
+
out.append(str(v.value))
|
|
54
|
+
elif isinstance(v, ast.FormattedValue):
|
|
55
|
+
d = v.value
|
|
56
|
+
nm = d.attr if isinstance(d, ast.Attribute) else d.id if isinstance(d, ast.Name) else "param"
|
|
57
|
+
out.append("{" + nm + "}")
|
|
58
|
+
return "".join(out)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _str(e) -> str | None:
|
|
63
|
+
return e.value if isinstance(e, ast.Constant) and isinstance(e.value, str) else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _kw(call, name):
|
|
67
|
+
for k in call.keywords:
|
|
68
|
+
if k.arg == name:
|
|
69
|
+
return k.value
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _rejects(fn) -> bool:
|
|
74
|
+
for sub in ast.walk(fn):
|
|
75
|
+
if isinstance(sub, ast.Raise) and sub.exc is not None:
|
|
76
|
+
t = sub.exc.func if isinstance(sub.exc, ast.Call) else sub.exc
|
|
77
|
+
if (getattr(t, "id", None) or getattr(t, "attr", None)) in ("ConnectionRefusedError",):
|
|
78
|
+
return True
|
|
79
|
+
if isinstance(sub, ast.Return) and isinstance(sub.value, ast.Constant) and sub.value.value is False:
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def index(prog, b, walk_body) -> dict:
|
|
85
|
+
mods = [m for m in prog.modules.values()
|
|
86
|
+
if any((v[1] if v[0] != "sym" else v[1]).split(".")[0] in ("socketio", "flask_socketio") for v in m.imports.values()
|
|
87
|
+
if isinstance(v, tuple) and len(v) > 1 and isinstance(v[1], str))]
|
|
88
|
+
if not mods:
|
|
89
|
+
return {}
|
|
90
|
+
objs: dict[tuple[str, str], str] = {}
|
|
91
|
+
for m in mods:
|
|
92
|
+
for st in m.tree.body:
|
|
93
|
+
if isinstance(st, (ast.Assign, ast.AnnAssign)) and isinstance(st.value, ast.Call):
|
|
94
|
+
full = _full(m, _dn(st.value.func))
|
|
95
|
+
role = "server" if full in SERVER else "client" if full in CLIENT else None
|
|
96
|
+
tgts = st.targets if isinstance(st, ast.Assign) else [st.target]
|
|
97
|
+
if role:
|
|
98
|
+
for t in tgts:
|
|
99
|
+
if isinstance(t, ast.Name):
|
|
100
|
+
objs[(m.name, t.id)] = role
|
|
101
|
+
if not objs:
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
def obj_of(m, name):
|
|
105
|
+
if (m.name, name) in objs:
|
|
106
|
+
return objs[(m.name, name)]
|
|
107
|
+
imp = m.imports.get(name)
|
|
108
|
+
if imp and imp[0] == "sym":
|
|
109
|
+
return objs.get((imp[1], imp[2]))
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
st = {"objects": len(objs), "receivers": 0, "sends": 0}
|
|
113
|
+
recvs = [] # (namespace, event, func, line, conf, role)
|
|
114
|
+
guards: dict[str, list] = {}
|
|
115
|
+
servers_ns: set = set()
|
|
116
|
+
ns_of_class: dict[str, str] = {}
|
|
117
|
+
for m in prog.modules.values():
|
|
118
|
+
for sub in ast.walk(m.tree):
|
|
119
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr == "register_namespace" \
|
|
120
|
+
and sub.args and isinstance(sub.args[0], ast.Call):
|
|
121
|
+
cn = _dn(sub.args[0].func)
|
|
122
|
+
ns = _str(sub.args[0].args[0]) if sub.args[0].args else None
|
|
123
|
+
if cn:
|
|
124
|
+
ns_of_class[cn.split(".")[-1]] = ns or "/"
|
|
125
|
+
for f in prog.funcs.values():
|
|
126
|
+
m = f.module
|
|
127
|
+
for d in f.decorators:
|
|
128
|
+
call = d if isinstance(d, ast.Call) else None
|
|
129
|
+
fn = call.func if call else d
|
|
130
|
+
if not isinstance(fn, ast.Attribute) or not isinstance(fn.value, ast.Name):
|
|
131
|
+
continue
|
|
132
|
+
role = obj_of(m, fn.value.id)
|
|
133
|
+
if not role:
|
|
134
|
+
continue
|
|
135
|
+
ns = (_str(_kw(call, "namespace")) if call else None) or "/"
|
|
136
|
+
if fn.attr == "on" and call and call.args:
|
|
137
|
+
ev = _event(call.args[0])
|
|
138
|
+
elif fn.attr == "event":
|
|
139
|
+
ev = f.name
|
|
140
|
+
else:
|
|
141
|
+
continue
|
|
142
|
+
if not ev:
|
|
143
|
+
continue
|
|
144
|
+
if ev in LIFECYCLE:
|
|
145
|
+
if ev == "connect" and role == "server":
|
|
146
|
+
servers_ns.add(ns)
|
|
147
|
+
if _rejects(f.node):
|
|
148
|
+
guards.setdefault(ns, []).append(f"{f.name} (connect handler rejects)")
|
|
149
|
+
continue
|
|
150
|
+
recvs.append((ns, ev, f, d.lineno, EXACT, role))
|
|
151
|
+
for c in prog.classes.values():
|
|
152
|
+
if prog.subclass_of(c, *NAMESPACE_BASES):
|
|
153
|
+
client = prog.subclass_of(c, "socketio.ClientNamespace", "socketio.AsyncClientNamespace")
|
|
154
|
+
ns = ns_of_class.get(c.name, "/")
|
|
155
|
+
for name, f in c.methods.items():
|
|
156
|
+
if not name.startswith("on_"):
|
|
157
|
+
continue
|
|
158
|
+
ev = name[3:]
|
|
159
|
+
if ev in LIFECYCLE:
|
|
160
|
+
if ev == "connect" and not client:
|
|
161
|
+
servers_ns.add(ns)
|
|
162
|
+
if _rejects(f.node):
|
|
163
|
+
guards.setdefault(ns, []).append(f"{c.name}.{name} (connect handler rejects)")
|
|
164
|
+
continue
|
|
165
|
+
recvs.append((ns, ev, f, f.line, RESOLVED, "client" if client else "server"))
|
|
166
|
+
for f in prog.funcs.values():
|
|
167
|
+
m = f.module
|
|
168
|
+
for sub in walk_body(f.node):
|
|
169
|
+
if not (isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and isinstance(sub.func.value, ast.Name)):
|
|
170
|
+
continue
|
|
171
|
+
role = obj_of(m, sub.func.value.id)
|
|
172
|
+
if not role:
|
|
173
|
+
continue
|
|
174
|
+
a = sub.func.attr
|
|
175
|
+
ns = _str(_kw(sub, "namespace")) or "/"
|
|
176
|
+
if a == "on" and len(sub.args) >= 2: # sio.on('event', handler)
|
|
177
|
+
ev = _event(sub.args[0])
|
|
178
|
+
h = prog.infer(sub.args[1], _ctx(prog, f))
|
|
179
|
+
if ev and ev not in LIFECYCLE and h and h[0] in ("func", "bound"):
|
|
180
|
+
recvs.append((ns, ev, h[1], sub.lineno, RESOLVED, role))
|
|
181
|
+
continue
|
|
182
|
+
if a not in ("emit", "call", "send"):
|
|
183
|
+
continue
|
|
184
|
+
ev = "message" if a == "send" else (_event(sub.args[0]) if sub.args else None)
|
|
185
|
+
if not ev:
|
|
186
|
+
continue
|
|
187
|
+
room = _kw(sub, "to") or _kw(sub, "room")
|
|
188
|
+
protocol_send(b, "socketio", f"{ns}#{ev}", f.id, f.file, sub.lineno, EXACT if "{" not in ev else RESOLVED,
|
|
189
|
+
role="request" if a == "call" else "emit", library=LIB, process=role,
|
|
190
|
+
room=_str(room) if room is not None else None, ack=a == "call" or None,
|
|
191
|
+
node_attrs={"namespace": ns, "event": ev})
|
|
192
|
+
st["sends"] += 1
|
|
193
|
+
for ns, ev, f, line, conf, role in recvs:
|
|
194
|
+
g = (guards.get(ns, []) if role == "server" else None)
|
|
195
|
+
protocol_receive(b, "socketio", f"{ns}#{ev}", f.id, f.file, line, conf, guards=g, library=LIB, process=role,
|
|
196
|
+
node_attrs={"namespace": ns, "event": ev})
|
|
197
|
+
st["receivers"] += 1
|
|
198
|
+
if guards:
|
|
199
|
+
st["guarded_namespaces"] = sorted(guards)
|
|
200
|
+
return st
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _dn(e):
|
|
204
|
+
from .plugin import dotted
|
|
205
|
+
return dotted(e)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _ctx(prog, f):
|
|
209
|
+
from .plugin import Ctx
|
|
210
|
+
return Ctx(f.module, f, f.cls)
|