Skip to content

Commit 7239f9d

Browse files
Merge branch 'microsoft:main' into kustoQuery
2 parents eca209a + 3c072d1 commit 7239f9d

34 files changed

Lines changed: 1668 additions & 472 deletions

.github/hooks/scripts/stop_hook.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,12 @@ def main() -> int:
110110
response = {
111111
"hookSpecificOutput": {
112112
"hookEventName": "Stop",
113-
"decision": "block",
113+
"decision": "warn",
114114
"reason": (
115115
"You have uncommitted TypeScript changes. "
116-
"Before finishing, use the run-pre-commit-checks skill "
116+
"Before finishing, consider using the run-pre-commit-checks skill "
117117
"or manually run: npm run lint && npm run compile-tests && npm run unittest. "
118-
"If checks pass and changes are ready, commit them. "
119-
"If this session is just research/exploration, you can proceed without committing."
118+
"Ask the user whether to commit or leave changes uncommitted."
120119
),
121120
}
122121
}
@@ -128,10 +127,10 @@ def main() -> int:
128127
response = {
129128
"hookSpecificOutput": {
130129
"hookEventName": "Stop",
131-
"decision": "block",
130+
"decision": "warn",
132131
"reason": (
133132
"You have staged changes that haven't been committed. "
134-
"Either commit them with a proper message or unstage them before finishing."
133+
"Ask the user whether to commit them or leave them staged."
135134
),
136135
}
137136
}

build/azure-pipeline.pre-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ extends:
8383
displayName: Update telemetry in package.json
8484

8585
- script: python ./build/update_ext_version.py --for-publishing
86-
displayName: Update build number
86+
displayName: Validate version number
8787

8888
- bash: |
8989
mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin

build/test_update_ext_version.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99

1010
TEST_DATETIME = "2022-03-14 01:23:45"
1111

12-
# The build ID is calculated via:
13-
# "1" + datetime.datetime.strptime(TEST_DATETIME,"%Y-%m-%d %H:%M:%S").strftime('%j%H%M')
14-
EXPECTED_BUILD_ID = "10730123"
15-
1612

1713
def create_package_json(directory, version):
1814
"""Create `package.json` in `directory` with a specified version of `version`."""
@@ -71,7 +67,7 @@ def test_invalid_args(tmp_path, version, args):
7167
["--build-id", "999999999999"],
7268
("1", "1", "999999999999", "rc"),
7369
),
74-
("1.1.0-rc", [], ("1", "1", EXPECTED_BUILD_ID, "rc")),
70+
("1.1.0-rc", [], ("1", "1", "0", "rc")),
7571
(
7672
"1.0.0-rc",
7773
["--release"],
@@ -80,7 +76,7 @@ def test_invalid_args(tmp_path, version, args):
8076
(
8177
"1.1.0-rc",
8278
["--for-publishing"],
83-
("1", "1", EXPECTED_BUILD_ID, ""),
79+
("1", "1", "0", ""),
8480
),
8581
(
8682
"1.0.0-rc",
@@ -95,7 +91,7 @@ def test_invalid_args(tmp_path, version, args):
9591
(
9692
"1.1.0-rc",
9793
[],
98-
("1", "1", EXPECTED_BUILD_ID, "rc"),
94+
("1", "1", "0", "rc"),
9995
),
10096
],
10197
)

build/update_ext_version.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ def main(package_json: pathlib.Path, argv: Sequence[str]) -> None:
7979
)
8080

8181
print(f"Updating build FROM: {package['version']}")
82+
83+
# Pre-release without --build-id: version is managed by the CI template
84+
# (standardizedVersioning). Just strip suffix if publishing.
85+
if not args.release and not args.build_id:
86+
if args.for_publishing and len(suffix):
87+
package["version"] = ".".join((major, minor, micro))
88+
print(f"Updating build TO: {package['version']}")
89+
package_json.write_text(
90+
json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8"
91+
)
92+
return
93+
8294
if args.build_id:
8395
# If build id is provided it should fall within the 0-INT32 max range
8496
# that the max allowed value for publishing to the Marketplace.
@@ -88,9 +100,6 @@ def main(package_json: pathlib.Path, argv: Sequence[str]) -> None:
88100
package["version"] = ".".join((major, minor, str(args.build_id)))
89101
elif args.release:
90102
package["version"] = ".".join((major, minor, micro))
91-
else:
92-
# micro version only updated for pre-release.
93-
package["version"] = ".".join((major, minor, micro_build_number()))
94103

