Skip to content

Commit 5900317

Browse files
committed
switch to optimistic +lazy registration for pipenv, pyenv and poetry managers
1 parent a040b37 commit 5900317

8 files changed

Lines changed: 60 additions & 264 deletions

File tree

docs/startup-flow.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,22 @@ ASYNC (setImmediate callback, still in extension.ts):
1818
1. spawn PET process (`createNativePythonFinder`)
1919
1. sets up a JSON-RPC connection to it over stdin/stdout
2020
2. register all built-in managers in parallel (Promise.all):
21-
- for each manager (system, conda, pyenv, pipenv, poetry):
22-
1. check if tool exists (e.g. `getConda(nativeFinder)` asks PET for the conda binary)
23-
2. if tool not found → log, return early (manager not registered)
24-
3. if tool found → create manager, call `api.registerEnvironmentManager(manager)`
25-
- this adds it to the `EnvironmentManagers` map
26-
- fires `onDidChangeEnvironmentManager``ManagerReady` deferred resolves for this manager
27-
3. all registrations complete (Promise.all resolves)
21+
- system: create SysPythonManager + VenvManager + PipPackageManager, register immediately
22+
- ✅ NO PET call — managers are created and registered with no tool detection
23+
- sets up file watcher for venv activation scripts
24+
- conda: `getConda(nativeFinder)` checks settings → cache → persistent state → PATH
25+
- if found → register CondaEnvManager + CondaPackageManager
26+
- if not found → PET fallback as last resort (rarely hit, conda is usually on PATH)
27+
- if not found at all → skip, send MANAGER_REGISTRATION.SKIPPED telemetry
28+
- pyenv: create PyEnvManager, register immediately
29+
- ✅ NO PET call — always registers unconditionally (lazy discovery)
30+
- pipenv: create PipenvManager, register immediately
31+
- ✅ NO PET call — always registers unconditionally (lazy discovery)
32+
- poetry: create PoetryManager + PoetryPackageManager, register immediately
33+
- ✅ NO PET call — always registers unconditionally (lazy discovery)
34+
- shellStartupVars: initialize
35+
- all managers fire `onDidChangeEnvironmentManager` → ManagerReady resolves
36+
3. all registrations complete (Promise.all resolves) — fast, typically milliseconds
2837

2938
--- gate point: `applyInitialEnvironmentSelection` ---
3039
📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
@@ -55,26 +64,29 @@ ASYNC (setImmediate callback, still in extension.ts):
5564
managerDiscovery — P1, P2, or P4 won (manager → interpreter):
5665
`resolvePriorityChainCore` returns { manager, environment: undefined }
5766
→ result.environment is undefined → falls through to `await result.manager.get(scope)`
58-
`manager.get(scope)` (e.g. `CondaEnvManager.get()`):
67+
`manager.get(scope)` (e.g. `CondaEnvManager.get()`, `PyEnvManager.get()`):
5968
4. `initialize()` — lazy, once-only per manager (guarded by deferred)
6069
a. `nativeFinder.refresh(hardRefresh=false)`:
61-
`handleSoftRefresh()` checks in-memory cache (Map) for key 'all' (bc one big scan, shared cache, all managers benefit)
70+
`handleSoftRefresh()` checks in-memory cache (Map) for key 'all'
6271
- on reload: cache is empty (Map was destroyed) → cache miss
6372
- falls through to `handleHardRefresh()`
6473
`handleHardRefresh()`:
6574
- adds request to WorkerPool queue (concurrency 1, so serialized)
6675
- when its turn comes, calls `doRefresh()`:
6776
1. `configure()` — JSON-RPC to PET with search paths, conda/poetry/pipenv paths, cache dir
6877
2. `refresh` — JSON-RPC to PET, PET scans filesystem
78+
- PET has had time to warm up since spawn (registration was fast)
6979
- PET may use its own on-disk cache (cacheDirectory) to speed this up
7080
- PET streams back results as 'environment' and 'manager' notifications
7181
- envs missing version/prefix get an inline resolve() call
7282
3. returns NativeInfo[] (all envs of all types)
7383
- result stored in in-memory cache under key 'all'
7484
→ subsequent managers calling nativeFinder.refresh(false) get cache hit → instant
75-
b. filter results to this manager's env type (e.g. conda filters to kind=conda)
76-
c. convert NativeEnvInfo → PythonEnvironment objects → populate collection
77-
d. `loadEnvMap()` — reads persisted env path from workspace state
85+
b. filter results to this manager's env type (e.g. conda filters to kind=conda, pyenv to kind=pyenv)
86+
c. for pipenv/poetry/pyenv: if tool CLI was not found via PATH during registration,
87+
extract tool executable from PET's manager info in the refresh results
88+
d. convert NativeEnvInfo → PythonEnvironment objects → populate collection
89+
e. `loadEnvMap()` — reads persisted env path from workspace state
7890
→ matches path against freshly discovered collection via `findEnvironmentByPath()`
7991
→ populates `fsPathToEnv` map
8092
5. look up scope in `fsPathToEnv` → return the matched env
@@ -86,6 +98,15 @@ ASYNC (setImmediate callback, still in extension.ts):
8698

