forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add support for Hatch environments #22779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
16a70e1
Some initial progress on a Hatch env locator
flying-sheep 1147fff
fix tests and lints
flying-sheep 21f73b7
fix remaining tests
flying-sheep c4199b1
docs
flying-sheep 7844917
move hatch locator
flying-sheep 779f97f
add setting
flying-sheep 968d693
relax locator
flying-sheep ddbbdaf
add tests
flying-sheep 791bc52
Merge branch 'main' into hatch-locator
flying-sheep e036a7f
more precise tests
flying-sheep 77db3f0
test multiple
flying-sheep f5267ab
Windows
flying-sheep 9ab2635
Discard changes to .vscode/launch.json
flying-sheep 7afca82
Merge branch 'main' into hatch-locator
flying-sheep c445f80
remove setting
flying-sheep ce0b848
Merge branch 'main' into hatch-locator
flying-sheep File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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
src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts
This file contains hidden or 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
src/client/pythonEnvironments/common/environmentManagers/hatch.ts
This file contains hidden or 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 hidden or 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 hidden or 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
src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts
This file contains hidden or 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); | ||
flying-sheep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.