-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Hatch environments (microsoft/vscode-python#22779)
Fixes microsoft/vscode-python#22810 TODO - [x] check if it actually works already or if more things need to be registered - [x] add config val - [x] add tests
- Loading branch information
1 parent
7e725ad
commit c03cd44
Showing
17 changed files
with
353 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
...ions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
'use strict'; | ||
|
||
import { PythonEnvKind } from '../../info'; | ||
import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; | ||
import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; | ||
import { Hatch } from '../../../common/environmentManagers/hatch'; | ||
import { asyncFilter } from '../../../../common/utils/arrayUtils'; | ||
import { pathExists } from '../../../common/externalDependencies'; | ||
import { traceError, traceVerbose } from '../../../../logging'; | ||
import { chain, iterable } from '../../../../common/utils/async'; | ||
import { getInterpreterPathFromDir } from '../../../common/commonUtils'; | ||
|
||
/** | ||
* Gets all default virtual environment locations to look for in a workspace. | ||
*/ | ||
async function getVirtualEnvDirs(root: string): Promise<string[]> { | ||
const hatch = await Hatch.getHatch(root); | ||
const envDirs = (await hatch?.getEnvList()) ?? []; | ||
return asyncFilter(envDirs, pathExists); | ||
} | ||
|
||
/** | ||
* Finds and resolves virtual environments created using Hatch. | ||
*/ | ||
export class HatchLocator extends LazyResourceBasedLocator { | ||
public readonly providerId: string = 'hatch'; | ||
|
||
public constructor(private readonly root: string) { | ||
super(); | ||
} | ||
|
||
protected doIterEnvs(): IPythonEnvsIterator<BasicEnvInfo> { | ||
async function* iterator(root: string) { | ||
const envDirs = await getVirtualEnvDirs(root); | ||
const envGenerators = envDirs.map((envDir) => { | ||
async function* generator() { | ||
traceVerbose(`Searching for Hatch virtual envs in: ${envDir}`); | ||
const filename = await getInterpreterPathFromDir(envDir); | ||
if (filename !== undefined) { | ||
try { | ||
yield { executablePath: filename, kind: PythonEnvKind.Hatch }; | ||
traceVerbose(`Hatch Virtual Environment: [added] ${filename}`); | ||
} catch (ex) { | ||
traceError(`Failed to process environment: ${filename}`, ex); | ||
} | ||
} | ||
} | ||
return generator(); | ||
}); | ||
|
||
yield* iterable(chain(envGenerators)); | ||
traceVerbose(`Finished searching for Hatch envs`); | ||
} | ||
|
||
return iterator(this.root); | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/hatch.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { isTestExecution } from '../../../common/constants'; | ||
import { exec, pathExists } from '../externalDependencies'; | ||
import { traceVerbose } from '../../../logging'; | ||
import { cache } from '../../../common/utils/decorators'; | ||
|
||
/** Wraps the "Hatch" utility, and exposes its functionality. | ||
*/ | ||
export class Hatch { | ||
/** | ||
* Locating Hatch binary can be expensive, since it potentially involves spawning or | ||
* trying to spawn processes; so we only do it once per session. | ||
*/ | ||
private static hatchPromise: Map<string, Promise<Hatch | undefined>> = new Map< | ||
string, | ||
Promise<Hatch | undefined> | ||
>(); | ||
|
||
/** | ||
* Creates a Hatch service corresponding to the corresponding "hatch" command. | ||
* | ||
* @param command - Command used to run hatch. This has the same meaning as the | ||
* first argument of spawn() - i.e. it can be a full path, or just a binary name. | ||
* @param cwd - The working directory to use as cwd when running hatch. | ||
*/ | ||
constructor(public readonly command: string, private cwd: string) {} | ||
|
||
/** | ||
* Returns a Hatch instance corresponding to the binary which can be used to run commands for the cwd. | ||
* | ||
* Every directory is a valid Hatch project, so this should always return a Hatch instance. | ||
*/ | ||
public static async getHatch(cwd: string): Promise<Hatch | undefined> { | ||
if (Hatch.hatchPromise.get(cwd) === undefined || isTestExecution()) { | ||
Hatch.hatchPromise.set(cwd, Hatch.locate(cwd)); | ||
} | ||
return Hatch.hatchPromise.get(cwd); | ||
} | ||
|
||
private static async locate(cwd: string): Promise<Hatch | undefined> { | ||
// First thing this method awaits on should be hatch command execution, | ||
// hence perform all operations before that synchronously. | ||
const hatchPath = 'hatch'; | ||
traceVerbose(`Probing Hatch binary ${hatchPath}`); | ||
const hatch = new Hatch(hatchPath, cwd); | ||
const virtualenvs = await hatch.getEnvList(); | ||
if (virtualenvs !== undefined) { | ||
traceVerbose(`Found hatch binary ${hatchPath}`); | ||
return hatch; | ||
} | ||
traceVerbose(`Failed to find Hatch binary ${hatchPath}`); | ||
|
||
// Didn't find anything. | ||
traceVerbose(`No Hatch binary found`); | ||
return undefined; | ||
} | ||
|
||
/** | ||
* Retrieves list of Python environments known to Hatch for this working directory. | ||
* Returns `undefined` if we failed to spawn in some way. | ||
* | ||
* Corresponds to "hatch env show --json". Swallows errors if any. | ||
*/ | ||
public async getEnvList(): Promise<string[] | undefined> { | ||
return this.getEnvListCached(this.cwd); | ||
} | ||
|
||
/** | ||
* Method created to facilitate caching. The caching decorator uses function arguments as cache key, | ||
* so pass in cwd on which we need to cache. | ||
*/ | ||
@cache(30_000, true, 10_000) | ||
private async getEnvListCached(_cwd: string): Promise<string[] | undefined> { | ||
const envInfoOutput = await exec(this.command, ['env', 'show', '--json'], { | ||
cwd: this.cwd, | ||
throwOnStdErr: true, | ||
}).catch(traceVerbose); | ||
if (!envInfoOutput) { | ||
return undefined; | ||
} | ||
const envPaths = await Promise.all( | ||
Object.keys(JSON.parse(envInfoOutput.stdout)).map(async (name) => { | ||
const envPathOutput = await exec(this.command, ['env', 'find', name], { | ||
cwd: this.cwd, | ||
throwOnStdErr: true, | ||
}).catch(traceVerbose); | ||
if (!envPathOutput) return undefined; | ||
const dir = envPathOutput.stdout.trim(); | ||
return (await pathExists(dir)) ? dir : undefined; | ||
}), | ||
); | ||
return envPaths.flatMap((r) => (r ? [r] : [])); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
...itron-python/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
import * as sinon from 'sinon'; | ||
import * as path from 'path'; | ||
import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; | ||
import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; | ||
import * as platformUtils from '../../../../../client/common/utils/platform'; | ||
import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; | ||
import { HatchLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/hatchLocator'; | ||
import { assertBasicEnvsEqual } from '../envTestUtils'; | ||
import { createBasicEnv } from '../../common'; | ||
import { makeExecHandler, projectDirs, venvDirs } from '../../../common/environmentManagers/hatch.unit.test'; | ||
|
||
suite('Hatch Locator', () => { | ||
let exec: sinon.SinonStub; | ||
let getPythonSetting: sinon.SinonStub; | ||
let getOSType: sinon.SinonStub; | ||
let locator: HatchLocator; | ||
|
||
suiteSetup(() => { | ||
getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); | ||
getPythonSetting.returns('hatch'); | ||
getOSType = sinon.stub(platformUtils, 'getOSType'); | ||
exec = sinon.stub(externalDependencies, 'exec'); | ||
}); | ||
|
||
suiteTeardown(() => sinon.restore()); | ||
|
||
suite('iterEnvs()', () => { | ||
setup(() => { | ||
getOSType.returns(platformUtils.OSType.Linux); | ||
}); | ||
|
||
interface TestArgs { | ||
osType?: platformUtils.OSType; | ||
pythonBin?: string; | ||
} | ||
|
||
const testProj1 = async ({ osType, pythonBin = 'bin/python' }: TestArgs = {}) => { | ||
if (osType) { | ||
getOSType.returns(osType); | ||
} | ||
|
||
locator = new HatchLocator(projectDirs.project1); | ||
exec.callsFake(makeExecHandler(venvDirs.project1, { path: true, cwd: projectDirs.project1 })); | ||
|
||
const iterator = locator.iterEnvs(); | ||
const actualEnvs = await getEnvs(iterator); | ||
|
||
const expectedEnvs = [createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, pythonBin))]; | ||
assertBasicEnvsEqual(actualEnvs, expectedEnvs); | ||
}; | ||
|
||
test('project with only the default env', () => testProj1()); | ||
test('project with only the default env on Windows', () => | ||
testProj1({ | ||
osType: platformUtils.OSType.Windows, | ||
pythonBin: 'Scripts/python.exe', | ||
})); | ||
|
||
test('project with multiple defined envs', async () => { | ||
locator = new HatchLocator(projectDirs.project2); | ||
exec.callsFake(makeExecHandler(venvDirs.project2, { path: true, cwd: projectDirs.project2 })); | ||
|
||
const iterator = locator.iterEnvs(); | ||
const actualEnvs = await getEnvs(iterator); | ||
|
||
const expectedEnvs = [ | ||
createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.default, 'bin/python')), | ||
createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.test, 'bin/python')), | ||
]; | ||
assertBasicEnvsEqual(actualEnvs, expectedEnvs); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.