Skip to content

Commit c75660a

Browse files
Address inline manager review feedback
Centralize the pyenv manager ID, simplify inline manager data flow and release comparisons, and share filesystem not-found classification across cache components. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c27
1 parent b36a3c8 commit c75660a

6 files changed

Lines changed: 90 additions & 58 deletions

File tree

src/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';
44
export const PYTHON_EXTENSION_ID = 'ms-python.python';
55
export const CONDA_MANAGER_ID = `${PYTHON_EXTENSION_ID}:conda`;
66
export const INLINE_SCRIPT_MANAGER_ID = `${PYTHON_EXTENSION_ID}:inline-script`;
7+
export const PYENV_MANAGER_ID = `${PYTHON_EXTENSION_ID}:pyenv`;
78
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
89
export const EXTENSION_ROOT_DIR = path.dirname(__dirname);
910
export const ISSUES_URL = 'https://github.com/microsoft/vscode-python-environments/issues';

src/common/inlineScriptCacheLayout.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Uri } from 'vscode';
88
import type { PythonEnvironment } from '../api';
99
import { INLINE_SCRIPT_MANAGER_ID } from './constants';
1010
import { traceWarn } from './logging';
11+
import { isFileNotFoundError } from './utils/filesystem';
1112
import { normalizePath } from './utils/pathUtils';
1213
import { isWindows } from './utils/platformUtils';
1314
import { getVenvPythonPath } from './utils/virtualEnvironment';
@@ -273,10 +274,6 @@ function parsePyvenvHome(raw: string): string | undefined {
273274
return undefined;
274275
}
275276

