depsgraph 0.1.3__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.
depsgraph/__init__.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Project-local dependency graph for a single Python file.
|
|
2
|
+
|
|
3
|
+
Given a project directory and a target Python file, this module prints the
|
|
4
|
+
project-internal import dependency graph (files only) as an ASCII tree and
|
|
5
|
+
optionally renders it as a Mermaid mindmap.
|
|
6
|
+
|
|
7
|
+
It only reports dependencies that resolve to Python files within the given
|
|
8
|
+
project directory. Stdlib and third-party imports are ignored by default.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import ast
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_IGNORED_DIR_NAMES: frozenset[str] = frozenset(
|
|
19
|
+
{
|
|
20
|
+
".git",
|
|
21
|
+
".hg",
|
|
22
|
+
".mypy_cache",
|
|
23
|
+
".pytest_cache",
|
|
24
|
+
".ruff_cache",
|
|
25
|
+
".tox",
|
|
26
|
+
".venv",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
"build",
|
|
29
|
+
"dist",
|
|
30
|
+
"site-packages",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_args(argv:list[str]|None=None) -> argparse.Namespace:
|
|
36
|
+
parser = argparse.ArgumentParser(description=("Show project-internal Python import dependencies for a file, as an ASCII graph and optionally a Mermaid mindmap."),)
|
|
37
|
+
parser.add_argument("target", help="Target Python file path (absolute or relative to project dir).")
|
|
38
|
+
parser.add_argument("--project-dir", default=".", help="Project directory to scan (default: current directory).")
|
|
39
|
+
parser.add_argument("--max-depth", type=int, default=50, help="Maximum recursion depth (default: 50).")
|
|
40
|
+
parser.add_argument("--show-missing", action="store_true", help="Also list imports that could not be resolved to project files.",)
|
|
41
|
+
parser.add_argument("--no-mermaid", action="store_true", help="Do not generate Mermaid mindmap files.")
|
|
42
|
+
parser.add_argument("--mermaid-mmd", default=None, help="Output path for Mermaid .mmd (default: <project-dir>/deps_graph.mmd).")
|
|
43
|
+
parser.add_argument("--mermaid-html", default=None, help="Output path for Mermaid HTML (default: <project-dir>/deps_graph.html).")
|
|
44
|
+
parser.add_argument("--ignored-dir", action="append", default=[], help=("Directory name to ignore (can be repeated). " "Defaults include .venv, .git, __pycache__, dist, build."),)
|
|
45
|
+
return parser.parse_args(argv)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_ignored_dir_name(dir_name: str, extra_ignored: frozenset[str]) -> bool:
|
|
49
|
+
return dir_name in DEFAULT_IGNORED_DIR_NAMES or dir_name in extra_ignored
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _iter_python_files(project_dir: Path, extra_ignored: frozenset[str]) -> list[Path]:
|
|
53
|
+
files: list[Path] = []
|
|
54
|
+
for path in project_dir.rglob("*.py"):
|
|
55
|
+
if not path.is_file():
|
|
56
|
+
continue
|
|
57
|
+
rel = path.relative_to(project_dir)
|
|
58
|
+
if any(_is_ignored_dir_name(part, extra_ignored) for part in rel.parts[:-1]):
|
|
59
|
+
continue
|
|
60
|
+
files.append(path)
|
|
61
|
+
files.sort()
|
|
62
|
+
return files
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _has_init_file(directory: Path) -> bool:
|
|
66
|
+
return (directory / "__init__.py").is_file()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _compute_module_name(project_dir: Path, file_path: Path) -> str | None:
|
|
70
|
+
"""Best-effort module name for a file based on __init__.py packages.
|
|
71
|
+
|
|
72
|
+
Returns None if the file isn't under any importable package structure.
|
|
73
|
+
For top-level modules (project_dir/foo.py) it returns "foo".
|
|
74
|
+
"""
|
|
75
|
+
rel = file_path.relative_to(project_dir)
|
|
76
|
+
if rel.suffix != ".py":
|
|
77
|
+
return None
|
|
78
|
+
if len(rel.parts) == 1:
|
|
79
|
+
return rel.stem
|
|
80
|
+
parts = list(rel.parts)
|
|
81
|
+
filename = parts.pop()
|
|
82
|
+
if filename == "__init__.py":
|
|
83
|
+
module_parts = parts
|
|
84
|
+
else:
|
|
85
|
+
module_parts = parts + [Path(filename).stem]
|
|
86
|
+
current = project_dir
|
|
87
|
+
package_parts: list[str] = []
|
|
88
|
+
for part in module_parts[:-1]:
|
|
89
|
+
current = current / part
|
|
90
|
+
if not _has_init_file(current):
|
|
91
|
+
return None
|
|
92
|
+
package_parts.append(part)
|
|
93
|
+
return ".".join(package_parts + [module_parts[-1]])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _compute_parent_package(project_dir: Path, file_path: Path) -> list[str]:
|
|
97
|
+
"""Return the dotted package parts for the file's parent package."""
|
|
98
|
+
rel = file_path.relative_to(project_dir)
|
|
99
|
+
if len(rel.parts) == 1:
|
|
100
|
+
return []
|
|
101
|
+
directory_parts = list(rel.parts[:-1])
|
|
102
|
+
current = project_dir
|
|
103
|
+
package_parts: list[str] = []
|
|
104
|
+
for part in directory_parts:
|
|
105
|
+
current = current / part
|
|
106
|
+
if not _has_init_file(current):
|
|
107
|
+
break
|
|
108
|
+
package_parts.append(part)
|
|
109
|
+
return package_parts
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build_module_index(project_dir: Path, python_files: list[Path]) -> dict[str, Path]:
|
|
113
|
+
index: dict[str, Path] = {}
|
|
114
|
+
for file_path in python_files:
|
|
115
|
+
module_name = _compute_module_name(project_dir, file_path)
|
|
116
|
+
if module_name is None:
|
|
117
|
+
continue
|
|
118
|
+
index.setdefault(module_name, file_path)
|
|
119
|
+
return index
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_imports(file_path: Path) -> tuple[list[ast.AST], str | None]:
|
|
123
|
+
try:
|
|
124
|
+
source = file_path.read_text(encoding="utf-8")
|
|
125
|
+
except (OSError, UnicodeError) as exc:
|
|
126
|
+
return [], f"read error: {exc}"
|
|
127
|
+
try:
|
|
128
|
+
tree = ast.parse(source, filename=str(file_path))
|
|
129
|
+
except SyntaxError as exc:
|
|
130
|
+
return [], f"syntax error: {exc.msg} (line {exc.lineno})"
|
|
131
|
+
nodes: list[ast.AST] = []
|
|
132
|
+
for node in ast.walk(tree):
|
|
133
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
134
|
+
nodes.append(node)
|
|
135
|
+
return nodes, None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _resolve_from_import(*, current_package: list[str], module: str | None, level: int, imported_names: list[str],) -> list[str]:
|
|
139
|
+
if level < 0:
|
|
140
|
+
level = 0
|
|
141
|
+
if level == 0:
|
|
142
|
+
base_parts = []
|
|
143
|
+
else:
|
|
144
|
+
up = level - 1
|
|
145
|
+
if up >= len(current_package):
|
|
146
|
+
base_parts = []
|
|
147
|
+
else:
|
|
148
|
+
base_parts = current_package[: len(current_package) - up]
|
|
149
|
+
module_parts = module.split(".") if module else []
|
|
150
|
+
base_module_parts = base_parts + module_parts
|
|
151
|
+
base_module = ".".join(base_module_parts)
|
|
152
|
+
candidates: list[str] = []
|
|
153
|
+
if base_module:
|
|
154
|
+
candidates.append(base_module)
|
|
155
|
+
for name in imported_names:
|
|
156
|
+
if not name or name == "*":
|
|
157
|
+
continue
|
|
158
|
+
if base_module:
|
|
159
|
+
candidates.append(f"{base_module}.{name}")
|
|
160
|
+
else:
|
|
161
|
+
candidates.append(name)
|
|
162
|
+
return candidates
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _resolve_import_candidates(*, project_dir: Path, file_path: Path, import_node: ast.AST,) -> list[str]:
|
|
166
|
+
current_package = _compute_parent_package(project_dir, file_path)
|
|
167
|
+
if isinstance(import_node, ast.Import):
|
|
168
|
+
names: list[str] = []
|
|
169
|
+
for alias in import_node.names:
|
|
170
|
+
if alias.name:
|
|
171
|
+
names.append(alias.name)
|
|
172
|
+
return names
|
|
173
|
+
if isinstance(import_node, ast.ImportFrom):
|
|
174
|
+
imported_names = [alias.name for alias in import_node.names if alias.name]
|
|
175
|
+
return _resolve_from_import(current_package=current_package, module=import_node.module, level=import_node.level, imported_names=imported_names,)
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _direct_project_deps_for_file(*, project_dir: Path, file_path: Path, module_index: dict[str, Path],) -> tuple[list[Path], list[str], str | None]:
|
|
180
|
+
nodes, error = _parse_imports(file_path)
|
|
181
|
+
if error is not None:
|
|
182
|
+
return [], [], error
|
|
183
|
+
deps: dict[Path, None] = {}
|
|
184
|
+
missing: dict[str, None] = {}
|
|
185
|
+
for node in nodes:
|
|
186
|
+
candidates = _resolve_import_candidates(project_dir=project_dir, file_path=file_path, import_node=node)
|
|
187
|
+
matched_any = False
|
|
188
|
+
for mod in candidates:
|
|
189
|
+
target = module_index.get(mod)
|
|
190
|
+
if target is None:
|
|
191
|
+
continue
|
|
192
|
+
deps[target] = None
|
|
193
|
+
matched_any = True
|
|
194
|
+
if not matched_any:
|
|
195
|
+
for mod in candidates:
|
|
196
|
+
missing[mod] = None
|
|
197
|
+
dep_files = sorted(deps.keys(), key=lambda p: str(p))
|
|
198
|
+
missing_imports = sorted(missing.keys())
|
|
199
|
+
return dep_files, missing_imports, None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _build_dependency_graph(*, project_dir: Path, root_file: Path, module_index: dict[str, Path], max_depth: int,) -> tuple[dict[Path, list[Path]], dict[Path, list[str]], dict[Path, str]]:
|
|
203
|
+
edges: dict[Path, list[Path]] = {}
|
|
204
|
+
missing_imports: dict[Path, list[str]] = {}
|
|
205
|
+
file_errors: dict[Path, str] = {}
|
|
206
|
+
to_visit: list[tuple[Path, int]] = [(root_file, 0)]
|
|
207
|
+
visited: set[Path] = set()
|
|
208
|
+
while to_visit:
|
|
209
|
+
file_path, depth = to_visit.pop()
|
|
210
|
+
if file_path in visited:
|
|
211
|
+
continue
|
|
212
|
+
visited.add(file_path)
|
|
213
|
+
if depth > max_depth:
|
|
214
|
+
continue
|
|
215
|
+
deps, missing, error = _direct_project_deps_for_file(project_dir=project_dir, file_path=file_path, module_index=module_index,)
|
|
216
|
+
if error is not None:
|
|
217
|
+
file_errors[file_path] = error
|
|
218
|
+
edges[file_path] = []
|
|
219
|
+
continue
|
|
220
|
+
edges[file_path] = deps
|
|
221
|
+
if missing:
|
|
222
|
+
missing_imports[file_path] = missing
|
|
223
|
+
next_depth = depth + 1
|
|
224
|
+
if next_depth <= max_depth:
|
|
225
|
+
for dep in deps:
|
|
226
|
+
if dep not in visited:
|
|
227
|
+
to_visit.append((dep, next_depth))
|
|
228
|
+
return edges, missing_imports, file_errors
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _format_rel(project_dir: Path, file_path: Path) -> str:
|
|
232
|
+
try:
|
|
233
|
+
return str(file_path.relative_to(project_dir))
|
|
234
|
+
except ValueError:
|
|
235
|
+
return str(file_path)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _render_ascii_tree(*, project_dir: Path, edges: dict[Path, list[Path]], root_file: Path, max_depth: int,) -> str:
|
|
239
|
+
lines: list[str] = []
|
|
240
|
+
def walk(node: Path, prefix: str, depth: int, stack: set[Path]) -> None:
|
|
241
|
+
if depth > max_depth:
|
|
242
|
+
return
|
|
243
|
+
deps = edges.get(node, [])
|
|
244
|
+
for i, dep in enumerate(deps):
|
|
245
|
+
is_last = i == (len(deps) - 1)
|
|
246
|
+
connector = "+-- "
|
|
247
|
+
line_prefix = prefix + connector
|
|
248
|
+
label = _format_rel(project_dir, dep)
|
|
249
|
+
if dep in stack:
|
|
250
|
+
lines.append(f"{line_prefix}{label} (cycle)")
|
|
251
|
+
continue
|
|
252
|
+
lines.append(f"{line_prefix}{label}")
|
|
253
|
+
child_prefix = prefix + ("| " if not is_last else " ")
|
|
254
|
+
stack.add(dep)
|
|
255
|
+
walk(dep, child_prefix, depth + 1, stack)
|
|
256
|
+
stack.remove(dep)
|
|
257
|
+
lines.append(_format_rel(project_dir, root_file))
|
|
258
|
+
walk(root_file, "", 0, {root_file})
|
|
259
|
+
return "\n".join(lines)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _render_mermaid_mindmap(*, project_dir: Path, edges: dict[Path, list[Path]], root_file: Path, max_depth: int,) -> str:
|
|
263
|
+
lines: list[str] = ["mindmap"]
|
|
264
|
+
root_label = _format_rel(project_dir, root_file)
|
|
265
|
+
lines.append(f" root(({root_label}))")
|
|
266
|
+
def walk(node: Path, indent: str, depth: int, stack: set[Path]) -> None:
|
|
267
|
+
if depth > max_depth:
|
|
268
|
+
return
|
|
269
|
+
deps = edges.get(node, [])
|
|
270
|
+
for dep in deps:
|
|
271
|
+
label = _format_rel(project_dir, dep)
|
|
272
|
+
if dep in stack:
|
|
273
|
+
lines.append(f"{indent}{label} (cycle)")
|
|
274
|
+
continue
|
|
275
|
+
lines.append(f"{indent}{label}")
|
|
276
|
+
stack.add(dep)
|
|
277
|
+
walk(dep, indent + " ", depth + 1, stack)
|
|
278
|
+
stack.remove(dep)
|
|
279
|
+
walk(root_file, " ", 0, {root_file})
|
|
280
|
+
return "\n".join(lines)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _write_text_file(path: Path, content: str) -> None:
|
|
284
|
+
path.write_text(content, encoding="utf-8")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _render_mermaid_html(mermaid_text: str) -> str:
|
|
288
|
+
return (
|
|
289
|
+
"<!doctype html>\n"
|
|
290
|
+
"<html lang=\"en\">\n"
|
|
291
|
+
" <head>\n"
|
|
292
|
+
" <meta charset=\"utf-8\">\n"
|
|
293
|
+
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
|
|
294
|
+
" <title>Dependency Mindmap</title>\n"
|
|
295
|
+
" <style>\n"
|
|
296
|
+
" body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; }\n"
|
|
297
|
+
" .wrap { padding: 24px; }\n"
|
|
298
|
+
" </style>\n"
|
|
299
|
+
" </head>\n"
|
|
300
|
+
" <body>\n"
|
|
301
|
+
" <div class=\"wrap\">\n"
|
|
302
|
+
" <div class=\"mermaid\">\n"
|
|
303
|
+
f"{mermaid_text}\n"
|
|
304
|
+
" </div>\n"
|
|
305
|
+
" </div>\n"
|
|
306
|
+
" <script type=\"module\">\n"
|
|
307
|
+
" import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';\n"
|
|
308
|
+
" mermaid.initialize({ startOnLoad: true });\n"
|
|
309
|
+
" </script>\n"
|
|
310
|
+
" </body>\n"
|
|
311
|
+
"</html>\n"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def main(argv: list[str] | None = None) -> int:
|
|
316
|
+
args = _parse_args(argv)
|
|
317
|
+
project_dir = Path(args.project_dir).resolve()
|
|
318
|
+
extra_ignored = frozenset(args.ignored_dir)
|
|
319
|
+
target = Path(args.target)
|
|
320
|
+
if not target.is_absolute():
|
|
321
|
+
target = (project_dir / target).resolve()
|
|
322
|
+
if not project_dir.is_dir():
|
|
323
|
+
raise SystemExit(f"project dir not found: {project_dir}")
|
|
324
|
+
if not target.is_file():
|
|
325
|
+
raise SystemExit(f"target file not found: {target}")
|
|
326
|
+
if target.suffix != ".py":
|
|
327
|
+
raise SystemExit(f"target must be a .py file: {target}")
|
|
328
|
+
python_files = _iter_python_files(project_dir, extra_ignored)
|
|
329
|
+
module_index = _build_module_index(project_dir, python_files)
|
|
330
|
+
edges, missing_imports, file_errors = _build_dependency_graph(project_dir=project_dir, root_file=target, module_index=module_index, max_depth=max(0, int(args.max_depth)))
|
|
331
|
+
print(_render_ascii_tree(project_dir=project_dir, edges=edges, root_file=target, max_depth=args.max_depth))
|
|
332
|
+
if not args.no_mermaid:
|
|
333
|
+
mermaid_text = _render_mermaid_mindmap(project_dir=project_dir, edges=edges, root_file=target, max_depth=args.max_depth)
|
|
334
|
+
mmd_path = Path(args.mermaid_mmd) if args.mermaid_mmd else project_dir / "deps_graph.mmd"
|
|
335
|
+
html_path = Path(args.mermaid_html) if args.mermaid_html else project_dir / "deps_graph.html"
|
|
336
|
+
_write_text_file(mmd_path, mermaid_text)
|
|
337
|
+
_write_text_file(html_path, _render_mermaid_html(mermaid_text))
|
|
338
|
+
print(f"\nMermaid mindmap saved: {mmd_path}")
|
|
339
|
+
print(f"Open in browser: {html_path}")
|
|
340
|
+
if file_errors:
|
|
341
|
+
print("\nErrors:")
|
|
342
|
+
for file_path in sorted(file_errors, key=lambda p: str(p)):
|
|
343
|
+
print(f"- {_format_rel(project_dir, file_path)}: {file_errors[file_path]}")
|
|
344
|
+
if args.show_missing and missing_imports:
|
|
345
|
+
print("\nUnresolved imports (not in project):")
|
|
346
|
+
for file_path in sorted(missing_imports, key=lambda p: str(p)):
|
|
347
|
+
imports = missing_imports[file_path]
|
|
348
|
+
if not imports:
|
|
349
|
+
continue
|
|
350
|
+
joined = ", ".join(imports)
|
|
351
|
+
print(f"- {_format_rel(project_dir, file_path)}: {joined}")
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: depsgraph
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Visualize internal Python import dependencies
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: build>=1.4.0
|
|
8
|
+
Requires-Dist: twine>=6.2.0
|
|
9
|
+
|
|
10
|
+
depsgraph is a minimal command-line tool that shows which Python modules a single file depends on *within your project*. It ignores stdlib and third-party imports by default, so you see only your own code’s coupling.
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
- Static analysis via AST (fast, no imports executed)
|
|
14
|
+
- ASCII tree printed to stdout
|
|
15
|
+
- Mermaid mindmap generated as `.mmd` and self-contained `.html`
|
|
16
|
+
- Configurable recursion depth and ignore patterns
|
|
17
|
+
- Detects import cycles and marks them
|
|
18
|
+
|
|
19
|
+
### Install
|
|
20
|
+
|
|
21
|
+
pip install depsgraph
|
|
22
|
+
|
|
23
|
+
### Usage
|
|
24
|
+
|
|
25
|
+
depsgraph path/to/file.py --project-dir /path/to/project
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
|
|
29
|
+
$ depsgraph src/handlers/api.py --project-dir .
|
|
30
|
+
src/handlers/api.py
|
|
31
|
+
+-- src/models/user.py
|
|
32
|
+
+-- src/utils/auth.py
|
|
33
|
+
| +-- src/config.py
|
|
34
|
+
+-- src/db/conn.py
|
|
35
|
+
+-- src/config.py
|
|
36
|
+
|
|
37
|
+
The command also creates `deps_graph.html` in the project directory; open it in a browser to view an interactive Mermaid mindmap.
|
|
38
|
+
|
|
39
|
+
### Options
|
|
40
|
+
- --project-dir: root of the project to scan (default ".")
|
|
41
|
+
- --max-depth: limit graph depth (default 50)
|
|
42
|
+
- --show-missing: list unresolved imports (stdlib, missing files)
|
|
43
|
+
- --no-mermaid: skip Mermaid file generation
|
|
44
|
+
- --ignored-dir: repeatable, e.g. `--ignored-dir generated`
|
|
45
|
+
|
|
46
|
+
### Requirements
|
|
47
|
+
Python 3.9+
|
|
48
|
+
|
|
49
|
+
### License
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
depsgraph/__init__.py,sha256=q7ljLiUy6J88wAHt6BU5JCR9SBRfIrI6w2cb9X3Sgw4,14112
|
|
2
|
+
depsgraph-0.1.3.dist-info/METADATA,sha256=fzUXhOVWHE5NYmBhw8E_5YtKoYC2NoGMHKyyh8ptMf4,1461
|
|
3
|
+
depsgraph-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
+
depsgraph-0.1.3.dist-info/top_level.txt,sha256=sfdLqiIoO2_HgHCf5PE6ZgXJqKm6c_s4I1QUmytMluw,10
|
|
5
|
+
depsgraph-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
depsgraph
|