From 16a70e1be7862663ac38f10e8d8b6895a72b91a8 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 23 Jan 2024 17:11:49 +0100 Subject: [PATCH 01/13] Some initial progress on a Hatch env locator --- .../pythonEnvironments/base/info/index.ts | 2 + .../base/locators/hatchLocator.ts | 57 ++++++++ .../common/environmentManagers/hatch.ts | 135 ++++++++++++++++++ src/client/pythonEnvironments/index.ts | 2 + 4 files changed, 196 insertions(+) create mode 100644 src/client/pythonEnvironments/base/locators/hatchLocator.ts create mode 100644 src/client/pythonEnvironments/common/environmentManagers/hatch.ts diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index e55031fe8078..b6e9567b4a4e 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -15,6 +15,7 @@ export enum PythonEnvKind { MicrosoftStore = 'global-microsoft-store', Pyenv = 'global-pyenv', Poetry = 'poetry', + Hatch = 'hatch', ActiveState = 'activestate', Custom = 'global-custom', OtherGlobal = 'global-other', @@ -44,6 +45,7 @@ export interface EnvPathType { export const virtualEnvKinds = [ PythonEnvKind.Poetry, + PythonEnvKind.Hatch, PythonEnvKind.Pipenv, PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, diff --git a/src/client/pythonEnvironments/base/locators/hatchLocator.ts b/src/client/pythonEnvironments/base/locators/hatchLocator.ts new file mode 100644 index 000000000000..118da1ecb203 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/hatchLocator.ts @@ -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 { + 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 { + 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); + } +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts new file mode 100644 index 000000000000..b56e382d4995 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts @@ -0,0 +1,135 @@ +import * as path from 'path'; +import { isTestExecution } from '../../../common/constants'; +import { exec, getPythonSetting, pathExists, pathExistsSync, readFileSync } from '../externalDependencies'; +import { traceError, 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> = new Map< + string, + Promise + >(); + + /** + * 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. + */ + public static async getHatch(cwd: string): Promise { + // Following check should be performed synchronously so we trigger hatch execution as soon as possible. + if (!hasValidHatchConfig(cwd)) { + // This check is not expensive and may change during a session, so we need not cache it. + return 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 { + // First thing this method awaits on should be hatch command execution, + // hence perform all operations before that synchronously. + + traceVerbose(`Getting hatch for cwd ${cwd}`); + // Produce a list of candidate binaries to be probed by exec'ing them. + function* getCandidates() { + try { + const customHatchPath = getPythonSetting('hatchPath'); + if (customHatchPath && customHatchPath !== 'hatch') { + // If user has specified a custom Hatch path, use it first. + yield customHatchPath; + } + } catch (ex) { + traceError(`Failed to get Hatch setting`, ex); + } + // Check unqualified filename, in case it's on PATH. + yield 'hatch'; + } + + // Probe the candidates, and pick the first one that exists and does what we need. + for (const hatchPath of getCandidates()) { + traceVerbose(`Probing Hatch binary for ${cwd}: ${hatchPath}`); + const hatch = new Hatch(hatchPath, cwd); + const virtualenvs = await hatch.getEnvList(); + if (virtualenvs !== undefined) { + traceVerbose(`Found hatch via filesystem probing for ${cwd}: ${hatchPath}`); + return hatch; + } + traceVerbose(`Failed to find Hatch for ${cwd}: ${hatchPath}`); + } + + // Didn't find anything. + traceVerbose(`No Hatch binary found for ${cwd}`); + 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 { + 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 { + 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] : [])); + } +} + +/** + * Does best effort to verify whether a folder has been setup for hatch. + * + * @param dir Directory to look for pyproject.toml file in. + */ +function hasValidHatchConfig(dir: string): boolean { + if (pathExistsSync(path.join(dir, 'hatch.toml'))) { + return true; + } + const pyprojectToml = path.join(dir, 'pyproject.toml'); + if (pathExistsSync(pyprojectToml)) { + const content = readFileSync(pyprojectToml); + if (/^\[?tool.hatch\b/.test(content)) { + return true; + } + } + return false; +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 5a5fceffa693..9b2d07972fac 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -28,6 +28,7 @@ import { MicrosoftStoreLocator } from './base/locators/lowLevel/microsoftStoreLo import { getEnvironmentInfoService } from './base/info/environmentInfoService'; import { registerNewDiscoveryForIOC } from './legacyIOC'; import { PoetryLocator } from './base/locators/lowLevel/poetryLocator'; +import { HatchLocator } from './base/locators/hatchLocator'; import { createPythonEnvironments } from './api'; import { createCollectionCache as createCache, @@ -186,6 +187,7 @@ function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { (root: vscode.Uri) => [ new WorkspaceVirtualEnvironmentLocator(root.fsPath), new PoetryLocator(root.fsPath), + new HatchLocator(root.fsPath), new CustomWorkspaceLocator(root.fsPath), ], // Add an ILocator factory func here for each kind of workspace-rooted locator. From 1147fff5754b86ac494733901a39c712ba6a63d8 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 30 Jan 2024 15:24:16 +0100 Subject: [PATCH 02/13] fix tests and lints --- src/client/pythonEnvironments/base/locators/hatchLocator.ts | 4 ++-- src/test/pythonEnvironments/base/info/envKind.unit.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/hatchLocator.ts b/src/client/pythonEnvironments/base/locators/hatchLocator.ts index 118da1ecb203..f5b4ba10e360 100644 --- a/src/client/pythonEnvironments/base/locators/hatchLocator.ts +++ b/src/client/pythonEnvironments/base/locators/hatchLocator.ts @@ -1,4 +1,4 @@ -'use strict' +'use strict'; import { PythonEnvKind } from '../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../locator'; @@ -15,7 +15,7 @@ import { getInterpreterPathFromDir } from '../../common/commonUtils'; */ async function getVirtualEnvDirs(root: string): Promise { const hatch = await Hatch.getHatch(root); - const envDirs = await hatch?.getEnvList() ?? []; + const envDirs = (await hatch?.getEnvList()) ?? []; return asyncFilter(envDirs, pathExists); } diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts index fdf174b4c551..997c8a08c7f2 100644 --- a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -13,6 +13,7 @@ const KIND_NAMES: [PythonEnvKind, string][] = [ [PythonEnvKind.MicrosoftStore, 'winStore'], [PythonEnvKind.Pyenv, 'pyenv'], [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Hatch, 'hatch'], [PythonEnvKind.Custom, 'customGlobal'], [PythonEnvKind.OtherGlobal, 'otherGlobal'], [PythonEnvKind.Venv, 'venv'], From 21f73b734d12052da6095234250c8aa2a371ae9e Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 30 Jan 2024 15:51:16 +0100 Subject: [PATCH 03/13] fix remaining tests --- src/client/pythonEnvironments/base/info/envKind.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index ff53a57d2f45..14e98afd40c0 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -15,6 +15,7 @@ export function getKindDisplayName(kind: PythonEnvKind): string { [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], [PythonEnvKind.Pyenv, 'pyenv'], [PythonEnvKind.Poetry, 'Poetry'], + [PythonEnvKind.Hatch, 'Hatch'], [PythonEnvKind.Custom, 'custom'], // For now we treat OtherGlobal like Unknown. [PythonEnvKind.Venv, 'venv'], @@ -61,6 +62,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] { PythonEnvKind.MicrosoftStore, PythonEnvKind.Pipenv, PythonEnvKind.Poetry, + PythonEnvKind.Hatch, PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnv, From c4199b1d4b9591a8d5f6bd9fe2de42060c4a5f50 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 30 Jan 2024 15:52:55 +0100 Subject: [PATCH 04/13] docs --- src/client/pythonEnvironments/base/info/envKind.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 14e98afd40c0..77ed7f22533e 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -40,12 +40,13 @@ export function getKindDisplayName(kind: PythonEnvKind): string { * Remarks: This is the order of detection based on how the various distributions and tools * configure the environment, and the fall back for identification. * Top level we have the following environment types, since they leave a unique signature - * in the environment or * use a unique path for the environments they create. + * in the environment or use a unique path for the environments they create. * 1. Conda * 2. Microsoft Store * 3. PipEnv * 4. Pyenv * 5. Poetry + * 6. Hatch * * Next level we have the following virtual environment tools. The are here because they * are consumed by the tools above, and can also be used independently. From 78449174cc119a65cbe2ffc59443f327af4f0b8a Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 2 Feb 2024 11:16:18 +0100 Subject: [PATCH 05/13] move hatch locator --- .../locators/{ => lowLevel}/hatchLocator.ts | 18 +++++++++--------- src/client/pythonEnvironments/index.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) rename src/client/pythonEnvironments/base/locators/{ => lowLevel}/hatchLocator.ts (74%) diff --git a/src/client/pythonEnvironments/base/locators/hatchLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts similarity index 74% rename from src/client/pythonEnvironments/base/locators/hatchLocator.ts rename to src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts index f5b4ba10e360..f7746a8c5a2e 100644 --- a/src/client/pythonEnvironments/base/locators/hatchLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts @@ -1,14 +1,14 @@ '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'; +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. diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 9b2d07972fac..7aaa8a58b1eb 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -28,7 +28,7 @@ import { MicrosoftStoreLocator } from './base/locators/lowLevel/microsoftStoreLo import { getEnvironmentInfoService } from './base/info/environmentInfoService'; import { registerNewDiscoveryForIOC } from './legacyIOC'; import { PoetryLocator } from './base/locators/lowLevel/poetryLocator'; -import { HatchLocator } from './base/locators/hatchLocator'; +import { HatchLocator } from './base/locators/lowLevel/hatchLocator'; import { createPythonEnvironments } from './api'; import { createCollectionCache as createCache, From 779f97f57aa5fd889a9e1343fd0feb967fdc0dfe Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 2 Feb 2024 13:07:30 +0100 Subject: [PATCH 06/13] add setting --- package.json | 6 ++++++ package.nls.json | 1 + resources/report_issue_user_settings.json | 1 + src/client/common/configSettings.ts | 4 ++++ src/client/common/types.ts | 1 + src/test/common/configSettings/configSettings.unit.test.ts | 2 ++ 6 files changed, 15 insertions(+) diff --git a/package.json b/package.json index 1772766d8dfe..f2b6b567be1d 100644 --- a/package.json +++ b/package.json @@ -656,6 +656,12 @@ "scope": "machine-overridable", "type": "string" }, + "python.hatchPath": { + "default": "hatch", + "description": "%python.hatchPath.description%", + "scope": "machine-overridable", + "type": "string" + }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", diff --git a/package.nls.json b/package.nls.json index 5a4c44a78191..cfde29ed0c60 100644 --- a/package.nls.json +++ b/package.nls.json @@ -59,6 +59,7 @@ "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", + "python.hatchPath.description": "Path to the Hatch executable.", "python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index eea4ca007da6..b0d0837c601a 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -11,6 +11,7 @@ "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", + "hatchPath": "placeholder", "devOptions": false, "globalModuleInstallation": false, "languageServer": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 88a5007467bb..8dc5744b6658 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -103,6 +103,8 @@ export class PythonSettings implements IPythonSettings { public poetryPath = ''; + public hatchPath = ''; + public devOptions: string[] = []; public autoComplete!: IAutoCompleteSettings; @@ -260,6 +262,8 @@ export class PythonSettings implements IPythonSettings { this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + const hatchPath = systemVariables.resolveAny(pythonSettings.get('hatchPath'))!; + this.hatchPath = hatchPath && hatchPath.length > 0 ? getAbsolutePath(hatchPath, workspaceRoot) : hatchPath; this.interpreter = pythonSettings.get('interpreter') ?? { infoVisibility: 'onPythonRelated', diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 67fcf5c7b700..9bacc9daa82f 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -167,6 +167,7 @@ export interface IPythonSettings { readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; + readonly hatchPath: string; readonly devOptions: string[]; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 83b5b4a3d524..b4a72e9fccd5 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -83,6 +83,7 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', + 'hatchPath', 'defaultInterpreterPath', ]) { config @@ -141,6 +142,7 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', + 'hatchPath', 'defaultInterpreterPath', ].forEach(async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); From 968d693ce11bd899a0bbde3dd364cd4fddf062f3 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 2 Feb 2024 13:57:20 +0100 Subject: [PATCH 07/13] relax locator --- .../common/environmentManagers/hatch.ts | 29 ++----------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts index b56e382d4995..ac9b0f0999fc 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts @@ -1,6 +1,5 @@ -import * as path from 'path'; import { isTestExecution } from '../../../common/constants'; -import { exec, getPythonSetting, pathExists, pathExistsSync, readFileSync } from '../externalDependencies'; +import { exec, getPythonSetting, pathExists } from '../externalDependencies'; import { traceError, traceVerbose } from '../../../logging'; import { cache } from '../../../common/utils/decorators'; @@ -27,13 +26,10 @@ export class Hatch { /** * 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 { - // Following check should be performed synchronously so we trigger hatch execution as soon as possible. - if (!hasValidHatchConfig(cwd)) { - // This check is not expensive and may change during a session, so we need not cache it. - return undefined; - } if (Hatch.hatchPromise.get(cwd) === undefined || isTestExecution()) { Hatch.hatchPromise.set(cwd, Hatch.locate(cwd)); } @@ -114,22 +110,3 @@ export class Hatch { return envPaths.flatMap((r) => (r ? [r] : [])); } } - -/** - * Does best effort to verify whether a folder has been setup for hatch. - * - * @param dir Directory to look for pyproject.toml file in. - */ -function hasValidHatchConfig(dir: string): boolean { - if (pathExistsSync(path.join(dir, 'hatch.toml'))) { - return true; - } - const pyprojectToml = path.join(dir, 'pyproject.toml'); - if (pathExistsSync(pyprojectToml)) { - const content = readFileSync(pyprojectToml); - if (/^\[?tool.hatch\b/.test(content)) { - return true; - } - } - return false; -} From ddbbdaf21c8dd410e838faf2c78151bc3e181c78 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 2 Feb 2024 13:57:38 +0100 Subject: [PATCH 08/13] add tests --- .../lowLevel/hatchLocator.unit.test.ts | 51 ++++++++++ .../environmentManagers/hatch.unit.test.ts | 96 +++++++++++++++++++ .../python/cK2g6fIm/project1/bin/python | 0 .../python/cK2g6fIm/project1/pyvenv.cfg | 3 + .../common/envlayouts/hatch/project1/.gitkeep | 0 5 files changed, 150 insertions(+) create mode 100644 src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts create mode 100644 src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts new file mode 100644 index 000000000000..b6825c05dda3 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -0,0 +1,51 @@ +// 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('Non-Windows', () => { + setup(() => { + locator = new HatchLocator(projectDirs.project1); + getOSType.returns(platformUtils.OSType.Linux); + exec.callsFake( + makeExecHandler(venvDirs.project1.default, { hatchPath: 'hatch', cwd: projectDirs.project1 }), + ); + }); + + test('iterEnvs()', async () => { + // Act + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + // Assert + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, 'bin/python')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts new file mode 100644 index 000000000000..3e2fbec41e9b --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { Hatch } from '../../../../client/pythonEnvironments/common/environmentManagers/hatch'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +export type HatchCommand = { cmd: 'env show --json' } | { cmd: 'env find'; env: string } | { cmd: null }; + +export function hatchCommand(args: string[]): HatchCommand { + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'env' && args[1] === 'show' && args[2] === '--json') { + return { cmd: 'env show --json' }; + } + if (args[0] === 'env' && args[1] === 'find') { + return { cmd: 'env find', env: args[2] }; + } + return { cmd: null }; +} + +interface VerifyOptions { + hatchPath?: string; + cwd?: string; +} + +export function makeExecHandler(venvDir: string, verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise> => { + if (verify.hatchPath && file !== verify.hatchPath) { + throw new Error('Command failed'); + } + if (verify.cwd) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error('Command failed'); + } + } + const cmd = hatchCommand(args); + if (cmd.cmd === 'env show --json') { + return { stdout: '{"default":{"type":"virtual"}}' }; + } + if (cmd.cmd === 'env find') { + return { stdout: venvDir }; + } + throw new Error('Command failed'); + }; +} + +const testHatchDir = path.join(TEST_LAYOUT_ROOT, 'hatch'); +// This is usually in /hatch, e.g. `~/.local/share/hatch` +const hatchEnvsDir = path.join(testHatchDir, 'env/virtual/python'); +export const projectDirs = { project1: path.join(testHatchDir, 'project1') }; +export const venvDirs = { project1: { default: path.join(hatchEnvsDir, 'cK2g6fIm/project1') } }; + +suite('Hatch binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (hatchPath: string, verify = true) => { + getPythonSetting.returns(hatchPath); + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake( + makeExecHandler(venvDirs.project1.default, verify ? { hatchPath, cwd: projectDirs.project1 } : undefined), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal(hatchPath); + }; + + test('Return a Hatch instance in an empty directory', () => testPath('hatchPath', false)); + test('When user has specified a valid Hatch path, use it', () => testPath('path/to/hatch/binary')); + // 'hatch' is the default value + test('When user hasn’t specified a path, use Hatch on PATH if available', () => testPath('hatch')); + + test('Return undefined if Hatch cannot be found', async () => { + getPythonSetting.returns('hatch'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg new file mode 100644 index 000000000000..365d6f5eacee --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.11 +include-system-site-packages = false +version = 3.11.1 diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep b/src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 From e036a7ff3b5156e0b97565ca0f6ae7d3fdf3ba44 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 2 Feb 2024 13:59:49 +0100 Subject: [PATCH 09/13] more precise tests --- .../common/environmentManagers/hatch.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts index 3e2fbec41e9b..6d41ca0ab148 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -44,7 +44,7 @@ export function makeExecHandler(venvDir: string, verify: VerifyOptions = {}) { if (cmd.cmd === 'env show --json') { return { stdout: '{"default":{"type":"virtual"}}' }; } - if (cmd.cmd === 'env find') { + if (cmd.cmd === 'env find' && cmd.env === 'default') { return { stdout: venvDir }; } throw new Error('Command failed'); From 77db3f0803d3f9fb9e6d7a77070b29fb5662fcf8 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 2 Feb 2024 14:49:35 +0100 Subject: [PATCH 10/13] test multiple --- .../lowLevel/hatchLocator.unit.test.ts | 27 +++++++++++++------ .../environmentManagers/hatch.unit.test.ts | 24 ++++++++++++----- .../python/q4In3tK-/project2/bin/python | 0 .../python/q4In3tK-/project2/pyvenv.cfg | 3 +++ .../virtual/python/q4In3tK-/test/bin/python | 0 .../virtual/python/q4In3tK-/test/pyvenv.cfg | 3 +++ .../envlayouts/hatch/project2/hatch.toml | 6 +++++ 7 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts index b6825c05dda3..99f88a60cd87 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -27,25 +27,36 @@ suite('Hatch Locator', () => { suiteTeardown(() => sinon.restore()); - suite('Non-Windows', () => { + suite('iterEnvs()', () => { setup(() => { - locator = new HatchLocator(projectDirs.project1); getOSType.returns(platformUtils.OSType.Linux); - exec.callsFake( - makeExecHandler(venvDirs.project1.default, { hatchPath: 'hatch', cwd: projectDirs.project1 }), - ); }); - test('iterEnvs()', async () => { - // Act + test('project with only the default env', async () => { + locator = new HatchLocator(projectDirs.project1); + exec.callsFake(makeExecHandler(venvDirs.project1, { hatchPath: 'hatch', cwd: projectDirs.project1 })); + const iterator = locator.iterEnvs(); const actualEnvs = await getEnvs(iterator); - // Assert const expectedEnvs = [ createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, 'bin/python')), ]; assertBasicEnvsEqual(actualEnvs, expectedEnvs); }); + + test('project with multiple defined envs', async () => { + locator = new HatchLocator(projectDirs.project2); + exec.callsFake(makeExecHandler(venvDirs.project2, { hatchPath: 'hatch', 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); + }); }); }); diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts index 6d41ca0ab148..8a1f1e0ee186 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -29,7 +29,7 @@ interface VerifyOptions { cwd?: string; } -export function makeExecHandler(venvDir: string, verify: VerifyOptions = {}) { +export function makeExecHandler(venvDirs: Record, verify: VerifyOptions = {}) { return async (file: string, args: string[], options: ShellOptions): Promise> => { if (verify.hatchPath && file !== verify.hatchPath) { throw new Error('Command failed'); @@ -42,10 +42,11 @@ export function makeExecHandler(venvDir: string, verify: VerifyOptions = {}) { } const cmd = hatchCommand(args); if (cmd.cmd === 'env show --json') { - return { stdout: '{"default":{"type":"virtual"}}' }; + const envs = Object.fromEntries(Object.keys(venvDirs).map((name) => [name, { type: 'virtual' }])); + return { stdout: JSON.stringify(envs) }; } - if (cmd.cmd === 'env find' && cmd.env === 'default') { - return { stdout: venvDir }; + if (cmd.cmd === 'env find' && cmd.env in venvDirs) { + return { stdout: venvDirs[cmd.env] }; } throw new Error('Command failed'); }; @@ -54,8 +55,17 @@ export function makeExecHandler(venvDir: string, verify: VerifyOptions = {}) { const testHatchDir = path.join(TEST_LAYOUT_ROOT, 'hatch'); // This is usually in /hatch, e.g. `~/.local/share/hatch` const hatchEnvsDir = path.join(testHatchDir, 'env/virtual/python'); -export const projectDirs = { project1: path.join(testHatchDir, 'project1') }; -export const venvDirs = { project1: { default: path.join(hatchEnvsDir, 'cK2g6fIm/project1') } }; +export const projectDirs = { + project1: path.join(testHatchDir, 'project1'), + project2: path.join(testHatchDir, 'project2'), +}; +export const venvDirs = { + project1: { default: path.join(hatchEnvsDir, 'cK2g6fIm/project1') }, + project2: { + default: path.join(hatchEnvsDir, 'q4In3tK-/project2'), + test: path.join(hatchEnvsDir, 'q4In3tK-/test'), + }, +}; suite('Hatch binary is located correctly', async () => { let exec: sinon.SinonStub; @@ -74,7 +84,7 @@ suite('Hatch binary is located correctly', async () => { getPythonSetting.returns(hatchPath); // If `verify` is false, don’t verify that the command has been called with that path exec.callsFake( - makeExecHandler(venvDirs.project1.default, verify ? { hatchPath, cwd: projectDirs.project1 } : undefined), + makeExecHandler(venvDirs.project1, verify ? { hatchPath, cwd: projectDirs.project1 } : undefined), ); const hatch = await Hatch.getHatch(projectDirs.project1); expect(hatch?.command).to.equal(hatchPath); diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml new file mode 100644 index 000000000000..9848374b54fd --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml @@ -0,0 +1,6 @@ +# this file is not actually used in tests, as all is mocked out + +# The default environment always exists +#[envs.default] + +[envs.test] From f5267ab0befcd3ae00bb4a1491708a0b3d396426 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Mon, 19 Feb 2024 11:48:24 +0100 Subject: [PATCH 11/13] Windows --- .vscode/launch.json | 2 +- .../lowLevel/hatchLocator.unit.test.ts | 24 +++++++++++++++---- .../cK2g6fIm/project1/Scripts/python.exe | 1 + 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe diff --git a/.vscode/launch.json b/.vscode/launch.json index 82981a93305d..5bed8bdbcd40 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -160,7 +160,7 @@ "--ui=tdd", "--recursive", "--colors", - //"--grep", "", + "--grep", "Hatch Locator", "--timeout=300000" ], "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts index 99f88a60cd87..33c908589446 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -32,18 +32,32 @@ suite('Hatch Locator', () => { getOSType.returns(platformUtils.OSType.Linux); }); - test('project with only the default env', async () => { + 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, { hatchPath: 'hatch', cwd: projectDirs.project1 })); const iterator = locator.iterEnvs(); const actualEnvs = await getEnvs(iterator); - const expectedEnvs = [ - createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, 'bin/python')), - ]; + 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); diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe From 9ab2635728696020de5b7821480fd47e26c93e4c Mon Sep 17 00:00:00 2001 From: Philipp A Date: Thu, 22 Feb 2024 09:18:29 +0100 Subject: [PATCH 12/13] Discard changes to .vscode/launch.json --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 5bed8bdbcd40..82981a93305d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -160,7 +160,7 @@ "--ui=tdd", "--recursive", "--colors", - "--grep", "Hatch Locator", + //"--grep", "", "--timeout=300000" ], "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], From c445f80e0ece35e03c69ebdde69f97c074d84da5 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 8 Mar 2024 09:29:54 +0100 Subject: [PATCH 13/13] remove setting --- package.json | 6 --- package.nls.json | 1 - resources/report_issue_user_settings.json | 1 - src/client/common/configSettings.ts | 4 -- src/client/common/types.ts | 1 - .../common/environmentManagers/hatch.ts | 41 +++++-------------- .../configSettings.unit.test.ts | 2 - .../lowLevel/hatchLocator.unit.test.ts | 4 +- .../environmentManagers/hatch.unit.test.ts | 16 +++----- 9 files changed, 19 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index fee28ccaa350..a753336396e4 100644 --- a/package.json +++ b/package.json @@ -650,12 +650,6 @@ "scope": "machine-overridable", "type": "string" }, - "python.hatchPath": { - "default": "hatch", - "description": "%python.hatchPath.description%", - "scope": "machine-overridable", - "type": "string" - }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", diff --git a/package.nls.json b/package.nls.json index cfde29ed0c60..5a4c44a78191 100644 --- a/package.nls.json +++ b/package.nls.json @@ -59,7 +59,6 @@ "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", - "python.hatchPath.description": "Path to the Hatch executable.", "python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index b0d0837c601a..eea4ca007da6 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -11,7 +11,6 @@ "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", - "hatchPath": "placeholder", "devOptions": false, "globalModuleInstallation": false, "languageServer": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 8dc5744b6658..88a5007467bb 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -103,8 +103,6 @@ export class PythonSettings implements IPythonSettings { public poetryPath = ''; - public hatchPath = ''; - public devOptions: string[] = []; public autoComplete!: IAutoCompleteSettings; @@ -262,8 +260,6 @@ export class PythonSettings implements IPythonSettings { this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; - const hatchPath = systemVariables.resolveAny(pythonSettings.get('hatchPath'))!; - this.hatchPath = hatchPath && hatchPath.length > 0 ? getAbsolutePath(hatchPath, workspaceRoot) : hatchPath; this.interpreter = pythonSettings.get('interpreter') ?? { infoVisibility: 'onPythonRelated', diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 9bacc9daa82f..67fcf5c7b700 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -167,7 +167,6 @@ export interface IPythonSettings { readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; - readonly hatchPath: string; readonly devOptions: string[]; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; diff --git a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts index ac9b0f0999fc..a22ce38bbffb 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts @@ -1,6 +1,6 @@ import { isTestExecution } from '../../../common/constants'; -import { exec, getPythonSetting, pathExists } from '../externalDependencies'; -import { traceError, traceVerbose } from '../../../logging'; +import { exec, pathExists } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; import { cache } from '../../../common/utils/decorators'; /** Wraps the "Hatch" utility, and exposes its functionality. @@ -39,37 +39,18 @@ export class Hatch { private static async locate(cwd: string): Promise { // First thing this method awaits on should be hatch command execution, // hence perform all operations before that synchronously. - - traceVerbose(`Getting hatch for cwd ${cwd}`); - // Produce a list of candidate binaries to be probed by exec'ing them. - function* getCandidates() { - try { - const customHatchPath = getPythonSetting('hatchPath'); - if (customHatchPath && customHatchPath !== 'hatch') { - // If user has specified a custom Hatch path, use it first. - yield customHatchPath; - } - } catch (ex) { - traceError(`Failed to get Hatch setting`, ex); - } - // Check unqualified filename, in case it's on PATH. - yield 'hatch'; - } - - // Probe the candidates, and pick the first one that exists and does what we need. - for (const hatchPath of getCandidates()) { - traceVerbose(`Probing Hatch binary for ${cwd}: ${hatchPath}`); - const hatch = new Hatch(hatchPath, cwd); - const virtualenvs = await hatch.getEnvList(); - if (virtualenvs !== undefined) { - traceVerbose(`Found hatch via filesystem probing for ${cwd}: ${hatchPath}`); - return hatch; - } - traceVerbose(`Failed to find Hatch for ${cwd}: ${hatchPath}`); + 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 for ${cwd}`); + traceVerbose(`No Hatch binary found`); return undefined; } diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index b4a72e9fccd5..83b5b4a3d524 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -83,7 +83,6 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', - 'hatchPath', 'defaultInterpreterPath', ]) { config @@ -142,7 +141,6 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', - 'hatchPath', 'defaultInterpreterPath', ].forEach(async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts index 33c908589446..9a2a69908f2a 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -43,7 +43,7 @@ suite('Hatch Locator', () => { } locator = new HatchLocator(projectDirs.project1); - exec.callsFake(makeExecHandler(venvDirs.project1, { hatchPath: 'hatch', cwd: projectDirs.project1 })); + exec.callsFake(makeExecHandler(venvDirs.project1, { path: true, cwd: projectDirs.project1 })); const iterator = locator.iterEnvs(); const actualEnvs = await getEnvs(iterator); @@ -61,7 +61,7 @@ suite('Hatch Locator', () => { test('project with multiple defined envs', async () => { locator = new HatchLocator(projectDirs.project2); - exec.callsFake(makeExecHandler(venvDirs.project2, { hatchPath: 'hatch', cwd: projectDirs.project2 })); + exec.callsFake(makeExecHandler(venvDirs.project2, { path: true, cwd: projectDirs.project2 })); const iterator = locator.iterEnvs(); const actualEnvs = await getEnvs(iterator); diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts index 8a1f1e0ee186..5d348aa2b131 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -25,13 +25,13 @@ export function hatchCommand(args: string[]): HatchCommand { } interface VerifyOptions { - hatchPath?: string; + path?: boolean; cwd?: string; } export function makeExecHandler(venvDirs: Record, verify: VerifyOptions = {}) { return async (file: string, args: string[], options: ShellOptions): Promise> => { - if (verify.hatchPath && file !== verify.hatchPath) { + if (verify.path && file !== 'hatch') { throw new Error('Command failed'); } if (verify.cwd) { @@ -80,20 +80,16 @@ suite('Hatch binary is located correctly', async () => { sinon.restore(); }); - const testPath = async (hatchPath: string, verify = true) => { - getPythonSetting.returns(hatchPath); + const testPath = async (verify = true) => { // If `verify` is false, don’t verify that the command has been called with that path exec.callsFake( - makeExecHandler(venvDirs.project1, verify ? { hatchPath, cwd: projectDirs.project1 } : undefined), + makeExecHandler(venvDirs.project1, verify ? { path: true, cwd: projectDirs.project1 } : undefined), ); const hatch = await Hatch.getHatch(projectDirs.project1); - expect(hatch?.command).to.equal(hatchPath); + expect(hatch?.command).to.equal('hatch'); }; - test('Return a Hatch instance in an empty directory', () => testPath('hatchPath', false)); - test('When user has specified a valid Hatch path, use it', () => testPath('path/to/hatch/binary')); - // 'hatch' is the default value - test('When user hasn’t specified a path, use Hatch on PATH if available', () => testPath('hatch')); + test('Use Hatch on PATH if available', () => testPath()); test('Return undefined if Hatch cannot be found', async () => { getPythonSetting.returns('hatch');