8799
📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? }
88100

101+
SIDEBAR ACCESS (on-demand, if user opens Python environments panel):
102+
- view iterates `providers.managers` → all registered managers appear (including pyenv/pipenv/poetry)
103+
- user expands a manager node → `getChildren()``manager.getEnvironments('all')`
104+
`initialize()` (lazy, once-only) → `nativeFinder.refresh(false)`:
105+
- if cache populated from earlier env selection → instant cache hit
106+
- if first access → warm PET call (no concurrent pressure, single caller)
107+
→ environments appear under the manager node
108+
→ if no environments found → "No environments" placeholder shown
109+
89110
POST-INIT:
90111
1. register terminal package watcher
91112
2. register settings change listener (`registerInterpreterSettingsChangeListener`) — re-runs priority chain if settings change

src/extension.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -601,12 +601,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
601601
'conda',
602602
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
603603
),
604-
safeRegister('pyenv', registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager)),
605-
safeRegister('pipenv', registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager)),
606-
safeRegister(
607-
'poetry',
608-
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
609-
),
604+
safeRegister('pyenv', registerPyenvFeatures(nativeFinder, context.subscriptions)),
605+
safeRegister('pipenv', registerPipenvFeatures(nativeFinder, context.subscriptions)),
606+
safeRegister('poetry', registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel)),
610607
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
611608
]);
612609

src/managers/pipenv/main.ts

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,17 @@
11
import { Disposable } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
4-
import { EventNames } from '../../common/telemetry/constants';
5-
import { classifyError } from '../../common/telemetry/errorClassifier';
6-
import { sendTelemetryEvent } from '../../common/telemetry/sender';
74
import { getPythonApi } from '../../features/pythonApi';
8-
import { PythonProjectManager } from '../../internal.api';
95
import { NativePythonFinder } from '../common/nativePythonFinder';
106
import { PipenvManager } from './pipenvManager';
11-
import { getPipenv, hasPipenvEnvironments } from './pipenvUtils';
12-
13-
import { notifyMissingManagerIfDefault } from '../common/utils';
147

158
export async function registerPipenvFeatures(
169
nativeFinder: NativePythonFinder,
1710
disposables: Disposable[],
18-
projectManager: PythonProjectManager,
1911
): Promise<void> {
20-
let stage = 'getPythonApi';
2112
const api: PythonEnvironmentApi = await getPythonApi();
2213

23-
try {
24-
stage = 'getPipenv';
25-
const pipenv = await getPipenv(nativeFinder);
26-
27-
// Register the manager if the CLI is found, or if there are existing pipenv environments.
28-
// This allows users with existing pipenv environments to still see and use them.
29-
stage = 'hasPipenvEnvironments';
30-
const hasPipenvEnvs = !pipenv && (await hasPipenvEnvironments(nativeFinder));
31-
32-
if (pipenv || hasPipenvEnvs) {
33-
stage = 'createManager';
34-
const mgr = new PipenvManager(nativeFinder, api);
35-
stage = 'registerManager';
36-
disposables.push(mgr, api.registerEnvironmentManager(mgr));
37-
if (!pipenv) {
38-
traceInfo(
39-
'Pipenv CLI not found, but pipenv environments were discovered. Registering manager for read-only environment management. To enable full pipenv features, set the "python.pipenvPath" setting to the path of your pipenv executable.',
40-
);
41-
}
42-
} else {
43-
traceInfo(
44-
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
45-
);
46-
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
47-
managerName: 'pipenv',
48-
reason: 'tool_not_found',
49-
});
50-
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
51-
}
52-
} catch (ex) {
53-
const failureStage = (ex as Error & { failureStage?: string })?.failureStage ?? stage;
54-
traceInfo(
55-
'Pipenv not found, turning off pipenv features. If you have pipenv installed in a non-standard location, set the "python.pipenvPath" setting.',
56-
ex,
57-
);
58-
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
59-
managerName: 'pipenv',
60-
errorType: classifyError(ex),
61-
failureStage,
62-
});
63-
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
64-
}
14+
traceInfo('Registering pipenv manager (environments will be discovered lazily)');
15+
const mgr = new PipenvManager(nativeFinder, api);
16+
disposables.push(mgr, api.registerEnvironmentManager(mgr));
6517
}

