Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/managers/builtin/inlineScript/envManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2363,16 +2363,19 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
this.baseManager,
);
const executable = resolved?.execInfo?.run.executable;
if (resolved && executable && pickCompatibleInterpreter([resolved], metadata.requiresPython)) {
if (
resolved &&
executable &&
pickCompatibleInterpreter([resolved], undefined) &&
(!requiresPython || this.matchesInstallConstraint(requiresPython, resolved.version))
) {
try {
const canonicalPath = await fs.realpath(executable);
if (!requiresPython || this.matchesInstallConstraint(requiresPython, resolved.version)) {
this.directlyResolvedBaseInterpreters.set(canonicalPath, resolved);
selected = {
environment: resolved,
canonicalPath,
};
}
this.directlyResolvedBaseInterpreters.set(canonicalPath, resolved);
selected = {
environment: resolved,
canonicalPath,
};
} catch (error) {
this.log.warn(
`Unable to resolve the Python installed for an inline script at ${executable}: ${getErrorMessage(error)}`,
Expand Down Expand Up @@ -2403,12 +2406,17 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
return { version: prereleaseLowerBound };
}
const lowerBoundRelease = lowerBound ? parseReleaseSegments(lowerBound) : undefined;
let needsCompleteCatalog = false;
if (lowerBound && lowerBoundRelease?.[0] === 3) {
if (/^>=\s*[^,]+$/.test(requiresPython) && this.matchesInstallConstraint(requiresPython, lowerBound)) {
return { version: lowerBound };
}
// PEP 440 `==3.13` is exact, while uv treats `3.13` as a broad minor selector.
if (/^==\s*[^,*]+$/.test(requiresPython) && this.matchesInstallConstraint(requiresPython, lowerBound)) {
return { version: lowerBound };
if (lowerBoundRelease.length >= 3) {
return { version: lowerBound };
}
needsCompleteCatalog = true;
}
}

Expand All @@ -2424,7 +2432,9 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
uvLookupResult === 'declined' ? 'compatible-python-declined' : 'install-failure',
};
}
available = await uvPythonInstaller.getAvailablePythonVersions();
available = needsCompleteCatalog
? await uvPythonInstaller.getAvailablePythonVersions({ allVersions: true })
: await uvPythonInstaller.getAvailablePythonVersions();
} catch (error) {
this.log.warn(`Unable to query Python versions available from uv: ${getErrorMessage(error)}`);
return { errorCategory: 'install-failure' };
Expand Down
16 changes: 14 additions & 2 deletions src/managers/builtin/uvPythonInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export interface UvPythonVersion {
arch: string;
}

export interface GetAvailablePythonVersionsOptions {
readonly allVersions?: boolean;
}

/**
* Checks if a command is available on the system.
*/
Expand Down Expand Up @@ -276,12 +280,20 @@ export async function getUvPythonPath(version?: string): Promise<string | undefi

/**
* Gets available Python versions from uv.
* @param options Set `allVersions` only when older patch releases are needed.
* @returns Promise that resolves to an array of Python versions
*/
export async function getAvailablePythonVersions(): Promise<UvPythonVersion[]> {
export async function getAvailablePythonVersions(
options?: GetAvailablePythonVersionsOptions,
): Promise<UvPythonVersion[]> {
return new Promise((resolve) => {
const chunks: string[] = [];
const proc = spawnProcess('uv', ['python', 'list', '--output-format', 'json']);
const args = ['python', 'list'];
if (options?.allVersions) {
args.push('--all-versions');
}
args.push('--output-format', 'json');
Comment on lines +291 to +295

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add --output-format json to the args list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean? it is added to the args list

const proc = spawnProcess('uv', args);
proc.stdout?.on('data', (data) => chunks.push(data.toString()));
proc.on('error', () => resolve([]));
proc.on('exit', (code) => {
Expand Down
59 changes: 59 additions & 0 deletions src/test/managers/builtin/inlineScript/envManager.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,25 @@ suite('InlineScriptEnvManager', () => {
assert.strictEqual(createWithProgressStub.callCount, 1);
});

test('uses strict PEP 440 matching for a directly resolved final release', async () => {
const uvExecutable = path.join(tempRoot, 'uv-python', isWindows() ? 'python.exe' : 'python');
await fs.outputFile(uvExecutable, '');
const uvBase = makeEnvironment('ms-python.python:system', '3.15.0', uvExecutable);
readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '!=3.15.0rc2' });
apiGetEnvironmentsStub.resolves([]);
getAvailablePythonVersionsStub.resolves([makeUvPythonVersion('3.15.0')]);
promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable });
resolveSystemPythonStub.resolves(uvBase);

assert.ok(await manager.create(scriptUri()));

sinon.assert.calledOnceWithExactly(promptInstallPythonViaUvStub, 'inlineScript', manager.log, {
requiresPython: '!=3.15.0rc2',
version: '3.15.0',
});
assert.strictEqual(createWithProgressStub.firstCall.args[4], uvBase);
});

test('selects an available uv release that satisfies exclusion clauses', async () => {
const uvExecutable = path.join(tempRoot, 'uv-python', isWindows() ? 'python.exe' : 'python');
await fs.outputFile(uvExecutable, '');
Expand Down Expand Up @@ -834,6 +853,46 @@ suite('InlineScriptEnvManager', () => {
requiresPython: '>=3.11,<3.12',
version: '3.11.14',
});
sinon.assert.calledOnceWithExactly(getAvailablePythonVersionsStub);
});

test('resolves a short exact requirement to an advertised concrete release', async () => {
const uvExecutable = path.join(tempRoot, 'uv-python', isWindows() ? 'python.exe' : 'python');
await fs.outputFile(uvExecutable, '');
const uvBase = makeEnvironment('ms-python.python:system', '3.13.0', uvExecutable);
readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '==3.13' });
apiGetEnvironmentsStub.onFirstCall().resolves([baseEnvironment]);
apiGetEnvironmentsStub.onSecondCall().resolves([baseEnvironment]);
apiGetEnvironmentsStub.onThirdCall().resolves([uvBase]);
const defaultCatalog = [makeUvPythonVersion('3.13.2')];
const completeCatalog = [...defaultCatalog, makeUvPythonVersion('3.13.0')];
getAvailablePythonVersionsStub.callsFake(async (options?: { allVersions?: boolean }) =>
options?.allVersions ? completeCatalog : defaultCatalog,
);
promptInstallPythonViaUvStub.resolves({ kind: 'installed', pythonPath: uvExecutable });

assert.ok(await manager.create(scriptUri()));

sinon.assert.calledOnceWithExactly(ensureUvForVersionLookupStub, '==3.13', manager.log);
sinon.assert.calledOnceWithExactly(getAvailablePythonVersionsStub, { allVersions: true });
sinon.assert.calledOnceWithExactly(promptInstallPythonViaUvStub, 'inlineScript', manager.log, {
requiresPython: '==3.13',
version: '3.13.0',
});
});

test('does not install a short exact requirement without an exact catalog candidate', async () => {
readMetadataStub.resolves({ ...VALID_METADATA, requiresPython: '==3.13' });
apiGetEnvironmentsStub.resolves([baseEnvironment]);
getAvailablePythonVersionsStub.resolves([makeUvPythonVersion('3.13.2')]);

assert.strictEqual(await manager.create(scriptUri()), undefined);

sinon.assert.calledOnceWithExactly(ensureUvForVersionLookupStub, '==3.13', manager.log);
sinon.assert.calledOnceWithExactly(getAvailablePythonVersionsStub, { allVersions: true });
assert.strictEqual(promptInstallPythonViaUvStub.callCount, 0);
assert.strictEqual(apiRefreshEnvironmentsStub.callCount, 0);
assert.strictEqual(createWithProgressStub.callCount, 0);
});

test('uses an exact requirement without needing an existing uv catalog', async () => {
Expand Down
26 changes: 26 additions & 0 deletions src/test/managers/builtin/uvPythonInstaller.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,32 @@ suite('uvPythonInstaller - getAvailablePythonVersions', () => {
assert.strictEqual(result.length, 2, 'Should return all versions');
assert.strictEqual(result[0].version, '3.13.1');
assert.strictEqual(result[1].version, '3.12.8');
sinon.assert.calledOnceWithExactly(spawnStub, 'uv', ['python', 'list', '--output-format', 'json']);
});

test('should request older patch releases only when all versions are requested', async () => {
const versions: UvPythonVersion[] = [
makeUvPythonVersion({ version: '3.13.2', path: null }),
makeUvPythonVersion({ version: '3.13.0', path: null }),
];
const args = ['python', 'list', '--all-versions', '--output-format', 'json'];
const mockProcess = new MockChildProcess('uv', args);
spawnStub.returns(mockProcess);

const resultPromise = getAvailablePythonVersions({ allVersions: true });

setTimeout(() => {
mockProcess.stdout?.emit('data', JSON.stringify(versions));
mockProcess.emit('exit', 0, null);
}, 10);

const result = await resultPromise;

assert.deepStrictEqual(
result.map((version) => version.version),
['3.13.2', '3.13.0'],
);
sinon.assert.calledOnceWithExactly(spawnStub, 'uv', args);
});

test('should return empty array on process error', async () => {
Expand Down
Loading