95104
if not args.for_publishing and not args.release and len(suffix):
96105
package["version"] += "-" + suffix

docs/startup-flow.md

Lines changed: 70 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -19,104 +19,79 @@ python environments extension begins activation
1919
**ASYNC (setImmediate callback, still in extension.ts):**
2020
1. spawn PET process (`createNativePythonFinder`)
2121
1. sets up a JSON-RPC connection to it over stdin/stdout
22-
2. register all built-in managers + shell env init in parallel (Promise.all):
23-
- `shellStartupVarsMgr.initialize()`
24-
- for each manager (system, conda, pyenv, pipenv, poetry):
25-
1. check if tool exists (e.g. `getConda(nativeFinder)` asks PET for the conda binary)
26-
2. if tool not found → log, return early (manager not registered)
27-
3. if tool found → create manager, call `api.registerEnvironmentManager(manager)`
28-
- this adds it to the `EnvironmentManagers` map
29-
- fires `onDidChangeEnvironmentManager``ManagerReady` deferred resolves for this manager
30-
3. all registrations complete (Promise.all resolves)
22+
2. register all built-in managers in parallel (Promise.all):
23+
- system: create SysPythonManager + VenvManager + PipPackageManager, register immediately (✅ NO PET call, sets up file watcher)
24+
- conda: `getConda(nativeFinder)` checks settings → cache → persistent state → PATH
25+
- pyenv & pipenv & poetry: create PyEnvManager, register immediately
26+
- ✅ NO PET call — always registers unconditionally (lazy discovery)
27+
- shellStartupVars: initialize
28+
- all managers fire `onDidChangeEnvironmentManager` → ManagerReady resolves
29+
3. all registrations complete (Promise.all resolves) — fast, typically milliseconds
30+
3131

3232
**--- gate point: `applyInitialEnvironmentSelection` ---**
3333

