jupyterlab_nb_venv_kernels_ui_extension 1.2.21 → 1.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +46 -16
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_nb_venv_kernels_ui_extension.spec.ts +101 -5
- package/src/index.ts +52 -16
package/lib/index.js
CHANGED
|
@@ -131,16 +131,41 @@ async function fetchVenvEnvironments() {
|
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve an nb_venv_kernels environment path to an absolute path.
|
|
136
|
+
*
|
|
137
|
+
* nb_venv_kernels reports `env.path` relative to its `workspace_root` (e.g.
|
|
138
|
+
* `delaval/.../datascience/.venv`); the kernelspec `argv[0]` we compare it
|
|
139
|
+
* against is absolute. Join them so the prefix check works.
|
|
140
|
+
*
|
|
141
|
+
* @param envPath - The `path` field from an nb_venv_kernels environment
|
|
142
|
+
* @param workspaceRoot - The `workspace_root` field from the environments response
|
|
143
|
+
* @returns The absolute path, or null if it cannot be resolved
|
|
144
|
+
*/
|
|
145
|
+
function absoluteEnvPath(envPath, workspaceRoot) {
|
|
146
|
+
if (!envPath) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (envPath.startsWith('/')) {
|
|
150
|
+
return envPath.replace(/\/+$/, '');
|
|
151
|
+
}
|
|
152
|
+
if (!workspaceRoot || !workspaceRoot.startsWith('/')) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return (workspaceRoot.replace(/\/+$/, '') + '/' + envPath.replace(/^\/+|\/+$/g, ''));
|
|
156
|
+
}
|
|
134
157
|
/**
|
|
135
158
|
* Find a venv environment matching the given display name.
|
|
136
159
|
*
|
|
137
|
-
* When `executablePath` is provided
|
|
138
|
-
* matching: the environment whose
|
|
139
|
-
*
|
|
140
|
-
* bug where two envs share a name
|
|
160
|
+
* When `executablePath` is provided as an absolute path, performs
|
|
161
|
+
* deterministic path-based matching: the environment whose (resolved,
|
|
162
|
+
* absolute) `path` is a prefix of the kernel's Python executable wins. This
|
|
163
|
+
* avoids the substring-collision class of bug where two envs share a name
|
|
164
|
+
* prefix (e.g. `demo` vs `demo-prod`).
|
|
141
165
|
*
|
|
142
|
-
* Falls back to substring matching on env names
|
|
143
|
-
*
|
|
166
|
+
* Falls back to substring matching on env names when no executable path is
|
|
167
|
+
* available, or when none of the env paths could be resolved to absolute
|
|
168
|
+
* (old nb_venv_kernels without `workspace_root`).
|
|
144
169
|
*
|
|
145
170
|
* @param displayName - The kernel display name (used for fallback match)
|
|
146
171
|
* @param executablePath - The kernel's `argv[0]` python path, if known
|
|
@@ -151,26 +176,31 @@ async function findVenvEnvironment(displayName, executablePath) {
|
|
|
151
176
|
if (!envData) {
|
|
152
177
|
return null;
|
|
153
178
|
}
|
|
154
|
-
// Path-based match (preferred) - exact prefix on env
|
|
155
|
-
// ambiguity between envs with overlapping names. If we have an
|
|
156
|
-
// executable path
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
179
|
+
// Path-based match (preferred) - exact prefix on the env's absolute path
|
|
180
|
+
// eliminates ambiguity between envs with overlapping names. If we have an
|
|
181
|
+
// absolute executable path AND at least one env path resolved, we trust
|
|
182
|
+
// the result: a non-match is then definitive (kernel not registered with
|
|
183
|
+
// nb_venv_kernels), so we do NOT fall back to substring matching - that
|
|
184
|
+
// would re-introduce the very collision bug this function prevents.
|
|
160
185
|
if (executablePath && executablePath.startsWith('/')) {
|
|
186
|
+
let attemptedPathMatch = false;
|
|
161
187
|
for (const env of envData.environments) {
|
|
162
188
|
if (env.type === 'conda') {
|
|
163
189
|
continue;
|
|
164
190
|
}
|
|
165
|
-
|
|
191
|
+
const envAbs = absoluteEnvPath(env.path, envData.workspace_root);
|
|
192
|
+
if (!envAbs) {
|
|
166
193
|
continue;
|
|
167
194
|
}
|
|
168
|
-
|
|
169
|
-
if (executablePath.startsWith(
|
|
195
|
+
attemptedPathMatch = true;
|
|
196
|
+
if (executablePath.startsWith(envAbs + '/')) {
|
|
170
197
|
return env;
|
|
171
198
|
}
|
|
172
199
|
}
|
|
173
|
-
|
|
200
|
+
if (attemptedPathMatch) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
// No env path could be resolved to absolute - fall through to substring.
|
|
174
204
|
}
|
|
175
205
|
// Fallback: substring match on env names, used when executablePath is
|
|
176
206
|
// missing or relative (e.g. nb_venv_kernels did not rewrite argv[0] for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_nb_venv_kernels_ui_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.22",
|
|
4
4
|
"description": "Jupyterlab extension to allow user to right-click on the kernel launcher button and select 'Show in File Browser' or 'Open Terminal at location' menus and get them to navigate to location or open location in terminal respectively",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -172,25 +172,48 @@ interface IVenvEnvironment {
|
|
|
172
172
|
path: string;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function absoluteEnvPath(
|
|
176
|
+
envPath: string,
|
|
177
|
+
workspaceRoot: string | undefined
|
|
178
|
+
): string | null {
|
|
179
|
+
if (!envPath) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (envPath.startsWith('/')) {
|
|
183
|
+
return envPath.replace(/\/+$/, '');
|
|
184
|
+
}
|
|
185
|
+
if (!workspaceRoot || !workspaceRoot.startsWith('/')) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return (
|
|
189
|
+
workspaceRoot.replace(/\/+$/, '') + '/' + envPath.replace(/^\/+|\/+$/g, '')
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
175
193
|
function findVenvEnvironmentMatch(
|
|
176
194
|
environments: IVenvEnvironment[],
|
|
177
195
|
displayName: string,
|
|
178
|
-
executablePath?: string | null
|
|
196
|
+
executablePath?: string | null,
|
|
197
|
+
workspaceRoot?: string
|
|
179
198
|
): IVenvEnvironment | null {
|
|
180
199
|
if (executablePath && executablePath.startsWith('/')) {
|
|
200
|
+
let attemptedPathMatch = false;
|
|
181
201
|
for (const env of environments) {
|
|
182
202
|
if (env.type === 'conda') {
|
|
183
203
|
continue;
|
|
184
204
|
}
|
|
185
|
-
|
|
205
|
+
const envAbs = absoluteEnvPath(env.path, workspaceRoot);
|
|
206
|
+
if (!envAbs) {
|
|
186
207
|
continue;
|
|
187
208
|
}
|
|
188
|
-
|
|
189
|
-
if (executablePath.startsWith(
|
|
209
|
+
attemptedPathMatch = true;
|
|
210
|
+
if (executablePath.startsWith(envAbs + '/')) {
|
|
190
211
|
return env;
|
|
191
212
|
}
|
|
192
213
|
}
|
|
193
|
-
|
|
214
|
+
if (attemptedPathMatch) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
const candidates = environments
|
|
@@ -338,6 +361,79 @@ describe('findVenvEnvironment matching', () => {
|
|
|
338
361
|
);
|
|
339
362
|
expect(match).toBeNull();
|
|
340
363
|
});
|
|
364
|
+
|
|
365
|
+
// Regression: nb_venv_kernels reports env.path relative to workspace_root,
|
|
366
|
+
// but the kernelspec argv[0] (executablePath) is absolute. Earlier the
|
|
367
|
+
// prefix check compared an absolute path against a relative one and never
|
|
368
|
+
// matched - every uv kernel showed "is not managed by nb_venv_kernels".
|
|
369
|
+
it('matches when env.path is relative to workspace_root', () => {
|
|
370
|
+
const envs: IVenvEnvironment[] = [
|
|
371
|
+
{
|
|
372
|
+
name: 'cp-kpi',
|
|
373
|
+
custom_name: 'cp-kpi',
|
|
374
|
+
type: 'uv',
|
|
375
|
+
exists: true,
|
|
376
|
+
has_kernel: true,
|
|
377
|
+
path: 'delaval/cp/graph-engine/datascience/.venv'
|
|
378
|
+
}
|
|
379
|
+
];
|
|
380
|
+
const match = findVenvEnvironmentMatch(
|
|
381
|
+
envs,
|
|
382
|
+
'Python [uv env:cp-kpi]',
|
|
383
|
+
'/home/lab/workspace/delaval/cp/graph-engine/datascience/.venv/bin/python',
|
|
384
|
+
'/home/lab/workspace'
|
|
385
|
+
);
|
|
386
|
+
expect(match?.name).toBe('cp-kpi');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('disambiguates relative env paths sharing a prefix', () => {
|
|
390
|
+
const envs: IVenvEnvironment[] = [
|
|
391
|
+
{
|
|
392
|
+
name: 'demo',
|
|
393
|
+
custom_name: 'demo',
|
|
394
|
+
type: 'uv',
|
|
395
|
+
exists: true,
|
|
396
|
+
has_kernel: true,
|
|
397
|
+
path: 'projects/demo/.venv'
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'demo-prod',
|
|
401
|
+
custom_name: 'demo-prod',
|
|
402
|
+
type: 'uv',
|
|
403
|
+
exists: true,
|
|
404
|
+
has_kernel: true,
|
|
405
|
+
path: 'projects/demo-prod/.venv'
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
const match = findVenvEnvironmentMatch(
|
|
409
|
+
envs,
|
|
410
|
+
'Python [uv env:demo-prod]',
|
|
411
|
+
'/home/lab/workspace/projects/demo-prod/.venv/bin/python',
|
|
412
|
+
'/home/lab/workspace'
|
|
413
|
+
);
|
|
414
|
+
expect(match?.name).toBe('demo-prod');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('falls back to substring when env paths are relative and workspace_root is missing', () => {
|
|
418
|
+
const envs: IVenvEnvironment[] = [
|
|
419
|
+
{
|
|
420
|
+
name: 'cp-kpi',
|
|
421
|
+
custom_name: 'cp-kpi',
|
|
422
|
+
type: 'uv',
|
|
423
|
+
exists: true,
|
|
424
|
+
has_kernel: true,
|
|
425
|
+
path: 'delaval/cp/datascience/.venv'
|
|
426
|
+
}
|
|
427
|
+
];
|
|
428
|
+
// old nb_venv_kernels: no workspace_root -> path-match can't be attempted
|
|
429
|
+
const match = findVenvEnvironmentMatch(
|
|
430
|
+
envs,
|
|
431
|
+
'Python [uv env:cp-kpi]',
|
|
432
|
+
'/home/lab/workspace/delaval/cp/datascience/.venv/bin/python',
|
|
433
|
+
undefined
|
|
434
|
+
);
|
|
435
|
+
expect(match?.name).toBe('cp-kpi');
|
|
436
|
+
});
|
|
341
437
|
});
|
|
342
438
|
|
|
343
439
|
describe('context menu configuration', () => {
|
package/src/index.ts
CHANGED
|
@@ -200,16 +200,47 @@ async function fetchVenvEnvironments(): Promise<IVenvEnvironmentsResponse | null
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Resolve an nb_venv_kernels environment path to an absolute path.
|
|
205
|
+
*
|
|
206
|
+
* nb_venv_kernels reports `env.path` relative to its `workspace_root` (e.g.
|
|
207
|
+
* `delaval/.../datascience/.venv`); the kernelspec `argv[0]` we compare it
|
|
208
|
+
* against is absolute. Join them so the prefix check works.
|
|
209
|
+
*
|
|
210
|
+
* @param envPath - The `path` field from an nb_venv_kernels environment
|
|
211
|
+
* @param workspaceRoot - The `workspace_root` field from the environments response
|
|
212
|
+
* @returns The absolute path, or null if it cannot be resolved
|
|
213
|
+
*/
|
|
214
|
+
function absoluteEnvPath(
|
|
215
|
+
envPath: string,
|
|
216
|
+
workspaceRoot: string | undefined
|
|
217
|
+
): string | null {
|
|
218
|
+
if (!envPath) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (envPath.startsWith('/')) {
|
|
222
|
+
return envPath.replace(/\/+$/, '');
|
|
223
|
+
}
|
|
224
|
+
if (!workspaceRoot || !workspaceRoot.startsWith('/')) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return (
|
|
228
|
+
workspaceRoot.replace(/\/+$/, '') + '/' + envPath.replace(/^\/+|\/+$/g, '')
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
203
232
|
/**
|
|
204
233
|
* Find a venv environment matching the given display name.
|
|
205
234
|
*
|
|
206
|
-
* When `executablePath` is provided
|
|
207
|
-
* matching: the environment whose
|
|
208
|
-
*
|
|
209
|
-
* bug where two envs share a name
|
|
235
|
+
* When `executablePath` is provided as an absolute path, performs
|
|
236
|
+
* deterministic path-based matching: the environment whose (resolved,
|
|
237
|
+
* absolute) `path` is a prefix of the kernel's Python executable wins. This
|
|
238
|
+
* avoids the substring-collision class of bug where two envs share a name
|
|
239
|
+
* prefix (e.g. `demo` vs `demo-prod`).
|
|
210
240
|
*
|
|
211
|
-
* Falls back to substring matching on env names
|
|
212
|
-
*
|
|
241
|
+
* Falls back to substring matching on env names when no executable path is
|
|
242
|
+
* available, or when none of the env paths could be resolved to absolute
|
|
243
|
+
* (old nb_venv_kernels without `workspace_root`).
|
|
213
244
|
*
|
|
214
245
|
* @param displayName - The kernel display name (used for fallback match)
|
|
215
246
|
* @param executablePath - The kernel's `argv[0]` python path, if known
|
|
@@ -224,26 +255,31 @@ async function findVenvEnvironment(
|
|
|
224
255
|
return null;
|
|
225
256
|
}
|
|
226
257
|
|
|
227
|
-
// Path-based match (preferred) - exact prefix on env
|
|
228
|
-
// ambiguity between envs with overlapping names. If we have an
|
|
229
|
-
// executable path
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
258
|
+
// Path-based match (preferred) - exact prefix on the env's absolute path
|
|
259
|
+
// eliminates ambiguity between envs with overlapping names. If we have an
|
|
260
|
+
// absolute executable path AND at least one env path resolved, we trust
|
|
261
|
+
// the result: a non-match is then definitive (kernel not registered with
|
|
262
|
+
// nb_venv_kernels), so we do NOT fall back to substring matching - that
|
|
263
|
+
// would re-introduce the very collision bug this function prevents.
|
|
233
264
|
if (executablePath && executablePath.startsWith('/')) {
|
|
265
|
+
let attemptedPathMatch = false;
|
|
234
266
|
for (const env of envData.environments) {
|
|
235
267
|
if (env.type === 'conda') {
|
|
236
268
|
continue;
|
|
237
269
|
}
|
|
238
|
-
|
|
270
|
+
const envAbs = absoluteEnvPath(env.path, envData.workspace_root);
|
|
271
|
+
if (!envAbs) {
|
|
239
272
|
continue;
|
|
240
273
|
}
|
|
241
|
-
|
|
242
|
-
if (executablePath.startsWith(
|
|
274
|
+
attemptedPathMatch = true;
|
|
275
|
+
if (executablePath.startsWith(envAbs + '/')) {
|
|
243
276
|
return env;
|
|
244
277
|
}
|
|
245
278
|
}
|
|
246
|
-
|
|
279
|
+
if (attemptedPathMatch) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
// No env path could be resolved to absolute - fall through to substring.
|
|
247
283
|
}
|
|
248
284
|
|
|
249
285
|
// Fallback: substring match on env names, used when executablePath is
|