src/managers/pipenv/pipenvUtils.ts

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -51,37 +51,16 @@ export async function clearPipenvCache(): Promise<void> {
5151
pipenvPath = undefined;
5252
}
5353

54-
/**
55-
* Check if any pipenv environments exist without requiring the pipenv CLI.
56-
* This allows the manager to be registered even if the CLI is not found.
57-
*/
58-
export async function hasPipenvEnvironments(nativeFinder: NativePythonFinder): Promise<boolean> {
59-
let stage = 'nativeFinderRefresh';
60-
try {
61-
const data = await nativeFinder.refresh(false);
62-
stage = 'filterPipenvEnvs';
63-
return data
64-
.filter((e) => isNativeEnvInfo(e))
65-
.some((e) => (e as NativeEnvInfo).kind === NativePythonEnvironmentKind.pipenv);
66-
} catch (ex) {
67-
const err = ex instanceof Error ? ex : new Error(String(ex));
68-
(err as Error & { failureStage?: string }).failureStage = `hasPipenvEnvironments:${stage}`;
69-
throw err;
70-
}
71-
}
72-
7354
function getPipenvPathFromSettings(): string | undefined {
7455
const pipenvPath = getSettingWorkspaceScope<string>('python', 'pipenvPath');
7556
return pipenvPath ? pipenvPath : undefined;
7657
}
7758

