From ccdc15634bd0e895a3f2e77f5e56256511bda90f Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 11:40:26 -0400 Subject: [PATCH 01/12] feat: add command to get env interpreter path --- package.json | 7 +++++++ src/commands.ts | 22 ++++++++++++++++++++++ src/common/constants.ts | 2 ++ src/extension.ts | 10 ++++++++-- test/extension.test.ts | 22 +++++++++++++++++++++- 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/commands.ts diff --git a/package.json b/package.json index 5ba3c28..b7e413a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,13 @@ } } }, + "commands": [ + { + "command": "hatch.envInterpreter", + "title": "Hatch: Get an environment’s interpreter path", + "enablement": false + } + ], "icons": { "hatch-logo": { "description": "Hatch Logo", diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..766ddda --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,22 @@ +import { Uri, workspace } from 'vscode' +import type { HatchEnvManager } from './hatch-env-manager.js' + +/** Command to get the interpreter for a given environment. + * + * Intended to be used via [variable substitution] like `"${command:hatch.envInterpreter?[\"hatch-test.py3.14\"]}"`. + * Modeled after [`python.interpreterPath`]. + * + * [variable substitution]: https://code.visualstudio.com/docs/debugtest/tasks#_variable-substitution + * [`python.interpreterPath`]: https://github.com/microsoft/vscode-python/blob/9ded8032f6a455289113026ed1dca4c5ed81e6e8/src/client/interpreter/interpreterPathCommand.ts + */ +export async function getEnvInterpreter( + envManager: HatchEnvManager, + env: string, + workspaceDir: string | undefined, +): Promise { + const workspaceUri = workspaceDir + ? Uri.file(workspaceDir) + : workspace.workspaceFolders?.[0]?.uri + const envs = await envManager.getEnvironments(workspaceUri ?? 'all') + return envs.find((e) => e.name === env)?.execInfo.run.executable ?? 'python' +} diff --git a/src/common/constants.ts b/src/common/constants.ts index 1ab58ed..3de1c72 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -8,3 +8,5 @@ export const EXE_CONFIG_SECTION = 'hatch' // dotted section name export const EXE_CONFIG_SETTING = 'executable' // last element export const ENVS_EXT_ID = 'ms-python.vscode-python-envs' + +export const CMD_ENV_INTERPRETER = 'hatch.envInterpreter' diff --git a/src/extension.ts b/src/extension.ts index cc866cb..9acc396 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ -import { type ExtensionContext, window } from 'vscode' +import { commands, type ExtensionContext, window } from 'vscode' import { HatchExecutableTracker } from './cli/index.js' -import { EXTENSION_ID } from './common/constants.js' +import { getEnvInterpreter } from './commands.js' +import { CMD_ENV_INTERPRETER, EXTENSION_ID } from './common/constants.js' import { registerLogger } from './common/logging.js' import { setWorkspacePersistentState } from './common/persistent-state.js' import { HatchEnvManager } from './hatch-env-manager.js' @@ -28,6 +29,11 @@ export async function activate(context: ExtensionContext): Promise { extensionId: EXTENSION_ID, }), api.registerPackageManager(pkgManager, { extensionId: EXTENSION_ID }), + commands.registerCommand( + CMD_ENV_INTERPRETER, + (env: string, wsDir?: string) => + getEnvInterpreter(envManager, env, wsDir), + ), ) return { diff --git a/test/extension.test.ts b/test/extension.test.ts index c256ca8..37e3007 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -5,7 +5,11 @@ import type { } from '@vscode/python-environments' import { before, beforeEach } from 'mocha' import * as vscode from 'vscode' -import { ENVS_EXT_ID, EXTENSION_ID } from '../src/common/constants' +import { + CMD_ENV_INTERPRETER, + ENVS_EXT_ID, + EXTENSION_ID, +} from '../src/common/constants' import type * as extension from '../src/extension' import MockExec from './mock-exec' import { tmpdir, waitForCondition } from './test-utils' @@ -84,4 +88,20 @@ describe('Env Manager', () => { assert.equal(envs[0].name, 'mockenv') assert.equal(envs[0].sysPrefix, 'mockpath') }) + + it('should implement an env interpreter path command', async () => { + await using dir = await tmpdir('hatch-') + api.addPythonProject({ name: 'test', uri: dir.uri }) + + exec.reset( + [['env', 'show', '--json'], { mockenv: { type: 'virtual' } }], + [['env', 'find', 'mockenv'], 'mockpath\n'], + ) + + const intp = await vscode.commands.executeCommand( + CMD_ENV_INTERPRETER, + 'mockenv', + ) + assert.equal(intp, 'mockpath/bin/python') + }) }) From c85f311a9622b9d27a3b76454cbf80b071778f3f Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:13:18 -0400 Subject: [PATCH 02/12] readme --- README.md | 25 +++++++++++++++++++++++++ src/commands.ts | 4 ++-- src/extension.ts | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e50d90..daded18 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ To make use of it, make sure your user settings contain `"python.useEnvironments - List all configured [Hatch environments] - Provide controls to set them as active environment for your project, activate them in a terminal, and delete them from disk - Temporarily modify an environment’s packages using the configured [`installer`] +- Define a `hatch.envInterpreter` command for use in `launch.json` or `tasks.json`, see [below](#commands) ![screenshot](./screenshot.png) @@ -18,6 +19,30 @@ Persistent modifications to the installed packages should be done by editing Hat [`installer`]: https://hatch.pypa.io/latest/how-to/environment/select-installer/ +## Commands +- `hatch.envInterpreter`: not an interactive command, but rather for use in `launch.json` or `tasks.json` via [variable substitution], e.g. for `program` in `tasks.json` or `python` in `launch.json`: + + ```jsonc + { // tasks.json + "version": "0.2.0", + "tasks": [ + { + "type": "process", + "program": "${command:hatch.envInterpreter?[\"docs\"]}"`, + "args": ["-m", "sphinx", "docs", "docs/_build"], + }, + ], + } + ``` + + When called without arguments, it returns the path to the `default` hatch environment of the currently open workspace. + The first parameter is the environment name as reported by Hatch. + The second parameter is an explicit workspace folder in case you have multiple workspaces open. + + (The syntax for specifying arguments in command Uris is an URL-encoded JSON array that has to be embedded in the `launch.json`/`tasks.json`. Quite cumbersome, but shouldn’t be an issue for common env names.) + +[variable substitution]: https://code.visualstudio.com/docs/editor/userdefinedsnippets + ## Extension Settings - `hatch.executable`: path to the `hatch` executable (supports `~` expansion). Defaults to the output of `which hatch`. diff --git a/src/commands.ts b/src/commands.ts index 766ddda..e927c05 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -11,8 +11,8 @@ import type { HatchEnvManager } from './hatch-env-manager.js' */ export async function getEnvInterpreter( envManager: HatchEnvManager, - env: string, - workspaceDir: string | undefined, + env: string | undefined = 'default', + workspaceDir?: string | undefined, ): Promise { const workspaceUri = workspaceDir ? Uri.file(workspaceDir) diff --git a/src/extension.ts b/src/extension.ts index 9acc396..66bfd16 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -31,7 +31,7 @@ export async function activate(context: ExtensionContext): Promise { api.registerPackageManager(pkgManager, { extensionId: EXTENSION_ID }), commands.registerCommand( CMD_ENV_INTERPRETER, - (env: string, wsDir?: string) => + (env?: string, wsDir?: string) => getEnvInterpreter(envManager, env, wsDir), ), ) From a9c624c1ecbfc6bcb6469718e9ca1614609aebbc Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:26:17 -0400 Subject: [PATCH 03/12] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index daded18..76557b8 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Persistent modifications to the installed packages should be done by editing Hat "tasks": [ { "type": "process", - "program": "${command:hatch.envInterpreter?[\"docs\"]}"`, + "program": "${command:hatch.envInterpreter?[\"docs\"]}", "args": ["-m", "sphinx", "docs", "docs/_build"], }, ], From 5457ebe653d5470ef2ece6777bce14d91e3a00fa Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:27:50 -0400 Subject: [PATCH 04/12] label --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 76557b8..45ab994 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Persistent modifications to the installed packages should be done by editing Hat "version": "0.2.0", "tasks": [ { + "label": "Build docs", "type": "process", "program": "${command:hatch.envInterpreter?[\"docs\"]}", "args": ["-m", "sphinx", "docs", "docs/_build"], From 4e0d92bccf4bd24130b80329e7c8e33621b76a39 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:36:06 -0400 Subject: [PATCH 05/12] reuse --- src/commands.ts | 1 + test/extension.test.ts | 18 ++++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index e927c05..1e1c855 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -17,6 +17,7 @@ export async function getEnvInterpreter( const workspaceUri = workspaceDir ? Uri.file(workspaceDir) : workspace.workspaceFolders?.[0]?.uri + await envManager.refresh(workspaceUri) const envs = await envManager.getEnvironments(workspaceUri ?? 'all') return envs.find((e) => e.name === env)?.execInfo.run.executable ?? 'python' } diff --git a/test/extension.test.ts b/test/extension.test.ts index 37e3007..9f735a0 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -73,14 +73,18 @@ describe('Env Manager', () => { envManager = ext.envManager }) - it('should return environments', async () => { + async function testProj(): ReturnType { await using dir = await tmpdir('hatch-') api.addPythonProject({ name: 'test', uri: dir.uri }) - exec.reset( [['env', 'show', '--json'], { mockenv: { type: 'virtual' } }], [['env', 'find', 'mockenv'], 'mockpath\n'], ) + return dir + } + + it('should return environments', async () => { + await using dir = await testProj() //This gets called automatically: await envManager.refresh(dir.uri) const envs = await envManager.getEnvironments(dir.uri) @@ -90,14 +94,8 @@ describe('Env Manager', () => { }) it('should implement an env interpreter path command', async () => { - await using dir = await tmpdir('hatch-') - api.addPythonProject({ name: 'test', uri: dir.uri }) - - exec.reset( - [['env', 'show', '--json'], { mockenv: { type: 'virtual' } }], - [['env', 'find', 'mockenv'], 'mockpath\n'], - ) - + await using _ = await testProj() + //This gets called automatically: await envManager.refresh(dir.uri) const intp = await vscode.commands.executeCommand( CMD_ENV_INTERPRETER, 'mockenv', From 944cadaf307347364dd57152ba7e60507637b642 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:37:02 -0400 Subject: [PATCH 06/12] whitespace --- .editorconfig | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index ecb0a87..731a625 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,6 @@ charset = utf-8 indent_style = tab indent_size = 4 -[*.{yml,yaml}] +[*.{yml,yaml,md}] indent_style = space indent_size = 2 diff --git a/README.md b/README.md index 45ab994..55bc6e9 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Persistent modifications to the installed packages should be done by editing Hat "version": "0.2.0", "tasks": [ { - "label": "Build docs", + "label": "Build docs", "type": "process", "program": "${command:hatch.envInterpreter?[\"docs\"]}", "args": ["-m", "sphinx", "docs", "docs/_build"], From e66ea034d63f310fd5677442dd9ba4613f3ed781 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:41:31 -0400 Subject: [PATCH 07/12] error instead --- src/commands.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 1e1c855..286846b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -11,13 +11,19 @@ import type { HatchEnvManager } from './hatch-env-manager.js' */ export async function getEnvInterpreter( envManager: HatchEnvManager, - env: string | undefined = 'default', + envName: string | undefined = 'default', workspaceDir?: string | undefined, ): Promise { const workspaceUri = workspaceDir ? Uri.file(workspaceDir) : workspace.workspaceFolders?.[0]?.uri + if (!workspaceUri) throw new Error('No workspace open') await envManager.refresh(workspaceUri) - const envs = await envManager.getEnvironments(workspaceUri ?? 'all') - return envs.find((e) => e.name === env)?.execInfo.run.executable ?? 'python' + const envs = await envManager.getEnvironments(workspaceUri) + const env = envs.find((e) => e.name === envName) + if (!env) + throw new Error( + `Environment “${envName}” not found in workspace “${workspaceUri.fsPath}”`, + ) + return env.execInfo.run.executable } From bc312c02948bdb707c66c18b4d97feeefda37595 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:44:16 -0400 Subject: [PATCH 08/12] fix disposable --- test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extension.test.ts b/test/extension.test.ts index 9f735a0..c772105 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -74,7 +74,7 @@ describe('Env Manager', () => { }) async function testProj(): ReturnType { - await using dir = await tmpdir('hatch-') + const dir = await tmpdir('hatch-') api.addPythonProject({ name: 'test', uri: dir.uri }) exec.reset( [['env', 'show', '--json'], { mockenv: { type: 'virtual' } }], From 54ebbc3379b3874e52da84985059c84c82d3e57d Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 12:59:18 -0400 Subject: [PATCH 09/12] fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 55bc6e9..c89cc56 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Persistent modifications to the installed packages should be done by editing Hat [`installer`]: https://hatch.pypa.io/latest/how-to/environment/select-installer/ ## Commands -- `hatch.envInterpreter`: not an interactive command, but rather for use in `launch.json` or `tasks.json` via [variable substitution], e.g. for `program` in `tasks.json` or `python` in `launch.json`: +- `hatch.envInterpreter`: not an interactive command, but rather for use in `launch.json` or `tasks.json` via [variable substitution], e.g. for `command` in `tasks.json` or `python` in `launch.json`: ```jsonc { // tasks.json @@ -29,7 +29,7 @@ Persistent modifications to the installed packages should be done by editing Hat { "label": "Build docs", "type": "process", - "program": "${command:hatch.envInterpreter?[\"docs\"]}", + "command": "${command:hatch.envInterpreter?[\"docs\"]}", "args": ["-m", "sphinx", "docs", "docs/_build"], }, ], From 2f7824eee2a1b2ad90f5c053b31f52e5a004e464 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 14:57:00 -0400 Subject: [PATCH 10/12] Use through `inputs` --- README.md | 21 +++++++++++++++------ src/commands.ts | 16 ++++++++++------ src/extension.ts | 8 +++----- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c89cc56..449b8e6 100644 --- a/README.md +++ b/README.md @@ -29,20 +29,29 @@ Persistent modifications to the installed packages should be done by editing Hat { "label": "Build docs", "type": "process", - "command": "${command:hatch.envInterpreter?[\"docs\"]}", + "command": "${input:docsInterpreter}", "args": ["-m", "sphinx", "docs", "docs/_build"], + "problemMatcher": [], + }, + ], + "inputs": [ + { + "id": "docsInterpreter", + "type": "command", + "command": "hatch.envInterpreter", + "args": { "env": "docs" }, }, ], } ``` - When called without arguments, it returns the path to the `default` hatch environment of the currently open workspace. - The first parameter is the environment name as reported by Hatch. - The second parameter is an explicit workspace folder in case you have multiple workspaces open. + The command supports the following `args`: + - `env`: name of the environment (defaults to `"default"`) + - `workspace`: path to the workspace root (defaults to the first currently open workspace) - (The syntax for specifying arguments in command Uris is an URL-encoded JSON array that has to be embedded in the `launch.json`/`tasks.json`. Quite cumbersome, but shouldn’t be an issue for common env names.) + It can be used without going through `inputs` using just `${command:hatch.envInterpreter}` to always use the `default` environment instead of the currently active one. -[variable substitution]: https://code.visualstudio.com/docs/editor/userdefinedsnippets +[variable substitution]: https://code.visualstudio.com/docs/reference/variables-reference ## Extension Settings - `hatch.executable`: path to the `hatch` executable (supports `~` expansion). Defaults to the output of `which hatch`. diff --git a/src/commands.ts b/src/commands.ts index 286846b..a562bf0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,21 +1,25 @@ import { Uri, workspace } from 'vscode' import type { HatchEnvManager } from './hatch-env-manager.js' +export interface CommandOptions { + /** environment name */ + env?: string | undefined + /** workspace directory */ + workspace?: string | undefined +} + /** Command to get the interpreter for a given environment. * - * Intended to be used via [variable substitution] like `"${command:hatch.envInterpreter?[\"hatch-test.py3.14\"]}"`. * Modeled after [`python.interpreterPath`]. * - * [variable substitution]: https://code.visualstudio.com/docs/debugtest/tasks#_variable-substitution * [`python.interpreterPath`]: https://github.com/microsoft/vscode-python/blob/9ded8032f6a455289113026ed1dca4c5ed81e6e8/src/client/interpreter/interpreterPathCommand.ts */ export async function getEnvInterpreter( envManager: HatchEnvManager, - envName: string | undefined = 'default', - workspaceDir?: string | undefined, + { env: envName = 'default', workspace: wsDir }: CommandOptions = {}, ): Promise { - const workspaceUri = workspaceDir - ? Uri.file(workspaceDir) + const workspaceUri = wsDir + ? Uri.file(wsDir) : workspace.workspaceFolders?.[0]?.uri if (!workspaceUri) throw new Error('No workspace open') await envManager.refresh(workspaceUri) diff --git a/src/extension.ts b/src/extension.ts index 66bfd16..b575f13 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,6 @@ import { commands, type ExtensionContext, window } from 'vscode' import { HatchExecutableTracker } from './cli/index.js' -import { getEnvInterpreter } from './commands.js' +import { type CommandOptions, getEnvInterpreter } from './commands.js' import { CMD_ENV_INTERPRETER, EXTENSION_ID } from './common/constants.js' import { registerLogger } from './common/logging.js' import { setWorkspacePersistentState } from './common/persistent-state.js' @@ -29,10 +29,8 @@ export async function activate(context: ExtensionContext): Promise { extensionId: EXTENSION_ID, }), api.registerPackageManager(pkgManager, { extensionId: EXTENSION_ID }), - commands.registerCommand( - CMD_ENV_INTERPRETER, - (env?: string, wsDir?: string) => - getEnvInterpreter(envManager, env, wsDir), + commands.registerCommand(CMD_ENV_INTERPRETER, (args?: CommandOptions) => + getEnvInterpreter(envManager, args), ), ) From e126dae766c6dd2a8739edce50395a72ca8e98bb Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 15:02:59 -0400 Subject: [PATCH 11/12] fix test --- test/extension.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/extension.test.ts b/test/extension.test.ts index c772105..b578b28 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -96,10 +96,9 @@ describe('Env Manager', () => { it('should implement an env interpreter path command', async () => { await using _ = await testProj() //This gets called automatically: await envManager.refresh(dir.uri) - const intp = await vscode.commands.executeCommand( - CMD_ENV_INTERPRETER, - 'mockenv', - ) + const intp = await vscode.commands.executeCommand(CMD_ENV_INTERPRETER, { + env: 'mockenv', + }) assert.equal(intp, 'mockpath/bin/python') }) }) From 75b5dae9c74f64d45f2bef8a6d24c733cc4d72a9 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 28 May 2026 15:04:31 -0400 Subject: [PATCH 12/12] types --- test/extension.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/extension.test.ts b/test/extension.test.ts index b578b28..1ee40f0 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -5,6 +5,7 @@ import type { } from '@vscode/python-environments' import { before, beforeEach } from 'mocha' import * as vscode from 'vscode' +import type { CommandOptions } from '../src/commands' import { CMD_ENV_INTERPRETER, ENVS_EXT_ID, @@ -98,7 +99,7 @@ describe('Env Manager', () => { //This gets called automatically: await envManager.refresh(dir.uri) const intp = await vscode.commands.executeCommand(CMD_ENV_INTERPRETER, { env: 'mockenv', - }) + } satisfies CommandOptions) assert.equal(intp, 'mockpath/bin/python') }) })