276-
function isFileNotFoundError(err: unknown): boolean {
277-
return typeof err === 'object' && err !== null && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT';
278-
}
279-
280277
function isDescendantPath(rootPath: string, candidatePath: string): boolean {
281278
const relative = path.relative(rootPath, candidatePath);
282279
return (

src/common/utils/filesystem.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
export function isFileNotFoundError(error: unknown): error is NodeJS.ErrnoException {
5+
return (
6+
typeof error === 'object' &&
7+
error !== null &&
8+
'code' in error &&
9+
(error as NodeJS.ErrnoException).code === 'ENOENT'
10+
);
11+
}

src/managers/builtin/inlineScriptEnvManager.ts

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
} from '../../api';
2222
import { computeCacheKey } from '../../common/inlineScriptCacheKey';
2323
import {
24-
InlineScriptEnvMeta,
2524
META_SCHEMA_VERSION,
2625
getBaseInterpreterStatus,
2726
getScriptEnvCacheRoot,
@@ -37,8 +36,9 @@ import {
3736
matchesPythonVersion,
3837
readInlineScriptMetadataFromFile,
3938
} from '../../common/inlineScriptMetadata';
40-
import { CONDA_MANAGER_ID, PYTHON_EXTENSION_ID, SYSTEM_MANAGER_ID } from '../../common/constants';
39+
import { CONDA_MANAGER_ID, PYENV_MANAGER_ID, SYSTEM_MANAGER_ID } from '../../common/constants';
4140
import { acquireFileLock, AcquiredFileLock } from '../../common/lockfile.apis';
41+
import { isFileNotFoundError } from '../../common/utils/filesystem';
4242
import { normalizePath } from '../../common/utils/pathUtils';
4343
import { compareReleaseSegments, parseReleaseSegments } from '../../common/utils/pep440Release';
4444
import { getVenvPythonPath } from '../../common/utils/virtualEnvironment';
@@ -48,12 +48,33 @@ import { createWithProgress, resolveVenvPythonEnvironmentPath } from './venvUtil
4848
const BASE_INTERPRETER_MANAGER_IDS = new Set([
4949
SYSTEM_MANAGER_ID,
5050
CONDA_MANAGER_ID,
51-
`${PYTHON_EXTENSION_ID}:pyenv`,
51+
PYENV_MANAGER_ID,
5252
]);
5353

5454
const CACHE_LOCK_TIMEOUT_MS = 5 * 60 * 1000;
5555
const CACHE_LOCK_RETRY_MS = 500;
5656

57+
interface SelectedBaseInterpreter {
58+
readonly environment: PythonEnvironment;
59+
readonly canonicalPath: string;
60+
}
61+
62+
interface CreateOrReuseEnvironmentOptions {
63+
readonly cacheKey: string;
64+
readonly packages: ReadonlyArray<string>;
65+
readonly metadata: InlineScriptMetadata;
66+
readonly selectedBase: SelectedBaseInterpreter;
67+
}
68+
69+
interface BuildCacheEntryResult {
70+
readonly environment?: PythonEnvironment;
71+
readonly retainLock?: boolean;
72+
}
73+
74+
type CacheEntryInspection =
75+
| { readonly kind: 'absent' | 'stale' | 'uncertain' }
76+
| { readonly kind: 'reusable'; readonly environment: PythonEnvironment };
77+
5778
/** Manages extension-owned PEP 723 script environments. */
5879
export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
5980
private readonly pendingCreations = new Map<string, Promise<PythonEnvironment | undefined>>();
@@ -100,9 +121,10 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
100121
return undefined;
101122
}
102123

103-
const packages = [...(metadata.dependencies ?? []), ...(options?.additionalPackages ?? [])].map((value) =>
104-
value.trim(),
105-
);
124+
const packages = [
125+
...(metadata.dependencies ?? []),
126+
...(options?.additionalPackages ?? []),
127+
].map((value) => value.trim());
106128
if (packages.some((value) => value.length === 0)) {
107129
this.log.warn(`Inline-script dependencies must not contain empty entries: ${scriptUri.fsPath}.`);
108130
return undefined;
@@ -113,6 +135,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
113135
this.log.warn(`No installed Python satisfies the inline-script requirements for ${scriptUri.fsPath}.`);
114136
return undefined;
115137
}
138+
116139
const cacheKey = computeCacheKey({
117140
dependencies: packages,
118141
interpreterPath: selectedBase.canonicalPath,
@@ -122,7 +145,12 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
122145
return await pending;
123146
}
124147

125-
const creation = this.createOrReuseEnvironment(cacheKey, packages, metadata, selectedBase);
148+
const creation = this.createOrReuseEnvironment({
149+
cacheKey,
150+
packages,
151+
metadata,
152+
selectedBase,
153+
});
126154
this.pendingCreations.set(cacheKey, creation);
127155
try {
128156
return await creation;
@@ -163,7 +191,8 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
163191
}
164192

165193
private async selectBaseInterpreter(metadata: InlineScriptMetadata): Promise<SelectedBaseInterpreter | undefined> {
166-
const reported = (await this.api.getEnvironments('global')).filter(
194+
const globalEnvironments = await this.api.getEnvironments('global');
195+
const reported = globalEnvironments.filter(
167196
(environment) =>
168197
BASE_INTERPRETER_MANAGER_IDS.has(environment.envId.managerId) &&
169198
(environment.envId.managerId !== CONDA_MANAGER_ID || environment.name === 'base'),
@@ -209,12 +238,12 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
209238
return undefined;
210239
}
211240

212-
private async createOrReuseEnvironment(
213-
cacheKey: string,
214-
packages: ReadonlyArray<string>,
215-
metadata: InlineScriptMetadata,
216-
selectedBase: SelectedBaseInterpreter,
217-
): Promise<PythonEnvironment | undefined> {
241+
private async createOrReuseEnvironment({
242+
cacheKey,
243+
packages,
244+
metadata,
245+
selectedBase,
246+
}: CreateOrReuseEnvironmentOptions): Promise<PythonEnvironment | undefined> {
218247
const cacheRoot = getScriptEnvCacheRoot(this.globalStorageUri);
219248
const envDir = getScriptEnvDir(this.globalStorageUri, cacheKey);
220249
await fs.ensureDir(cacheRoot.fsPath);
@@ -279,7 +308,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
279308
return { kind: 'uncertain' };
280309
}
281310
} catch (error) {
282-
return this.isFileNotFoundError(error) ? { kind: 'absent' } : { kind: 'uncertain' };
311+
return isFileNotFoundError(error) ? { kind: 'absent' } : { kind: 'uncertain' };
283312
}
284313

285314
let resolvedEntry: string | undefined;
@@ -329,8 +358,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
329358
if (environmentStatus !== 'expected') {
330359
return { kind: environmentStatus };
331360
}
332-
const releaseComparison = this.comparePythonReleases(environment.version, selectedBase.environment.version);
333-
if (releaseComparison !== 'same') {
361+
if (!this.areEqualPythonReleases(environment.version, selectedBase.environment.version)) {
334362
return { kind: 'stale' };
335363
}
336364
const requiresPython = metadata.requiresPython?.trim();
@@ -385,22 +413,21 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
385413
return {};
386414
}
387415
if (
388-
this.comparePythonReleases(result.environment.version, selectedBase.environment.version) !== 'same' ||
416+
!this.areEqualPythonReleases(result.environment.version, selectedBase.environment.version) ||
389417
(await inspectOwnedCacheEntry(result.environment, cacheRoot, envDir)) !== 'expected'
390418
) {
391419
this.log.error('Created inline-script environment does not match the requested cache entry.');
392420
await this.removeCacheEntry(envDir);
393421
return {};
394422
}
395423

396-
const sidecar: InlineScriptEnvMeta = {
397-
schemaVersion: META_SCHEMA_VERSION,
398-
baseInterpreterPath: selectedBase.canonicalPath,
399-
baseInterpreterVersion: selectedBase.environment.version,
400-
lastUsedAt: new Date().toISOString(),
401-
};
402424
try {
403-
await writeMetaJson(envDir, sidecar);
425+
await writeMetaJson(envDir, {
426+
schemaVersion: META_SCHEMA_VERSION,
427+
baseInterpreterPath: selectedBase.canonicalPath,
428+
baseInterpreterVersion: selectedBase.environment.version,
429+
lastUsedAt: new Date().toISOString(),
430+
});
404431
} catch (error) {
405432
this.log.error(`Failed to record inline-script cache metadata: ${this.errorMessage(error)}`);
406433
await this.removeCacheEntry(envDir);
@@ -420,22 +447,13 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
420447
}
421448
}
422449

423-
private isFileNotFoundError(error: unknown): boolean {
424-
return (
425-
typeof error === 'object' &&
426-
error !== null &&
427-
'code' in error &&
428-
(error as NodeJS.ErrnoException).code === 'ENOENT'
429-
);
430-
}
431-
432-
private comparePythonReleases(actual: string, expected: string): PythonReleaseComparison {
450+
private areEqualPythonReleases(actual: string, expected: string): boolean {
433451
const actualRelease = parseReleaseSegments(actual);
434452
const expectedRelease = parseReleaseSegments(expected);
435453
if (actualRelease === undefined || expectedRelease === undefined) {
436-
return 'uncertain';
454+
return false;
437455
}
438-
return compareReleaseSegments(actualRelease, expectedRelease) === 0 ? 'same' : 'different';
456+
return compareReleaseSegments(actualRelease, expectedRelease) === 0;
439457
}
440458

441459
private errorMessage(error: unknown): string {
@@ -447,19 +465,3 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
447465
this._onDidChangeEnvironment.dispose();
448466
}
449467
}
450-
451-
interface SelectedBaseInterpreter {
452-
readonly environment: PythonEnvironment;
453-
readonly canonicalPath: string;
454-
}
455-
456-
interface BuildCacheEntryResult {
457-
readonly environment?: PythonEnvironment;
458-
readonly retainLock?: boolean;
459-
}
460-
461-
type CacheEntryInspection =
462-
| { readonly kind: 'absent' | 'stale' | 'uncertain' }
463-
| { readonly kind: 'reusable'; readonly environment: PythonEnvironment };
464-
465-
type PythonReleaseComparison = 'same' | 'different' | 'uncertain';

src/managers/pyenv/pyenvManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ResolveEnvironmentContext,
1616
SetEnvironmentScope,
1717
} from '../../api';
18+
import { PYENV_MANAGER_ID } from '../../common/constants';
1819
import { PyenvStrings } from '../../common/localize';
1920
import { traceError, traceInfo } from '../../common/logging';
2021
import { StopWatch } from '../../common/stopWatch';
@@ -120,7 +121,7 @@ export class PyEnvManager implements EnvironmentManager, Disposable {
120121
if (toolSource === 'none') {
121122
result = 'tool_not_found';
122123
if (this.projectManager) {
123-
await notifyMissingManagerIfDefault('ms-python.python:pyenv', this.projectManager, this.api);
124+
await notifyMissingManagerIfDefault(PYENV_MANAGER_ID, this.projectManager, this.api);
124125
}
125126
}
126127
} catch (ex) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import assert from 'assert';
5+
import { isFileNotFoundError } from '../../common/utils/filesystem';
6+
7+
suite('filesystem utilities', () => {
8+
test('recognizes ENOENT errors', () => {
9+
const error = Object.assign(new Error('missing'), { code: 'ENOENT' });
10+
11+
assert.strictEqual(isFileNotFoundError(error), true);
12+
});
13+
14+
test('rejects other errors and non-errors', () => {
15+
assert.strictEqual(isFileNotFoundError(Object.assign(new Error('not a directory'), { code: 'ENOTDIR' })), false);
16+
assert.strictEqual(isFileNotFoundError(new Error('missing code')), false);
17+
assert.strictEqual(isFileNotFoundError(undefined), false);
18+
assert.strictEqual(isFileNotFoundError('ENOENT'), false);
19+
});
20+
});

0 commit comments

Comments
 (0)