78-
export async function getPipenv(native?: NativePythonFinder): Promise<string | undefined> {
79-
let stage = 'checkSettings';
59+
export async function getPipenv(): Promise<string | undefined> {
8060
try {
8161
// Priority 1: Settings (if explicitly set and valid)
8262
const settingPath = getPipenvPathFromSettings();
8363
if (settingPath) {
84-
stage = 'validateSettingsPath';
8564
if (await fs.exists(untildify(settingPath))) {
8665
traceInfo(`Using pipenv from settings: ${settingPath}`);
8766
return untildify(settingPath);
@@ -90,22 +69,17 @@ export async function getPipenv(native?: NativePythonFinder): Promise<string | u
9069
}
9170

9271
// Priority 2: In-memory cache
93-
stage = 'checkCache';
9472
if (pipenvPath) {
95-
stage = 'validateCachePath';
9673
if (await fs.exists(untildify(pipenvPath))) {
9774
return untildify(pipenvPath);
9875
}
9976
pipenvPath = undefined;
10077
}
10178

10279
// Priority 3: Persistent state
103-
stage = 'getPersistentState';
10480
const state = await getWorkspacePersistentState();
105-
stage = 'checkPersistentState';
10681
const storedPath = await state.get<string>(PIPENV_PATH_KEY);
10782
if (storedPath) {
108-
stage = 'validatePersistentStatePath';
10983
if (await fs.exists(untildify(storedPath))) {
11084
pipenvPath = storedPath;
11185
traceInfo(`Using pipenv from persistent state: ${pipenvPath}`);
@@ -115,37 +89,18 @@ export async function getPipenv(native?: NativePythonFinder): Promise<string | u
11589
}
11690

11791
// Priority 4: PATH lookup
118-
stage = 'pathLookup';
11992
const foundPipenv = await findPipenv();
12093
if (foundPipenv) {
12194
pipenvPath = foundPipenv;
12295
traceInfo(`Found pipenv in PATH: ${foundPipenv}`);
12396
return foundPipenv;
12497
}
12598

126-
// Priority 5: Native finder as fallback
127-
stage = 'nativeFinderRefresh';
128-
if (native) {
129-
const data = await native.refresh(false);
130-
stage = 'filterNativeFinderResults';
131-
const managers = data
132-
.filter((e) => !isNativeEnvInfo(e))
133-
.map((e) => e as NativeEnvManagerInfo)
134-
.filter((e) => e.tool.toLowerCase() === 'pipenv');
135-
if (managers.length > 0) {
136-
pipenvPath = managers[0].executable;
137-
traceInfo(`Using pipenv from native finder: ${pipenvPath}`);
138-
stage = 'persistNativeFinderResult';
139-
await state.set(PIPENV_PATH_KEY, pipenvPath);
140-
return pipenvPath;
141-
}
142-
}
143-
144-
traceInfo('Pipenv not found');
99+
traceInfo('Pipenv not found via settings, cache, or PATH');
145100
return undefined;
146101
} catch (ex) {
147102
const err = ex instanceof Error ? ex : new Error(String(ex));
148-
(err as Error & { failureStage?: string }).failureStage = `getPipenv:${stage}`;
103+
(err as Error & { failureStage?: string }).failureStage = `getPipenv`;
149104
throw err;
150105
}
151106
}

src/managers/poetry/main.ts

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,26 @@
11
import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
4-
import { EventNames } from '../../common/telemetry/constants';
5-
import { classifyError } from '../../common/telemetry/errorClassifier';
6-
import { sendTelemetryEvent } from '../../common/telemetry/sender';
74
import { getPythonApi } from '../../features/pythonApi';
8-
import { PythonProjectManager } from '../../internal.api';
95
import { NativePythonFinder } from '../common/nativePythonFinder';
10-
import { notifyMissingManagerIfDefault } from '../common/utils';
116
import { PoetryManager } from './poetryManager';
127
import { PoetryPackageManager } from './poetryPackageManager';
13-
import { getPoetry, getPoetryVersion } from './poetryUtils';
148

159
export async function registerPoetryFeatures(
1610
nativeFinder: NativePythonFinder,
1711
disposables: Disposable[],
1812
outputChannel: LogOutputChannel,
19-
projectManager: PythonProjectManager,
2013
): Promise<void> {
21-
let stage = 'getPythonApi';
2214
const api: PythonEnvironmentApi = await getPythonApi();
2315

24-
try {
25-
stage = 'getPoetry';
26-
const poetryPath = await getPoetry(nativeFinder);
27-
if (poetryPath) {
28-
traceInfo(
29-
'The `shell` command is not available by default in Poetry versions 2.0.0 and above. Therefore all shell activation will be handled by calling `source <path-to-activate>`. If you face any problems with shell activation, please file an issue at https://github.com/microsoft/vscode-python-environments/issues to help us improve this implementation.',
30-
);
31-
stage = 'getPoetryVersion';
32-
const version = await getPoetryVersion(poetryPath);
33-
traceInfo(`Poetry found at ${poetryPath}, version: ${version}`);
34-
stage = 'createEnvManager';
35-
const envManager = new PoetryManager(nativeFinder, api);
36-
stage = 'createPkgManager';
37-
const pkgManager = new PoetryPackageManager(api, outputChannel, envManager);
16+
traceInfo('Registering poetry manager (environments will be discovered lazily)');
17+
const envManager = new PoetryManager(nativeFinder, api);
18+
const pkgManager = new PoetryPackageManager(api, outputChannel, envManager);
3819

39-
stage = 'registerManagers';
40-
disposables.push(
41-
envManager,
42-
pkgManager,
43-
api.registerEnvironmentManager(envManager),
44-
api.registerPackageManager(pkgManager),
45-
);
46-
} else {
47-
traceInfo('Poetry not found, turning off poetry features.');
48-
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, {
49-
managerName: 'poetry',
50-
reason: 'tool_not_found',
51-
});
52-
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
53-
}
54-
} catch (ex) {
55-
const failureStage = (ex as Error & { failureStage?: string })?.failureStage ?? stage;
56-
traceInfo('Poetry not found, turning off poetry features.', ex);
57-
sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_FAILED, undefined, {
58-
managerName: 'poetry',
59-
errorType: classifyError(ex),
60-
failureStage,
61-
});
62-
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
63-
}
20+
disposables.push(
21+
envManager,
22+
pkgManager,
23+
api.registerEnvironmentManager(envManager),
24+
api.registerPackageManager(pkgManager),
25+
);
6426
}

0 commit comments

Comments
 (0)