Skip to content

Commit 343160a

Browse files
committed
feat: initial poetry feature commit
1 parent 1a96c22 commit 343160a

File tree

6 files changed

+945
-0
lines changed

6 files changed

+945
-0
lines changed

src/common/localize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ export namespace PyenvStrings {
158158
export const pyenvRefreshing = l10n.t('Refreshing Pyenv Python versions');
159159
}
160160

161+
export namespace PoetryStrings {
162+
export const poetryManager = l10n.t('Manages Poetry environments');
163+
export const poetryDiscovering = l10n.t('Discovering Poetry environments');
164+
export const poetryRefreshing = l10n.t('Refreshing Poetry environments');
165+
}
166+
161167
export namespace ProjectCreatorString {
162168
export const addExistingProjects = l10n.t('Add Existing Projects');
163169
export const autoFindProjects = l10n.t('Auto Find Projects');

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './in
6666
import { registerSystemPythonFeatures } from './managers/builtin/main';
6767
import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder';
6868
import { registerCondaFeatures } from './managers/conda/main';
69+
import { registerPoetryFeatures } from './managers/poetry/main';
6970
import { registerPyenvFeatures } from './managers/pyenv/main';
7071

7172
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
@@ -296,6 +297,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
296297
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel),
297298
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
298299
registerPyenvFeatures(nativeFinder, context.subscriptions),
300+
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel),
299301
shellStartupVarsMgr.initialize(),
300302
]);
301303

