Skip to content

Commit 6efcfb0

Browse files
karthiknadigCopilot
andcommitted
fix: keep non-uv Pythons as the default global interpreter
Listing uv-managed installations under Global made them candidates for the default interpreter, so a newer uv build replaced an existing python.org or Store Python on machines that have both. uv marks its installations EXTERNALLY-MANAGED (PEP 668), so installing packages into the new default failed. Prefer installations not managed by uv when choosing the default and fall back to uv only when nothing else is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2ae5508 commit 6efcfb0

4 files changed

Lines changed: 82 additions & 9 deletions

File tree

‎src/managers/builtin/sysPythonManager.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { normalizePath } from '../../common/utils/pathUtils';
2323
import { withProgress } from '../../common/window.apis';
2424
import { getProjectFsPathForScope, tryFastPathGet } from '../common/fastPath';
2525
import { NativePythonFinder } from '../common/nativePythonFinder';
26-
import { getLatest } from '../common/utils';
2726
import {
2827
clearSystemEnvCache,
2928
getSystemEnvForGlobal,
@@ -32,7 +31,7 @@ import {
3231
setSystemEnvForWorkspace,
3332
setSystemEnvForWorkspaces,
3433
} from './cache';
35-
import { refreshPythons, resolveSystemPythonEnvironmentPath } from './utils';
34+
import { getDefaultGlobalPython, refreshPythons, resolveSystemPythonEnvironmentPath } from './utils';
3635
import { installPythonWithUv, promptInstallPythonViaUv, selectPythonVersionToInstall } from './uvPythonInstaller';
3736

3837
export class SysPythonManager implements EnvironmentManager {
@@ -176,7 +175,7 @@ export class SysPythonManager implements EnvironmentManager {
176175

177176
async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise<void> {
178177
if (scope === undefined) {
179-
this.globalEnv = environment ?? getLatest(this.collection);
178+
this.globalEnv = environment ?? getDefaultGlobalPython(this.collection);
180179
if (environment) {
181180
await setSystemEnvForGlobal(environment.environmentPath.fsPath);
182181
}
@@ -372,7 +371,7 @@ export class SysPythonManager implements EnvironmentManager {
372371

373372
// If a global environment is still not set, try using the latest environment
374373
if (!this.globalEnv) {
375-
this.globalEnv = getLatest(this.collection);
374+
this.globalEnv = getDefaultGlobalPython(this.collection);
376375
}
377376

378377
// Try to find workspace environments

‎src/managers/builtin/utils.ts‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import {
1414
NativePythonEnvironmentKind,
1515
NativePythonFinder,
1616
} from '../common/nativePythonFinder';
17-
import { shortenVersionString, sortEnvironments } from '../common/utils';
17+
import { getLatest, shortenVersionString, sortEnvironments } from '../common/utils';
1818

1919
const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code';
2020
const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask';
2121
let pixiRecommendationShown = false;
2222

23+
// Global environments created for uv-managed Python installations.
24+
const uvManagedPythons = new WeakSet<PythonEnvironment>();
25+
2326
function asPackageQuickPickItem(name: string, version?: string): QuickPickItem {
2427
return {
2528
label: name,
@@ -146,6 +149,19 @@ export async function isUvManagedPythonInstall(env: NativeEnvInfo): Promise<bool
146149
return !(await fs.pathExists(path.join(env.prefix, 'pyvenv.cfg')));
147150
}
148151

152+
/**
153+
* Returns the global Python to use by default: the newest one, preferring installations that are not
154+
* managed by uv. uv marks its installations EXTERNALLY-MANAGED (PEP 668), so installing packages into
155+
* them fails; they are used by default only when no other Python is available.
156+
*
157+
* @param globals Global Python environments.
158+
* @returns The default global Python, or `undefined` when there is none.
159+
*/
160+
export function getDefaultGlobalPython(globals: PythonEnvironment[]): PythonEnvironment | undefined {
161+
const preferred = globals.filter((e) => !e.error && !uvManagedPythons.has(e));
162+
return getLatest(preferred.length > 0 ? preferred : globals);
163+
}
164+
149165
export async function refreshPythons(
150166
hardRefresh: boolean,
151167
nativeFinder: NativePythonFinder,
@@ -188,6 +204,9 @@ export async function refreshPythons(
188204
try {
189205
const envInfo = getPythonInfo(env, uvManaged);
190206
const python = api.createPythonEnvironmentItem(envInfo, manager);
207+
if (uvManaged) {
208+
uvManagedPythons.add(python);
209+
}
191210
collection.push(python);
192211
} catch (e) {
193212
log.error((e as Error).message);
@@ -246,8 +265,12 @@ export async function resolveSystemPythonEnvironmentPath(
246265

247266
// This is supposed to handle a python interpreter as long as we know some basic things about it
248267
if (resolved.executable && resolved.version && resolved.prefix) {
249-
const envInfo = getPythonInfo(resolved, await isUvManagedPythonInstall(resolved));
250-
return api.createPythonEnvironmentItem(envInfo, manager);
268+
const uvManaged = await isUvManagedPythonInstall(resolved);
269+
const python = api.createPythonEnvironmentItem(getPythonInfo(resolved, uvManaged), manager);
270+
if (uvManaged) {
271+
uvManagedPythons.add(python);
272+
}
273+
return python;
251274
}
252275
} catch (ex) {
253276
traceVerbose(`Failed to resolve env "${fsPath}": ${ex}`);

‎src/managers/builtin/venvManager.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { findParentIfFile } from '../../features/envCommands';
3232
import { getProjectFsPathForScope, tryFastPathGet } from '../common/fastPath';
3333
import { NativePythonFinder } from '../common/nativePythonFinder';
3434
import { getLatest, shortenVersionString, sortEnvironments } from '../common/utils';
35+
import { getDefaultGlobalPython } from './utils';
3536
import { promptInstallPythonViaUv } from './uvPythonInstaller';
3637
import {
3738
clearVenvCache,
@@ -568,7 +569,7 @@ export class VenvManager implements EnvironmentManager {
568569

569570
// If a global environment is still not set, use latest from globals
570571
if (!this.globalEnv) {
571-
this.globalEnv = getLatest(globals);
572+
this.globalEnv = getDefaultGlobalPython(globals);
572573
}
573574
}
574575

‎src/test/managers/builtin/uvManagedPythonInstall.unit.test.ts‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import * as path from 'path';
88
import * as sinon from 'sinon';
99
import { LogOutputChannel, Uri } from 'vscode';
1010
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../../api';
11-
import { isUvManagedPythonInstall, refreshPythons, resolveSystemPythonEnvironmentPath } from '../../../managers/builtin/utils';
11+
import {
12+
getDefaultGlobalPython,
13+
isUvManagedPythonInstall,
14+
refreshPythons,
15+
resolveSystemPythonEnvironmentPath,
16+
} from '../../../managers/builtin/utils';
1217
import * as uvEnvironments from '../../../managers/builtin/uvEnvironments';
1318
import { findVirtualEnvironments, resolveVenvPythonEnvironmentPath } from '../../../managers/builtin/venvUtils';
1419
import {
@@ -151,4 +156,49 @@ suite('uv-managed Python installations', () => {
151156
assert.strictEqual(install?.shortDisplayName, '3.13.15 (uv)');
152157
assert.strictEqual(venv?.shortDisplayName, '3.13.15');
153158
});
159+
160+
suite('default global Python', () => {
161+
let pythonOrg: NativeEnvInfo;
162+
163+
setup(async () => {
164+
// An older installation that is not managed by uv (for example from python.org).
165+
const prefix = path.join(tempRoot, 'Python312');
166+
await fs.outputFile(executableIn(prefix), '');
167+
pythonOrg = {
168+
kind: NativePythonEnvironmentKind.globalPaths,
169+
executable: executableIn(prefix),
170+
prefix,
171+
version: '3.12.10',
172+
};
173+
});
174+
175+
test('prefers an installation not managed by uv over a newer uv-managed one', async () => {
176+
const envs = await refreshPythons(false, finderReturning([uvInstall, pythonOrg]), api, log, systemManager);
177+
178+
assert.strictEqual(
179+
getDefaultGlobalPython(envs)?.environmentPath.fsPath,
180+
Uri.file(pythonOrg.executable!).fsPath,
181+
);
182+
});
183+
184+
test('prefers an installation not managed by uv over a uv-managed one resolved by path', async () => {
185+
const finder = finderReturning([uvInstall, pythonOrg]);
186+
const envs = await refreshPythons(false, finderReturning([pythonOrg]), api, log, systemManager);
187+
const resolved = await resolveSystemPythonEnvironmentPath(uvInstall.executable!, finder, api, systemManager);
188+
189+
assert.strictEqual(
190+
getDefaultGlobalPython([resolved!, ...envs])?.environmentPath.fsPath,
191+
Uri.file(pythonOrg.executable!).fsPath,
192+
);
193+
});
194+
195+
test('uses a uv-managed installation when it is the only Python', async () => {
196+
const envs = await refreshPythons(false, finderReturning([uvInstall]), api, log, systemManager);
197+
198+
assert.strictEqual(
199+
getDefaultGlobalPython(envs)?.environmentPath.fsPath,
200+
Uri.file(uvInstall.executable!).fsPath,
201+
);
202+
});
203+
});
154204
});

0 commit comments

Comments
 (0)