-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathpythonDependencyVisualization.ts
More file actions
149 lines (129 loc) · 5.9 KB
/
pythonDependencyVisualization.ts
File metadata and controls
149 lines (129 loc) · 5.9 KB
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import * as vscode from 'vscode';
import { Uri, workspace, FileType } from 'vscode';
import * as path from 'path';
import * as child_process_promise from 'child-process-promise';
import { promises as fsp } from 'fs';
import _ from 'lodash';
import { API, Visualization, VisualizationSettings, Connection } from "../api";
import { dependencies } from 'webpack';
export async function activate(context: vscode.ExtensionContext) {
const cbrvAPI = new API(context);
context.subscriptions.push(
vscode.commands.registerCommand('pythonDependencyVisualization.start', async () => {
await installPyDepsIfNeeded();
const visualization = await createPythonDependencyVisualization(cbrvAPI);
}),
);
}
export async function installPyDepsIfNeeded() {
const pythonPath = vscode.workspace.getConfiguration('python').get<string>('defaultInterpreterPath', 'python');
let installed = false;
try {
await child_process_promise.spawn(pythonPath, ['-m', 'pydeps', '--version']);
installed = true;
} catch {
installed = false;
}
if (!installed) {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Installing pydeps...",
cancellable: false
}, async (progress, token) => {
try {
await child_process_promise.spawn(pythonPath, ['-m', 'pip', 'install', 'pydeps'],
{ capture: ['stdout', 'stderr'] });
vscode.window.showInformationMessage(`Installed pydeps`);
} catch (e: any) {
const message = `Failed to install pydeps:\n ${e.stderr}`;
vscode.window.showErrorMessage(message);
throw new Error(message);
}
});
}
}
async function createPythonDependencyVisualization(cbrvAPI: API): Promise<Visualization> {
const settings: VisualizationSettings = {
title: "Python Dependency Visualization",
directed: true,
showOnHover: true,
connectionDefaults: {
tooltip: (conn, vis) => _(conn.connections)
.map(c => `"${vis.getRelativePath(c.from)}" -> "${vis.getRelativePath(c.to)}"`)
.sortBy()
.join("<br/>")
},
mergeRules: {
file: "ignore",
line: "ignore",
direction: "ignore",
width: "greatest",
color: "mostCommon",
},
filters: {
include: "**/*.py",
}
};
const visualization = await cbrvAPI.create(settings);
visualization.onFilesChange(async (visState) => {
visState.connections = await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Loading dependency graph...",
cancellable: false,
}, async (progress, token) => {
return await getDependencyGraph(visState.codebase, visState.files);
});
}, { immediate: true });
return visualization;
}
async function isSymlink(file: Uri) {
const stat = await workspace.fs.stat(file);
return (stat.type & FileType.SymbolicLink) === FileType.SymbolicLink;
}
export async function getDependencyGraph(codebase: Uri, files: Uri[]): Promise<Connection[]> {
const pythonPath = vscode.workspace.getConfiguration('python').get<string>('defaultInterpreterPath', 'python');
const codebasePath = await fsp.realpath(codebase.fsPath);
// normalize and resolve symlinks in path
const fsPaths = await Promise.all(files.map(f => fsp.realpath(f.fsPath)));
const fsPathsSet = new Set(fsPaths);
// NOTE: We can use `--max-bacon 0` to recurse the tree in one process call and make it a lot faster. See 74c02cd
// for an implementation of this. But pydeps treats module imports slightly differently depending on how the module
// was reached, causing inconsistent results based on file order. E.g if `b.c` is the root file, or if its reached
// via a relative import `from .b import c`, it will depend on the root package `__init__.py`. But if we reach it
// via `import b.c` it won't depend on the root `__init__.py`. Just naively calling pydeps on each file fixes this.
const promises = _(fsPaths)
.map<Promise<[string, string[]]>>(async (fsPath) => {
const ext = path.extname(fsPath).toLocaleLowerCase();
if (ext == ".py" && !(await isSymlink(Uri.file(fsPath)))) {
const result = await child_process_promise.spawn(pythonPath,
['-m', 'pydeps', '--show-deps', '--no-output', '--max-bacon', '2', fsPath],
{ capture: ['stdout', 'stderr'], cwd: codebasePath }
);
const graph = JSON.parse(result.stdout);
// Resolve all links in graph (I could probably skip this, pydeps seems to do it already)
const realPaths = await Promise.all(_(graph)
.map<Promise<[string, any]>>(async (info, moduleName: string) =>
[moduleName, typeof info.path == 'string' ? await fsp.realpath(info.path) : info.path]
).value()
);
for (const [moduleName, realPath] of realPaths) {
graph[moduleName].path = realPath;
}
const modules: string[] = _(graph).find(info => info.path == fsPath)?.imports ?? [];
const imports = modules
.map(m => graph[m].path)
.filter(p => typeof p == 'string' && fsPathsSet.has(p));
return [fsPath, imports];
} else {
return [fsPath, []];
}
})
.value();
return (await Promise.all(promises))
.flatMap(([source, dependencies]) =>
dependencies.map(dep => ({
from: Uri.file(source),
to: Uri.file(dep),
}))
);
}