34-
📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
35-
36-
1. for each workspace folder + global scope (no workspace case), run `resolvePriorityChainCore` to find manager:
37-
- P1: pythonProjects[] setting → specific manager for this project
38-
- P2: user-configured defaultEnvManager setting
39-
- P3: user-configured python.defaultInterpreterPath → nativeFinder.resolve(path)
40-
- P4: auto-discovery → try venv manager, fall back to system python
41-
- for workspace scope: call `venvManager.get(scope)`
42-
- if venv found (local .venv/venv) → use venv manager with that env
43-
- if no local venv → venv manager may still return its `globalEnv` (system Python)
44-
- if venvManager.get returns undefined → fall back to system python manager
45-
- for global scope: use system python manager directly
46-
47-
2. get the environment from the winning priority level:
48-
49-
--- fork point: `result.environment ?? await result.manager.get(folder.uri)` ---
50-
left side truthy = envPreResolved | left side undefined = managerDiscovery
51-
52-
envPreResolved — P3 won (interpreter → manager):
53-
`resolvePriorityChainCore` calls `tryResolveInterpreterPath()`:
54-
1. `nativeFinder.resolve(path)` — single PET call, resolves just this one binary
55-
2. find which manager owns the resolved env (by managerId)
56-
3. return { manager, environment } — BOTH are known
57-
→ result.environment is set → the `??` short-circuits
58-
→ no `manager.get()` called, no `initialize()`, no full discovery
59-
60-
managerDiscovery — P1, P2, or P4 won (manager → interpreter):
61-
`resolvePriorityChainCore` returns { manager, environment: undefined }
62-
→ falls through to `await result.manager.get(scope)`
63-
64-
**--- inner fork: fast path vs slow path (tryFastPathGet in fastPath.ts) ---**
65-
Conditions checked before entering fast path:
66-
a. `_initialized` deferred is undefined (never created) OR has not yet completed
67-
b. scope is a `Uri` (not global/undefined)
68-
69-
FAST PATH (background init kickoff + optional early return):
70-
**Race-condition safety (runs before any await):**
71-
1. if `_initialized` doesn't exist yet:
72-
- create deferred and **register immediately** via `setInitialized()` callback
73-
- this blocks concurrent callers from spawning duplicate background inits
74-
- kick off `startBackgroundInit()` as fire-and-forget
75-
- this happens as soon as (a) and (b) are true, **even if** no persisted path exists
76-
2. get project fsPath: `getProjectFsPathForScope(api, scope)`
77-
- prefers resolved project path if available, falls back to scope.fsPath
78-
- shared across all managers to avoid lambda duplication
79-
3. read persisted path (only if scope is a `Uri`; may return undefined)
80-
4. if a persisted path exists:
81-
- attempt `resolve(persistedPath)`
82-
- failure (no env, mismatched manager, etc.) → fall through to SLOW PATH
83-
- success → return env immediately (background init continues in parallel)
84-
**Failure recovery (in startBackgroundInit error handler):**
85-
- if background init throws: `setInitialized(undefined)` — clear deferred so next `get()` call retries init
86-
87-
SLOW PATH — fast path conditions not met, or fast path failed:
88-
4. `initialize()` — lazy, once-only per manager (guarded by `_initialized` deferred)
89-
**Once-only guarantee:**
90-
- first caller creates `_initialized` deferred (if not already created by fast path)
91-
- concurrent callers see the existing deferred and await it instead of re-running init
92-
- deferred is **not cleared on failure** here (unlike in fast-path background handler)
93-
so only one init attempt runs, but subsequent calls still await the same failed init
94-
**Note:** In the fast path, if background init fails, the deferred is cleared to allow retry
95-
a. `nativeFinder.refresh(hardRefresh=false)`:
96-
→ internally calls `handleSoftRefresh()` → computes cache key from options
97-
- on reload: cache is empty (Map was destroyed) → cache miss
98-
- falls through to `handleHardRefresh()`
99-
→ `handleHardRefresh()` adds request to WorkerPool queue (concurrency 1):
100-
1. run `configure()` to setup PET search paths
101-
2. run `refresh` — PET scans filesystem
102-
- PET may use its own on-disk cache
103-
3. returns NativeInfo[] (all envs of all types)
104-
- result stored in in-memory cache so subsequent managers get instant cache hit
105-
b. filter results to this manager's env type (e.g. conda filters to kind=conda)
106-
c. convert NativeEnvInfo → PythonEnvironment objects → populate collection
107-
d. `loadEnvMap()` — reads persisted env path from workspace state
108-
→ matches path against PET discovery results
109-
→ populates `fsPathToEnv` map
110-
5. look up scope in `fsPathToEnv` → return the matched env
111-
112-
📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection }
113-
114-
3. env is cached in memory (no settings.json write)
115-
4. Python extension / status bar can now get the selected env via `api.getEnvironment(scope)`
116-
117-
📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? }
118-
119-
**POST-INIT:**
34+
📊 TELEMETRY: ENV_SELECTION.STARTED { duration, registeredManagerCount, registeredManagerIds, workspaceFolderCount }
35+
36+
**Step 1 — pick a manager** (`resolvePriorityChainCore`, per workspace folder + global):
37+
38+
| Priority | Source | Returns |
39+
|----------|--------|---------|
40+
| P1 | `pythonProjects[]` setting | manager only |
41+
| P2 | `defaultEnvManager` setting | manager only |
42+
| P3 | `python.defaultInterpreterPath``nativeFinder.resolve(path)` | manager **+ environment** |
43+
| P4 | auto-discovery: venv → system python fallback | manager only |
44+
45+
**Step 2 — get the environment** (`result.environment ?? await result.manager.get(scope)`):
46+
47+
- **If P3 won:** environment is already resolved → done, no `get()` call needed.
48+
- **Otherwise:** calls `manager.get(scope)`, which has two internal paths:
49+
50+
**Fast path** (`tryFastPathGet` in `fastPath.ts`) — entered when `_initialized` hasn't completed and scope is a `Uri`:
51+
1. Synchronously create `_initialized` deferred + kick off `startBackgroundInit()` (fire-and-forget full PET discovery)
52+
2. Read persisted env path from workspace state
53+
3. If persisted path exists → `resolve(path)` → return immediately (background init continues in parallel)
54+
4. If no persisted path or resolve fails → fall through to slow path
55+
- *On background init failure:* clears `_initialized` so next `get()` retries
56+
57+
**Slow path** — fast path skipped or failed:
58+
1. `initialize()` — lazy, once-only (guarded by `_initialized` deferred, concurrent callers await it)
59+
- `nativeFinder.refresh(false)` → PET scan (cached across managers after first call)
60+
- Filter results to this manager's type → populate `collection`
61+
- `loadEnvMap()` → match persisted paths against discovered envs
62+
2. Look up scope in `fsPathToEnv` → return matched env
63+
64+
📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration, scope, prioritySource, managerId, path, hasPersistedSelection }
65+
66+
**Step 3 — done:**
67+
- env cached in memory (no settings.json write)
68+
- available via `api.getEnvironment(scope)`
69+
70+
📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration, result, failureStage?, errorType? }
71+
72+
---
73+
74+
### Other entry points to `initialize()`
75+
76+
All three trigger `initialize()` lazily (once-only, guarded by `_initialized` deferred). After the first call completes, subsequent calls are no-ops.
77+
78+
**`manager.get(scope)`** — environment selection (Step 2 above):
79+
- Called during `applyInitialEnvironmentSelection` or when settings change triggers re-selection
80+
- Fast path may resolve immediately; slow path awaits `initialize()`
81+
82+
**`manager.getEnvironments(scope)`** — sidebar / listing:
83+
- Called when user expands a manager node in the Python environments panel
84+
- Also called by any API consumer requesting the full environment list
85+
- If PET cache populated from earlier `get()` → instant hit; otherwise warm PET call
86+
87+
**`manager.resolve(context)`** — path resolution:
88+
- Called when resolving a specific Python binary path to check if it belongs to this manager
89+
- Used by `tryResolveInterpreterPath()` in the priority chain (P3) and by external API consumers
90+
- Awaits `initialize()`, then delegates to manager-specific resolve (e.g., `resolvePipenvPath`)
91+
92+
---
93+
94+
POST-INIT:
12095
1. register terminal package watcher
12196
2. register settings change listener (`registerInterpreterSettingsChangeListener`) — re-runs priority chain if settings change
12297
3. initialize terminal manager

