Skip to content

Commit d4dbfb6

Browse files
committed
Address PR comments
1 parent 1930418 commit d4dbfb6

10 files changed

Lines changed: 271 additions & 33 deletions

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,15 @@ The extension uses `pip` as the default package manager, but you can use the pac
8686

8787
Set `"python-envs.defaultPackageManager": "ms-python.python:uv"` to select uv for package operations.
8888
Environment-only API calls still use the environment manager's preferred package manager
89-
(for example, pip for venv); project-scoped UI and tool operations use the selected package manager.
89+
(for example, pip for venv). Project-scoped UI operations use the selected package manager;
90+
tools honor compatible selections, including explicit uv selection for pip-based environments.
9091
Automatic routing preserves the existing uv selection rules: when the configured or preferred
9192
manager is pip, `python-envs.alwaysUseUv` and whether the environment was created by uv determine
9293
whether the router selects pip or uv. Each manager executes only its own commands. Selecting the uv
9394
manager explicitly always uses uv, independently of that setting. The uv package manager resolves
94-
uv from PATH, a current-session installation, or the owning trusted workspace's `.pyprojectx/main/uv`
95+
uv from PATH, a current-session installation, or the requesting trusted workspace's `.pyprojectx/main/uv`
9596
(`uv.exe` on Windows). If uv is unavailable, it reports an error rather than falling back to pip.
97+
Scoped requests use the project to discover uv even when the target environment is outside the workspace.
9698

9799
#### Default Package Manager by Environment Manager
98100

