Skip to content

Commit 20f42b0

Browse files
authored
Merge branch 'main' into implicit-boar
2 parents 8a033b5 + 6ec9f33 commit 20f42b0

37 files changed

Lines changed: 1683 additions & 433 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: 80 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,89 +5,94 @@
55
user opens VS Code
66
python environments extension begins activation
77

8-
SYNC (`activate` in extension.ts):
9-
1. create core objects: ProjectManager, EnvironmentManagers, ManagerReady
10-
2. `setPythonApi()` — API object created, deferred resolved (API is now available to consumers)
11-
3. create views (EnvManagerView, ProjectView), status bar, terminal manager
12-
4. register all commands
13-
5. activate() returns — extension is "active" from VS Code's perspective
8+
**SYNC (`activate` in extension.ts):**
9+
1. create StatusBar, ProjectManager, EnvVarManager, EnvironmentManagers, ManagerReady
10+
2. create TerminalActivation, shell providers, TerminalManager
11+
3. create ProjectCreators
12+
4. `setPythonApi()` — API object created, deferred resolved (API is now available to consumers)
13+
5. create views (EnvManagerView, ProjectView)
14+
6. register all commands
15+
7. activate() returns — extension is "active" from VS Code's perspective
1416

1517
📊 TELEMETRY: EXTENSION.ACTIVATION_DURATION { duration }
1618

17-
ASYNC (setImmediate callback, still in extension.ts):
19+
**ASYNC (setImmediate callback, still in extension.ts):**
1820
1. spawn PET process (`createNativePythonFinder`)
1921
1. sets up a JSON-RPC connection to it over stdin/stdout
2022
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)
28-
29-
--- gate point: `applyInitialEnvironmentSelection` ---
30-
📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount }
31-
32-
1. for each workspace folder + global scope (no workspace case), run `resolvePriorityChainCore` to find manager:
33-
- P1: pythonProjects[] setting → specific manager for this project
34-
- P2: user-configured defaultEnvManager setting
35-
- P3: user-configured python.defaultInterpreterPath → nativeFinder.resolve(path)
36-
- P4: auto-discovery → try venv manager (local .venv), fall back to system python
37-
- for workspace scope: ask venv manager if there's a local env (.venv/venv in the folder)
38-
- if found → use venv manager with that env
39-
- if not found → fall back to system python manager
40-
- for global scope: use system python manager directly
41-
42-
2. get the environment from the winning priority level:
43-
44-
--- fork point: `result.environment ?? await result.manager.get(folder.uri)` ---
45-
left side truthy = envPreResolved | left side undefined = managerDiscovery
46-
47-
envPreResolved — P3 won (interpreter → manager):
48-
`resolvePriorityChainCore` calls `tryResolveInterpreterPath()`:
49-
1. `nativeFinder.resolve(path)` — single PET call, resolves just this one binary
50-
2. find which manager owns the resolved env (by managerId)
51-
3. return { manager, environment } — BOTH are known
52-
→ result.environment is set → the `??` short-circuits
53-
→ no `manager.get()` called, no `initialize()`, no full discovery
54-
55-
managerDiscovery — P1, P2, or P4 won (manager → interpreter):
56-
`resolvePriorityChainCore` returns { manager, environment: undefined }
57-
→ result.environment is undefined → falls through to `await result.manager.get(scope)`
58-
`manager.get(scope)` (e.g. `CondaEnvManager.get()`):
59-
4. `initialize()` — lazy, once-only per manager (guarded by deferred)
60-
a. `nativeFinder.refresh(hardRefresh=false)`:
61-
`handleSoftRefresh()` checks in-memory cache (Map) for key 'all' (bc one big scan, shared cache, all managers benefit)
62-
- on reload: cache is empty (Map was destroyed) → cache miss
63-
- falls through to `handleHardRefresh()`
64-
`handleHardRefresh()`:
65-
- adds request to WorkerPool queue (concurrency 1, so serialized)
66-
- when its turn comes, calls `doRefresh()`:
67-
1. `configure()` — JSON-RPC to PET with search paths, conda/poetry/pipenv paths, cache dir
68-
2. `refresh` — JSON-RPC to PET, PET scans filesystem
69-
- PET may use its own on-disk cache (cacheDirectory) to speed this up
70-
- PET streams back results as 'environment' and 'manager' notifications
71-
- envs missing version/prefix get an inline resolve() call
72-
3. returns NativeInfo[] (all envs of all types)
73-
- result stored in in-memory cache under key 'all'
74-
→ 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
78-
→ matches path against freshly discovered collection via `findEnvironmentByPath()`
79-
→ populates `fsPathToEnv` map
80-
5. look up scope in `fsPathToEnv` → return the matched env
81-
82-
📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection }
83-
84-
3. env is cached in memory (no settings.json write)
85-
4. Python extension / status bar can now get the selected env via `api.getEnvironment(scope)`
86-
87-
📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? }
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+
31+
32+
**--- gate point: `applyInitialEnvironmentSelection` ---**
33+
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+
---
8893

8994
POST-INIT:
9095
1. register terminal package watcher
9196
2. register settings change listener (`registerInterpreterSettingsChangeListener`) — re-runs priority chain if settings change
9297
3. initialize terminal manager
93-
4. send telemetry (manager selection, project structure, discovery summary)
98+
4. send telemetry (manager selection, project structure, discovery summary)

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 {

0 commit comments

Comments
 (0)