examples/sample1/src/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,10 +818,14 @@ export interface PythonEnvironmentManagerRegistrationApi {
818818
* Register an environment manager implementation.
819819
*
820820
* @param manager Environment Manager implementation to register.
821+
* @param options Optional registration options.
822+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
823+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
824+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
821825
* @returns A disposable that can be used to unregister the environment manager.
822826
* @see {@link EnvironmentManager}
823827
*/
824-
registerEnvironmentManager(manager: EnvironmentManager): Disposable;
828+
registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable;
825829
}
826830

827831
export interface PythonEnvironmentItemApi {
@@ -922,10 +926,14 @@ export interface PythonPackageManagerRegistrationApi {
922926
* Register a package manager implementation.
923927
*
924928
* @param manager Package Manager implementation to register.
929+
* @param options Optional registration options.
930+
* @param options.extensionId The extension ID of the calling extension. This is used as a fallback when
931+
* automatic extension detection fails, such as during F5 debugging where the extension's file path
932+
* does not contain its marketplace ID. If automatic detection succeeds, this value is ignored.
925933
* @returns A disposable that can be used to unregister the package manager.
926934
* @see {@link PackageManager}
927935
*/
928-
registerPackageManager(manager: PackageManager): Disposable;
936+
registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable;
929937
}
930938

931939
export interface PythonPackageGetterApi {

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-python-envs",
33
"displayName": "Python Environments",
44
"description": "Provides a unified python environment experience",
5-
"version": "1.27.0",
5+
"version": "1.29.0",
66
"publisher": "ms-python",
77
"preview": true,
88
"engines": {
@@ -124,7 +124,7 @@
124124
"python-envs.workspaceSearchPaths": {
125125
"type": "array",
126126
"description": "%python-envs.workspaceSearchPaths.description%",
127-
"default": ["./**/.venv"],
127+
"default": [".venv", "*/.venv"],
128128
"scope": "resource",
129129
"items": {
130130
"type": "string"

0 commit comments

Comments
 (0)