Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
# VS Code Hatch
An extension to manage Hatch environments through [`vscode-python-environments`][].
# Hatch Code
An extension to manage [Hatch environments] through [`vscode-python-environments`][].

To make use of it, make sure your user settings contain `"python.useEnvironmentsExtension": true`.

[hatch environments]: https://hatch.pypa.io/latest/tutorials/environment/basic-usage/
[`vscode-python-environments`]: https://github.com/microsoft/vscode-python-environments/#readme

## Features
- List all configured Hatch envs and allow to activate them
- List all configured [Hatch environments]
- Provide controls to set them as active environment for your project, activate them in a terminal, and remove them[^1]
- Temporarily[^2] modify an environment’s packages using the configured [`installer`]

[^1]: “remove” in this context means deleting it on disk, it will stay listed and will be recreated when interacting with it.
[^2]: since many actions currently use `hatch run` and therefore sync the environment, e.g. removing a package that is pulled in as a dependency will not persist for long.

[`installer`]: https://hatch.pypa.io/latest/how-to/environment/select-installer/

## Extension Settings
TODO

## Limitations
- It’s pretty unclear which environments exist on disk and which don’t
- We list internal envs that users don’t usually interact with, such as `hatch-uv` and `hatch-build`
- [Terminal activation is slow](https://github.com/microsoft/vscode-python-environments/issues/1391)

## Release Notes
### 0.0.1
Unreleased
81 changes: 81 additions & 0 deletions src/cli/hatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { run } from './index.js'

export interface HatchEnvInfo {
name: string
path: string
conf: HatchEnvConf
projectPath: string
}

export interface HatchEnvConf {
installer: 'uv' | 'pip'
type: 'virtual'
dependencies?: string[]
'extra-dependencies'?: string[]
scripts?: { [name: string]: string[] }
'env-vars'?: { [name: string]: string }
'default-args'?: string[]
features?: string[]
python?: string
'skip-install'?: boolean
'pre-install-commands'?: string[]
'post-install-commands'?: string[]
platforms?: ('windows' | 'linux' | 'macos')[]
description?: string
}

export async function getEnvs(projectPath: string): Promise<HatchEnvInfo[]> {
const json = await run('hatch', ['env', 'show', '--json'], {
cwd: projectPath,
})
const envs = JSON.parse(json) as { [name: string]: HatchEnvConf }
return await Promise.all(
Object.entries(envs).map(async ([name, conf]) => ({
name,
conf,
path: await findEnv(name, projectPath),
projectPath,
})),
)
}

export async function findEnv(
name: string,
projectPath: string,
): Promise<string> {
const results = await run('hatch', ['env', 'find', name], {
cwd: projectPath,
})
const [p] = results
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
return p
}

interface CreateEnvOptions {
mode?: 'create' | 'sync' | 'ensure'
}

export async function createEnv(
name: string,
projectPath: string,
{ mode = 'create' }: CreateEnvOptions = {},
): Promise<void> {
const args =
mode === 'sync'
? ['-e', name, 'run', 'python', '-V']
: ['env', 'create', name]
if (mode === 'ensure')
try {
await run('hatch', args, { cwd: projectPath })
} catch (_) {}
await run('hatch', args, { cwd: projectPath })
}

export async function removeEnv(
name: string,
projectPath: string,
): Promise<void> {
await run('hatch', ['env', 'remove', name], { cwd: projectPath })
}
26 changes: 26 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
type ExecFileException,
execFile as execFileCb,
type ProcessEnvOptions,
} from 'node:child_process'
import paths from 'node:path'
import { promisify } from 'node:util'
import { traceError } from '../common/logging.js'
import { isWindows } from '../common/platform.js'

const execFile = promisify(execFileCb)

export async function run(
cmd: string,
args: string[],
opts: ProcessEnvOptions = {},
): Promise<string> {
try {
const { stdout } = await execFile(cmd, args, opts)
return stdout
} catch (e) {
const err = e as ExecFileException
traceError(err, err.stderr)
throw err
}
}
36 changes: 36 additions & 0 deletions src/cli/installer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { HatchEnvInfo } from './hatch.js'
import { run } from './index.js'

async function runPipOrUv(env: HatchEnvInfo, args: string[]): Promise<string> {
const args_ =
env.conf.installer === 'uv'
? ['uv', 'pip', ...args]
: ['pip', ...args, ...(args[0] === 'uninstall' ? ['--yes'] : [])]

return run('hatch', ['-e', env.name, 'run', ...args_], {
cwd: env.projectPath,
})
}

export async function listPackages(
env: HatchEnvInfo,
): Promise<{ name: string; version: string }[]> {
const json = await runPipOrUv(env, ['list', '--format=json'])
return JSON.parse(json) as { name: string; version: string }[]
}

export async function installPackages(
env: HatchEnvInfo,
packages: string[],
{ upgrade = false }: { upgrade?: boolean } = {},
): Promise<void> {
const args = [...(upgrade ? ['--upgrade'] : []), ...packages]
await runPipOrUv(env, ['install', ...args])
}

export async function uninstallPackages(
env: HatchEnvInfo,
packages: string[],
): Promise<void> {
await runPipOrUv(env, ['uninstall', ...packages])
}
7 changes: 6 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type ExtensionContext, window } from 'vscode'
import { registerLogger } from './common/logging.js'
import { setWorkspacePersistentState } from './common/persistent-state.js'
import { HatchEnvManager } from './hatch-env-manager.js'
import { HatchPackageManager } from './hatch-pkg-manager.js'
import { getEnvExtApi } from './python-envs-api.js'

export async function activate(context: ExtensionContext) {
Expand All @@ -12,7 +13,11 @@ export async function activate(context: ExtensionContext) {
const api = await getEnvExtApi()
await setWorkspacePersistentState(context) // resolves instantly
const envManager = new HatchEnvManager(api, log)
context.subscriptions.push(api.registerEnvironmentManager(envManager))
const pkgManager = new HatchPackageManager(api, log)
context.subscriptions.push(
api.registerEnvironmentManager(envManager),
api.registerPackageManager(pkgManager),
)
}

export function deactivate() {}
84 changes: 0 additions & 84 deletions src/hatch-cli.ts

This file was deleted.

Loading