src/managers/poetry/main.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Disposable, LogOutputChannel } from 'vscode';
2+
import { PythonEnvironmentApi } from '../../api';
3+
import { traceInfo } from '../../common/logging';
4+
import { getPythonApi } from '../../features/pythonApi';
5+
import { NativePythonFinder } from '../common/nativePythonFinder';
6+
import { PoetryManager } from './poetryManager';
7+
import { PoetryPackageManager } from './poetryPackageManager';
8+
import { getPoetry } from './poetryUtils';
9+
10+
export async function registerPoetryFeatures(
11+
nativeFinder: NativePythonFinder,
12+
disposables: Disposable[],
13+
outputChannel: LogOutputChannel,
14+
): Promise<void> {
15+
const api: PythonEnvironmentApi = await getPythonApi();
16+
17+
try {
18+
await getPoetry(nativeFinder);
19+
20+
const envManager = new PoetryManager(nativeFinder, api);
21+
const pkgManager = new PoetryPackageManager(api, outputChannel, envManager);
22+
23+
disposables.push(
24+
envManager,
25+
pkgManager,
26+
api.registerEnvironmentManager(envManager),
27+
api.registerPackageManager(pkgManager)
28+
);
29+
} catch (ex) {
30+
traceInfo('Poetry not found, turning off poetry features.', ex);
31+
}
32+
}
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import * as path from 'path';
2+
import { Disposable, EventEmitter, MarkdownString, ProgressLocation, ThemeIcon, Uri } from 'vscode';
3+
import {
4+
DidChangeEnvironmentEventArgs,
5+
DidChangeEnvironmentsEventArgs,
6+
EnvironmentChangeKind,
7+
EnvironmentManager,
8+
GetEnvironmentScope,
9+
GetEnvironmentsScope,
10+
IconPath,
11+
PythonEnvironment,
12+
PythonEnvironmentApi,
13+
PythonProject,
14+
RefreshEnvironmentsScope,
15+
ResolveEnvironmentContext,
16+
SetEnvironmentScope,
17+
} from '../../api';
18+
import { PoetryStrings } from '../../common/localize';
19+
import { traceError, traceInfo } from '../../common/logging';
20+
import { createDeferred, Deferred } from '../../common/utils/deferred';
21+
import { withProgress } from '../../common/window.apis';
22+
import { NativePythonFinder } from '../common/nativePythonFinder';
23+
import { getLatest } from '../common/utils';
24+
import {
25+
clearPoetryCache,
26+
getPoetryForGlobal,
27+
getPoetryForWorkspace,
28+
POETRY_ENVIRONMENTS,
29+
refreshPoetry,
30+
resolvePoetryPath,
31+
setPoetryForGlobal,
32+
setPoetryForWorkspace,
33+
setPoetryForWorkspaces,
34+
} from './poetryUtils';
35+
36+
export class PoetryManager implements EnvironmentManager, Disposable {
37+
private collection: PythonEnvironment[] = [];
38+
private fsPathToEnv: Map<string, PythonEnvironment> = new Map();
39+
private globalEnv: PythonEnvironment | undefined;
40+
41+
private readonly _onDidChangeEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
42+
public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event;
43+
44+
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
45+
public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event;
46+
47+
constructor(private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi) {
48+
this.name = 'poetry';
49+
this.displayName = 'Poetry';
50+
this.preferredPackageManagerId = 'ms-python.python:poetry';
51+
this.tooltip = new MarkdownString(PoetryStrings.poetryManager, true);
52+
this.iconPath = new ThemeIcon('python');
53+
}
54+
55+
name: string;
56+
displayName: string;
57+
preferredPackageManagerId: string;
58+
description?: string;
59+
tooltip: string | MarkdownString;
60+
iconPath?: IconPath;
61+
62+
public dispose() {
63+
this.collection = [];
64+
this.fsPathToEnv.clear();
65+
}
66+
67+
private _initialized: Deferred<void> | undefined;
68+
async initialize(): Promise<void> {
69+
if (this._initialized) {
70+
return this._initialized.promise;
71+
}
72+
73+
this._initialized = createDeferred();
74+
75+
await withProgress(
76+
{
77+
location: ProgressLocation.Window,
78+
title: PoetryStrings.poetryDiscovering,
79+
},
80+
async () => {
81+
this.collection = await refreshPoetry(false, this.nativeFinder, this.api, this);
82+
await this.loadEnvMap();
83+
84+
this._onDidChangeEnvironments.fire(
85+
this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })),
86+
);
87+
},
88+
);
89+
this._initialized.resolve();
90+
}
91+
92+
async getEnvironments(scope: GetEnvironmentsScope): Promise<PythonEnvironment[]> {
93+
await this.initialize();
94+
95+
if (scope === 'all') {
96+
return Array.from(this.collection);
97+
}
98+
99+
if (scope === 'global') {
100+
return this.collection.filter((env) => {
101+
return env.group === POETRY_ENVIRONMENTS;
102+
});
103+
}
104+
105+
if (scope instanceof Uri) {
106+
const env = this.fromEnvMap(scope);
107+
if (env) {
108+
return [env];
109+
}
110+
}
111+
112+
return [];
113+
}
114+
115+
async refresh(context: RefreshEnvironmentsScope): Promise<void> {
116+
if (context === undefined) {
117+
await withProgress(
118+
{
119+
location: ProgressLocation.Window,
120+
title: PoetryStrings.poetryRefreshing,
121+
},
122+
async () => {
123+
traceInfo('Refreshing Poetry Environments');
124+
const discard = this.collection.map((c) => c);
125+
this.collection = await refreshPoetry(true, this.nativeFinder, this.api, this);
126+
127+
await this.loadEnvMap();
128+
129+
const args = [
130+
...discard.map((env) => ({ kind: EnvironmentChangeKind.remove, environment: env })),
131+
...this.collection.map((env) => ({ kind: EnvironmentChangeKind.add, environment: env })),
132+
];
133+
134+
this._onDidChangeEnvironments.fire(args);
135+
},
136+
);
137+
}
138+
}
139+
140+
async get(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
141+
await this.initialize();
142+
if (scope instanceof Uri) {
143+
let env = this.fsPathToEnv.get(scope.fsPath);
144+
if (env) {
145+
return env;
146+
}
147+
const project = this.api.getPythonProject(scope);
148+
if (project) {
149+
env = this.fsPathToEnv.get(project.uri.fsPath);
150+
if (env) {
151+
return env;
152+
}
153+
}
154+
}
155+
156+
return this.globalEnv;
157+
}
158+
159+
async set(scope: SetEnvironmentScope, environment?: PythonEnvironment | undefined): Promise<void> {
160+
if (scope === undefined) {
161+
await setPoetryForGlobal(environment?.environmentPath?.fsPath);
162+
} else if (scope instanceof Uri) {
163+
const folder = this.api.getPythonProject(scope);
164+
const fsPath = folder?.uri?.fsPath ?? scope.fsPath;
165+
if (fsPath) {
166+
if (environment) {
167+
this.fsPathToEnv.set(fsPath, environment);
168+
} else {
169+
this.fsPathToEnv.delete(fsPath);
170+
}
171+
await setPoetryForWorkspace(fsPath, environment?.environmentPath?.fsPath);
172+
}
173+
} else if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) {
174+
const projects: PythonProject[] = [];
175+
scope
176+
.map((s) => this.api.getPythonProject(s))
177+
.forEach((p) => {
178+
if (p) {
179+
projects.push(p);
180+
}
181+
});
182+
183+
const before: Map<string, PythonEnvironment | undefined> = new Map();
184+
projects.forEach((p) => {
185+
before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath));
186+
if (environment) {
187+
this.fsPathToEnv.set(p.uri.fsPath, environment);
188+
} else {
189+
this.fsPathToEnv.delete(p.uri.fsPath);
190+
}
191+
});
192+
193+
await setPoetryForWorkspaces(
194+
projects.map((p) => p.uri.fsPath),
195+
environment?.environmentPath?.fsPath,
196+
);
197+
198+
projects.forEach((p) => {
199+
const b = before.get(p.uri.fsPath);
200+
if (b?.envId.id !== environment?.envId.id) {
201+
this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment });
202+
}
203+
});
204+
}
205+
}
206+
207+
async resolve(context: ResolveEnvironmentContext): Promise<PythonEnvironment | undefined> {
208+
await this.initialize();
209+
210+
if (context instanceof Uri) {
211+
const env = await resolvePoetryPath(context.fsPath, this.nativeFinder, this.api, this);
212+
if (env) {
213+
const _collectionEnv = this.findEnvironmentByPath(env.environmentPath.fsPath);
214+
if (_collectionEnv) {
215+
return _collectionEnv;
216+
}
217+
218+
this.collection.push(env);
219+
this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: env }]);
220+
221+
return env;
222+
}
223+
224+
return undefined;
225+
}
226+
}
227+
228+
async clearCache(): Promise<void> {
229+
await clearPoetryCache();
230+
}
231+
232+
private async loadEnvMap() {
233+
this.globalEnv = undefined;
234+
this.fsPathToEnv.clear();
235+
236+
// Try to find a global environment
237+
const fsPath = await getPoetryForGlobal();
238+
239+
if (fsPath) {
240+
this.globalEnv = this.findEnvironmentByPath(fsPath);
241+
242+
// If the environment is not found, resolve the fsPath
243+
if (!this.globalEnv) {
244+
this.globalEnv = await resolvePoetryPath(fsPath, this.nativeFinder, this.api, this);
245+
246+
// If the environment is resolved, add it to the collection
247+
if (this.globalEnv) {
248+
this.collection.push(this.globalEnv);
249+
}
250+
}
251+
}
252+
253+
if (!this.globalEnv) {
254+
this.globalEnv = getLatest(this.collection.filter((e) => e.group === POETRY_ENVIRONMENTS));
255+
}
256+
257+
// Find any poetry environments that might be associated with the current projects
258+
// Poetry typically has a pyproject.toml file in the project root
259+
const pathSorted = this.collection
260+
.filter((e) => this.api.getPythonProject(e.environmentPath))
261+
.sort((a, b) => {
262+
if (a.environmentPath.fsPath !== b.environmentPath.fsPath) {
263+
return a.environmentPath.fsPath.length - b.environmentPath.fsPath.length;
264+
}
265+
return a.environmentPath.fsPath.localeCompare(b.environmentPath.fsPath);
266+
});
267+
268+
// Try to find workspace environments
269+
const paths = this.api.getPythonProjects().map((p) => p.uri.fsPath);
270+
for (const p of paths) {
271+
const env = await getPoetryForWorkspace(p);
272+
273+
if (env) {
274+
const found = this.findEnvironmentByPath(env);
275+
276+
if (found) {
277+
this.fsPathToEnv.set(p, found);
278+
} else {
279+
// If not found, resolve the poetry path
280+
const resolved = await resolvePoetryPath(env, this.nativeFinder, this.api, this);
281+
282+
if (resolved) {
283+
// If resolved add it to the collection
284+
this.fsPathToEnv.set(p, resolved);
285+
this.collection.push(resolved);
286+
} else {
287+
traceError(`Failed to resolve poetry environment: ${env}`);
288+
}
289+
}
290+
} else {
291+
// If there is not an environment already assigned by user to this project
292+
// then see if there is one in the collection
293+
if (pathSorted.length === 1) {
294+
this.fsPathToEnv.set(p, pathSorted[0]);
295+
} else {
296+
// If there is more than one environment then we need to check if the project
297+
// is a subfolder of one of the environments
298+
const found = pathSorted.find((e) => {
299+
const t = this.api.getPythonProject(e.environmentPath)?.uri.fsPath;
300+
return t && path.normalize(t) === p;
301+
});
302+
if (found) {
303+
this.fsPathToEnv.set(p, found);
304+
}
305+
}
306+
}
307+
}
308+
}
309+
310+
private fromEnvMap(uri: Uri): PythonEnvironment | undefined {
311+
// Find environment directly using the URI mapping
312+
const env = this.fsPathToEnv.get(uri.fsPath);
313+
if (env) {
314+
return env;
315+
}
316+
317+
// Find environment using the Python project for the Uri
318+
const project = this.api.getPythonProject(uri);
319+
if (project) {
320+
return this.fsPathToEnv.get(project.uri.fsPath);
321+
}
322+
323+
return undefined;
324+
}
325+
326+
private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined {
327+
const normalized = path.normalize(fsPath);
328+
return this.collection.find((e) => {
329+
const n = path.normalize(e.environmentPath.fsPath);
330+
return n === normalized || path.dirname(n) === normalized || path.dirname(path.dirname(n)) === normalized;
331+
});
332+
}
333+
}

0 commit comments

Comments
 (0)