‎src/extension.ts‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Uri,
1010
window,
1111
} from 'vscode';
12-
import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api';
12+
import { PackageManagementOptions, PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api';
1313
import { ENVS_EXTENSION_ID } from './common/constants';
1414
import { ensureCorrectVersion } from './common/extVersion';
1515
import { InlineScriptPackagesNotManagedError } from './common/inlineScript/errors';
@@ -275,12 +275,42 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
275275
),
276276
commands.registerCommand(
277277
'python-envs.test.getDirectPackageNames',
278-
async (environment: PythonEnvironment) => {
279-
const manager = envManagers.getPackageManager(environment);
278+
async (environment: PythonEnvironment, project?: Uri) => {
279+
const manager = envManagers.getPackageManager(project ?? environment);
280280
const names = await manager?.getDirectPackageNames?.(environment);
281281
return names ? Array.from(names) : undefined;
282282
},
283283
),
284+
commands.registerCommand(
285+
'python-envs.test.getPackagesForProject',
286+
async (environment: PythonEnvironment, project: Uri) => {
287+
const manager = envManagers.getPackageManager(project);
288+
if (!manager) {
289+
throw new Error(`Package manager not found for project: ${project.fsPath}`);
290+
}
291+
return manager.getPackages(environment, { skipCache: true });
292+
},
293+
),
294+
commands.registerCommand(
295+
'python-envs.test.managePackagesForProject',
296+
async (environment: PythonEnvironment, project: Uri, options: PackageManagementOptions) => {
297+
const manager = envManagers.getPackageManager(project);
298+
if (!manager) {
299+
throw new Error(`Package manager not found for project: ${project.fsPath}`);
300+
}
301+
await manager.manage(environment, options);
302+
},
303+
),
304+
commands.registerCommand(
305+
'python-envs.test.getPackageAvailableVersionsForProject',
306+
async (environment: PythonEnvironment, project: Uri, packageName: string) => {
307+
const manager = envManagers.getPackageManager(project);
308+
if (!manager) {
309+
throw new Error(`Package manager not found for project: ${project.fsPath}`);
310+
}
311+
return manager.getPackageAvailableVersions(environment, packageName, { errorMode: 'throw' });
312+
},
313+
),
284314
]
285315
: []),
286316
commands.registerCommand('python-envs.searchSettings', async () => {

‎src/internal/pythonTools.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import * as path from 'path';
77
import { CancellationError, CancellationToken, Disposable, l10n, Uri } from 'vscode';
88
import {
99
CONDA_MANAGER_ID,
10+
DEFAULT_PACKAGE_MANAGER_ID,
1011
INLINE_SCRIPT_MANAGER_ID,
1112
PYENV_MANAGER_ID,
13+
PYTHON_EXTENSION_ID,
1214
SYSTEM_MANAGER_ID,
1315
VENV_MANAGER_ID,
1416
} from '../common/constants';
@@ -617,7 +619,12 @@ export class PythonToolsApiImpl implements PythonToolsApi {
617619
const manager = await this.waitForRegistration(
618620
() => {
619621
const scoped = this.managers.getPackageManager(scope);
620-
return scoped?.id === preferredManagerId ? scoped : this.managers.getPackageManager(environment);
622+
const compatibleUv =
623+
preferredManagerId === DEFAULT_PACKAGE_MANAGER_ID &&
624+
scoped?.id === `${PYTHON_EXTENSION_ID}:uv`;
625+
return scoped?.id === preferredManagerId || compatibleUv
626+
? scoped
627+
: this.managers.getPackageManager(environment);
621628
},
622629
(listener) =>
623630
Disposable.from(

‎src/managers/builtin/builtInPackageManager.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,27 @@ export class BuiltInPackageManager implements PackageManager, Disposable {
5454
manage: (environment, options, token, scope) =>
5555
this.manage(environment, { ...options, runHeadless: true }, token, scope),
5656
getPackages: async (environment, token, scope) =>
57-
(await this.getManager(environment, token))[pythonToolSupport].getPackages(environment, token, scope),
57+
(await this.getManager(environment, token, scope))[pythonToolSupport].getPackages(environment, token, scope),
5858
};
5959

6060
private async getManager(
6161
environment: PythonEnvironment,
6262
token?: CancellationToken,
63+
scope?: Uri,
6364
): Promise<PipPackageManager | UvPackageManager> {
6465
throwIfCancelled(token);
6566
if (!environment.execInfo?.run?.executable) {
6667
return this.pip;
6768
}
6869
const envPath = environment.environmentPath.fsPath;
69-
return await shouldUseUv(this.pip.log, envPath, envPath, token) ? this.uv : this.pip;
70+
return await shouldUseUv(this.pip.log, envPath, scope?.fsPath ?? envPath, token) ? this.uv : this.pip;
7071
}
7172

7273
private routeCapability(capability: PackageManagerCapability): CapabilityCheck {
7374
return async (context) => {
74-
const manager = context.environment ? await this.getManager(context.environment) : this.pip;
75+
const manager = context.environment
76+
? await this.getManager(context.environment, undefined, context.scope instanceof Uri ? context.scope : undefined)
77+
: this.pip;
7578
return manager.capabilities[capability](context);
7679
};
7780
}
@@ -88,7 +91,7 @@ export class BuiltInPackageManager implements PackageManager, Disposable {
8891
return;
8992
}
9093
const support = await this.pip.capabilities[PackageCapability.Install]({ environment });
91-
const manager = support.supported ? await this.getManager(environment, toolToken) : this.pip;
94+
const manager = support.supported ? await this.getManager(environment, toolToken, scope) : this.pip;
9295
return manager.manage(environment, options, toolToken, scope);
9396
}
9497

‎src/managers/builtin/uvPackageManager.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export class UvPackageManager implements PackageManager, Disposable {
8282
readonly [pythonToolSupport]: PackageToolSupport = {
8383
manage: (environment, options, token, scope) =>
8484
this.manage(environment, { ...options, runHeadless: true }, token, scope),
85-
getPackages: async (environment, token) => {
86-
const packages = await this.fetchPackages(environment, false, token);
85+
getPackages: async (environment, token, scope) => {
86+
const packages = await this.fetchPackages(environment, false, token, scope);
8787
if (!packages) {
8888
throw new Error('Package listing returned no result.');
8989
}
@@ -100,7 +100,7 @@ export class UvPackageManager implements PackageManager, Disposable {
100100
throw new Error(support.reason);
101101
}
102102
const token = options.toolExecution ? options.cancellationToken : undefined;
103-
const uvExecutable = await getUvExecutable(this.log, environment.environmentPath.fsPath, token);
103+
const uvExecutable = await getUvExecutable(this.log, options.cwd ?? environment.environmentPath.fsPath, token);
104104
if (!uvExecutable) {
105105
throw new Error(l10n.t('uv is unavailable. Install uv or select a different package manager.'));
106106
}
@@ -131,7 +131,7 @@ export class UvPackageManager implements PackageManager, Disposable {
131131
const projects = this.venv.getProjectsByEnvironment(environment);
132132
const result = await getWorkspacePackagesToInstall(
133133
this.api, options, projects, environment, this.log,
134-
async () => new UvListCommand(await this.getCommandOptions(environment)).execute(),
134+
async () => new UvListCommand(await this.getCommandOptions(environment, { cwd: scope?.fsPath })).execute(),
135135
);
136136
if (!result) {
137137
return;
@@ -172,7 +172,7 @@ export class UvPackageManager implements PackageManager, Disposable {
172172
environment,
173173
this.packages.get(environment.envId.id),
174174
(changes) => this._onDidChangePackages.fire({ environment, manager: this, changes }),
175-
() => this.fetchPackages(environment, !options.runHeadless, toolToken),
175+
() => this.fetchPackages(environment, !options.runHeadless, toolToken, scope),
176176
!!toolToken,
177177
);
178178
} catch (error) {
@@ -235,11 +235,12 @@ export class UvPackageManager implements PackageManager, Disposable {
235235
environment: PythonEnvironment,
236236
showErrors = true,
237237
token?: CancellationToken,
238+
scope?: Uri,
238239
): Promise<Package[] | undefined> {
239240
try {
240241
throwIfCancelled(token);
241242
const command = new UvListCommand(await this.getCommandOptions(environment, {
242-
cancellationToken: token, toolExecution: !!token,
243+
cwd: scope?.fsPath, cancellationToken: token, toolExecution: !!token,
243244
}));
244245
const data = await command.execute(
245246
token ? { cancellationToken: token, strict: true, toolExecution: true } : undefined,

‎src/test/integration/packageManager.integration.test.ts‎

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import * as vscode from 'vscode';
22

3-
import { compare } from '@renovatebot/pep440';
3+
import { compare, Pep440Version } from '@renovatebot/pep440';
44
import assert from 'assert';
55
import {
66
Package,
7+
PackageManagementOptions,
78
PythonEnvironment,
89
PythonEnvironmentApi,
910
isPackageVersionLookupNotSupportedError,
1011
} from '../../api';
11-
import { CONDA_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants';
12+
import { CONDA_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID, VENV_MANAGER_ID } from '../../common/constants';
1213
import { ENVS_EXTENSION_ID } from '../constants';
1314
import { waitForCondition } from '../testUtils';
1415
import {
@@ -102,6 +103,21 @@ for (const profile of profiles) {
102103
let fixture: EnvironmentFixture | undefined;
103104
let previousAlwaysUseUv: boolean | undefined;
104105
let alwaysUseUvUpdated = false;
106+
let previousDefaultPackageManager: string | undefined;
107+
let defaultPackageManagerUpdated = false;
108+
109+
function getPackages(): Thenable<Package[] | undefined> {
110+
return vscode.commands.executeCommand<Package[]>(
111+
'python-envs.test.getPackagesForProject', environment!, fixture!.projectUri,
112+
);
113+
}
114+
115+
function managePackages(options: PackageManagementOptions): Thenable<void> {
116+
return vscode.commands.executeCommand<void>(
117+
'python-envs.test.managePackagesForProject', environment!, fixture!.projectUri, options,
118+
);
119+
}
120+
105121
suiteSetup(async function () {
106122
if (process.env.VSC_PYTHON_PACKAGE_NETWORK_TEST !== '1') {
107123
this.skip();
@@ -121,10 +137,19 @@ for (const profile of profiles) {
121137
assert.ok(workspaceFolder, 'Integration test workspace not found');
122138
const config = vscode.workspace.getConfiguration('python-envs', workspaceFolder.uri);
123139

124-
if (profile.packageManagerId === DEFAULT_PACKAGE_MANAGER_ID) {
140+
// Fixture projects live outside the workspace, so select their manager globally.
141+
previousDefaultPackageManager = config.inspect<string>('defaultPackageManager')?.globalValue;
142+
await config.update('defaultPackageManager', profile.packageManagerId, vscode.ConfigurationTarget.Global);
143+
defaultPackageManagerUpdated = true;
144+
145+
if (profile.provider.managerId === VENV_MANAGER_ID) {
125146
previousAlwaysUseUv = config.inspect<boolean>('alwaysUseUv')?.globalValue;
126147
await config.update('alwaysUseUv', false, vscode.ConfigurationTarget.Global);
127148
alwaysUseUvUpdated = true;
149+
assert.strictEqual(
150+
vscode.workspace.getConfiguration('python-envs', workspaceFolder.uri).get<boolean>('alwaysUseUv'),
151+
false,
152+
);
128153
}
129154

130155
fixture = await createEnvironmentFixture(api, workspaceFolder, {
@@ -133,6 +158,10 @@ for (const profile of profiles) {
133158
provider: profile.provider,
134159
});
135160
environment = fixture.environment;
161+
assert.strictEqual(
162+
vscode.workspace.getConfiguration('python-envs', fixture.projectUri).get<string>('defaultPackageManager'),
163+
profile.packageManagerId,
164+
);
136165
assert.strictEqual(
137166
environment.envId.managerId,
138167
profile.provider.managerId,
@@ -142,7 +171,7 @@ for (const profile of profiles) {
142171

143172
test(`${profile.name} Package Manager should install, list, and uninstall a package`, async () => {
144173
const packageName = profile.packageName;
145-
const baseline = await api.getPackages(environment!, { skipCache: true });
174+
const baseline = await getPackages();
146175
assert.ok(baseline, 'Unable to list packages before installation');
147176
assert.ok(
148177
baseline.every((pkg) => pkg.pkgId.managerId === profile.packageManagerId),
@@ -151,12 +180,12 @@ for (const profile of profiles) {
151180
const wasInstalled = baseline.some((pkg) => pkg.name.toLowerCase() === packageName);
152181

153182
if (!wasInstalled) {
154-
await api.managePackages(environment!, { install: [packageName], runHeadless: true });
183+
await managePackages({ install: [packageName], runHeadless: true });
155184
}
156185
let packages: Package[] | undefined;
157186
await waitForCondition(
158187
async () => {
159-
packages = await api.getPackages(environment!, { skipCache: true });
188+
packages = await getPackages();
160189
return packages?.some((pkg) => pkg.name.toLowerCase() === packageName) ?? false;
161190
},
162191
30_000,
@@ -169,16 +198,17 @@ for (const profile of profiles) {
169198
const directPackageNames = await vscode.commands.executeCommand<string[] | undefined>(
170199
'python-envs.test.getDirectPackageNames',
171200
environment!,
201+
fixture!.projectUri,
172202
);
173203
if (directPackageNames !== undefined) {
174204
assert.ok(directPackageNames.includes(packageName), 'Installed package was not reported as direct');
175205
}
176206

177207
if (!wasInstalled) {
178-
await api.managePackages(environment!, { uninstall: [packageName], runHeadless: true });
208+
await managePackages({ uninstall: [packageName], runHeadless: true });
179209
await waitForCondition(
180210
async () => {
181-
packages = await api.getPackages(environment!, { skipCache: true });
211+
packages = await getPackages();
182212
return packages !== undefined && !packages.some((pkg) => pkg.name.toLowerCase() === packageName);
183213
},
184214
30_000,
@@ -191,7 +221,7 @@ for (const profile of profiles) {
191221
});
192222

193223
test(`${profile.name} Package Manager should list available package versions`, async function () {
194-
const packages = await api.getPackages(environment!, { skipCache: true });
224+
const packages = await getPackages();
195225
assert.ok(packages, 'Unable to list packages before version lookup');
196226
const supportsVersionLookup = profile.supportsVersionLookup(packages);
197227
assert.notStrictEqual(
@@ -202,9 +232,10 @@ for (const profile of profiles) {
202232

203233
let versions;
204234
try {
205-
versions = await api.getPackageAvailableVersions(environment!, profile.packageName, {
206-
errorMode: 'throw',
207-
});
235+
versions = await vscode.commands.executeCommand<Pep440Version[]>(
236+
'python-envs.test.getPackageAvailableVersionsForProject',
237+
environment!, fixture!.projectUri, profile.packageName,
238+
);
208239
} catch (error) {
209240
if (isPackageVersionLookupNotSupportedError(error)) {
210241
assert.strictEqual(
@@ -233,11 +264,21 @@ for (const profile of profiles) {
233264
await fixture.dispose();
234265
}
235266
} finally {
236-
if (alwaysUseUvUpdated) {
267+
if (alwaysUseUvUpdated || defaultPackageManagerUpdated) {
237268
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
238269
assert.ok(workspaceFolder, 'Integration test workspace not found during teardown');
239270
const config = vscode.workspace.getConfiguration('python-envs', workspaceFolder.uri);
240-
await config.update('alwaysUseUv', previousAlwaysUseUv, vscode.ConfigurationTarget.Global);
271+
try {
272+
if (alwaysUseUvUpdated) {
273+
await config.update('alwaysUseUv', previousAlwaysUseUv, vscode.ConfigurationTarget.Global);
274+
}
275+
} finally {
276+
if (defaultPackageManagerUpdated) {
277+
await config.update(
278+
'defaultPackageManager', previousDefaultPackageManager, vscode.ConfigurationTarget.Global,
279+
);
280+
}
281+
}
241282
}
242283
}
243284
});

0 commit comments

Comments
 (0)