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
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,18 @@
"category": "Python",
"icon": "$(trash)"
},
{
"command": "python-envs.clearScriptEnvCache",
"title": "%python-envs.clearScriptEnvCache.title%",
"category": "Python Envs",
"icon": "$(trash)"
},
{
"command": "python-envs.setupInlineScriptEnvs",
"title": "%python-envs.setupInlineScriptEnvs.title%",
"category": "Python Envs",
"icon": "$(tools)"
},
{
"command": "python-envs.runInTerminal",
"title": "%python-envs.runInTerminal.title%",
Expand Down Expand Up @@ -414,6 +426,14 @@
"command": "python-envs.runAsTask",
"when": "config.python.useEnvironmentsExtension != false"
},
{
"command": "python-envs.clearScriptEnvCache",
"when": "pythonEnvsInlineScriptsEnabled"
},
{
"command": "python-envs.setupInlineScriptEnvs",
"when": "pythonEnvsInlineScriptsEnabled"
},
{
"command": "python-envs.terminal.activate",
"when": "pythonTerminalActivation"
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"python-envs.refreshPackages.title": "Refresh Packages List",
"python-envs.packages.title": "Manage Packages",
"python-envs.clearCache.title": "Clear Cache",
"python-envs.clearScriptEnvCache.title": "Clear Inline Script Environment Cache",
"python-envs.setupInlineScriptEnvs.title": "Set Up Environments for Inline Script Files",
"python-envs.runInTerminal.title": "Run in Terminal",
"python-envs.createTerminal.title": "Create Python Terminal",
"python-envs.runAsTask.title": "Run as Task",
Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ import { NewScriptProject } from './features/creators/newScriptProject';
import { ProjectCreatorsImpl } from './features/creators/projectCreators';
import {
addPythonProjectCommand,
copyPathToClipboard,
clearEnvironmentCachesCommand,
clearScriptEnvironmentCacheCommand,
copyPathToClipboard,
createAnyEnvironmentCommand,
createEnvironmentCommand,
createTerminalCommand,
Expand Down Expand Up @@ -192,6 +192,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
context.subscriptions.push(inlineScriptRouting);
}

void commands.executeCommand('setContext', 'pythonEnvsInlineScriptsEnabled', inlineScriptFeatureActivation.enabled);

const envVarManager: EnvVarManager = new PythonEnvVariableManager(projectManager);
context.subscriptions.push(envVarManager);

Expand Down
32 changes: 26 additions & 6 deletions src/features/inlineScript/setupEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import { commands, Disposable, l10n, QuickPickItem, Uri, window } from 'vscode';
import { PythonEnvironment } from '../../api';
import { INLINE_SCRIPT_MANAGER_ID } from '../../common/constants';
import { InlineScriptRoutingRegistry } from '../../common/inlineScript/routingRegistry';
import { readInlineScriptMetadataFromFile } from '../../common/inlineScript/metadata';
import { InlineScriptRoutingRegistry } from '../../common/inlineScript/routingRegistry';
import { traceError, traceInfo } from '../../common/logging';
import { normalizePath } from '../../common/utils/pathUtils';
import { showErrorMessage, showInformationMessage, showQuickPickWithButtons } from '../../common/window.apis';
import { asRelativePath, findFiles } from '../../common/workspace.apis';
import { asRelativePath, findFiles, getOpenTextDocuments } from '../../common/workspace.apis';
import { EnvironmentManagers } from '../../internal.api';
import { registerInlineScriptCodeLens } from './codeLens';

Expand All @@ -19,8 +20,9 @@ import { registerInlineScriptCodeLens } from './codeLens';
export const SETUP_INLINE_SCRIPT_ENV_COMMAND = 'python-envs.setupInlineScriptEnv';

/**
* Hidden command that scans the workspace and sets up environments for the selected inline-script
* files. Intentionally not contributed in `package.json` while the feature is behind the internal flag.
* Command that scans the workspace and sets up environments for the selected inline-script files.
* Contributed in `package.json` but only shown in the Command Palette while the inline-scripts
* feature flag is enabled (gated by the `pythonEnvsInlineScriptsEnabled` context key).
*/
export const SETUP_INLINE_SCRIPT_ENVS_COMMAND = 'python-envs.setupInlineScriptEnvs';

Expand Down Expand Up @@ -53,6 +55,7 @@ export async function setUpInlineScriptEnvironment(
traceError('Inline-script setup requested but the inline-script environment manager is not registered.');
return undefined;
}
await seedRoutingMetadataForClosedScript(scriptUri, routing);
const metadataIdentityBeforeCreate = routing.getMetadataIdentity(scriptUri);
const environment = await manager.create(scriptUri, undefined);
if (!environment) {
Expand All @@ -69,6 +72,23 @@ export async function setUpInlineScriptEnvironment(
return environment;
}

async function seedRoutingMetadataForClosedScript(scriptUri: Uri, routing: InlineScriptRoutingRegistry): Promise<void> {
if (routing.getMetadata(scriptUri)) {
return;
}
const scriptPath = normalizePath(scriptUri.fsPath);
const isOpen = getOpenTextDocuments().some(
(document) => document.uri.scheme === 'file' && normalizePath(document.uri.fsPath) === scriptPath,
);
if (isOpen) {
return;
}
const metadata = await readInlineScriptMetadataFromFile(scriptUri);
if (metadata && !routing.getMetadata(scriptUri)) {
routing.setMetadata(scriptUri, metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Could this re-check whether the document became open after the awaited metadata read? A user can open and edit the script while the read is pending; in that case this can publish stale on-disk metadata even though the detector intentionally withholds metadata for the edited document.

}
}

function setupInlineScriptEnvironmentHandler(
em: EnvironmentManagers,
routing: InlineScriptRoutingRegistry,
Expand Down Expand Up @@ -182,8 +202,8 @@ async function filterInlineScriptFiles(files: readonly Uri[]): Promise<Uri[]> {

/**
* Register the inline-script user-facing surfaces (the CodeLens and its setup commands). Only called
* when the PEP 723 inline-script feature flag is enabled; the commands are intentionally hidden from
* `package.json` for now.
* when the PEP 723 inline-script feature flag is enabled. The single-file setup command is invoked by
* the CodeLens and stays out of `package.json`; the bulk command is palette-gated behind the flag.
*/
export function registerInlineScriptUx(em: EnvironmentManagers, routing: InlineScriptRoutingRegistry): Disposable[] {
return [
Expand Down
74 changes: 55 additions & 19 deletions src/managers/builtin/inlineScript/envManager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import type { Stats } from 'fs';
import * as fs from 'fs-extra';
import * as path from 'path';
import type { Stats } from 'fs';
import {
Disposable,
Event,
Expand All @@ -20,8 +20,8 @@ import {
CreateEnvironmentScope,
DidChangeEnvironmentEventArgs,
DidChangeEnvironmentsEventArgs,
EnvironmentManager,
EnvironmentChangeKind,
EnvironmentManager,
GetEnvironmentScope,
GetEnvironmentsScope,
IconPath,
Expand All @@ -31,23 +31,29 @@ import {
ResolveEnvironmentContext,
SetEnvironmentScope,
} from '../../../api';
import {
CONDA_MANAGER_ID,
INLINE_SCRIPT_MANAGER_ID,
PYENV_MANAGER_ID,
SYSTEM_MANAGER_ID,
} from '../../../common/constants';
import { getErrorMessage } from '../../../common/errors/utils';
import { computeCacheKey, normalizeDependency } from '../../../common/inlineScript/cacheKey';
import {
CacheEntrySummary,
CacheEnvironmentInspection,
INLINE_SCRIPT_CACHE_DIR_NAME,
InlineScriptEnvMeta,
hashSourceMetadataIdentity,
mergeSourceMetadataIdentityHashes,
META_SCHEMA_VERSION,
getBaseInterpreterStatus,
getScriptEnvCacheRoot,
getScriptEnvDir,
inspectOwnedCacheEntry,
hashSourceMetadataIdentity,
INLINE_SCRIPT_CACHE_DIR_NAME,
InlineScriptEnvMeta,
inspectMetaJson,
restoreMetaJsonBackupUnderLock,
inspectOwnedCacheEntry,
mergeSourceMetadataIdentityHashes,
META_SCHEMA_VERSION,
resolveCacheEntryPath,
restoreMetaJsonBackupUnderLock,
selectStaleEntries,
writeMetaJson,
} from '../../../common/inlineScript/cacheLayout';
Expand All @@ -59,20 +65,13 @@ import {
InlineScriptRoutingRegistry,
} from '../../../common/inlineScript/routingRegistry';
import {
CONDA_MANAGER_ID,
INLINE_SCRIPT_MANAGER_ID,
PYENV_MANAGER_ID,
SYSTEM_MANAGER_ID,
} from '../../../common/constants';
import {
acquireFileLock,
AcquiredFileLock,
acquireFileLock,
FILE_LOCK_DIR_SUFFIX,
getFileLockPath,
inspectFileLock,
reclaimFileLock,
} from '../../../common/lockfile.apis';
import { InlineAssociationAccessor, InlineScriptAssociationStore } from './associationStore';
import { EventNames, InlineScriptEnvErrorCategory } from '../../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../../common/telemetry/sender';
import { createDeferred, Deferred } from '../../../common/utils/deferred';
Expand All @@ -87,6 +86,7 @@ import { sortEnvironments } from '../../common/utils';
import { resolveSystemPythonEnvironmentPath } from '../utils';
import * as uvPythonInstaller from '../uvPythonInstaller';
import { createWithProgress, hasMinimumPathDepth, isDriveRoot, resolveVenvPythonEnvironmentPath } from '../venvUtils';
import { InlineAssociationAccessor, InlineScriptAssociationStore } from './associationStore';

const BASE_INTERPRETER_MANAGER_IDS = new Set([SYSTEM_MANAGER_ID, CONDA_MANAGER_ID, PYENV_MANAGER_ID]);

Expand All @@ -108,6 +108,7 @@ interface CreateOrReuseEnvironmentOptions {
readonly metadata: InlineScriptMetadata;
readonly selectedBase: SelectedBaseInterpreter;
readonly pendingCreation: PendingCreationContext;
readonly scriptUri: Uri;
}

interface BuildCacheEntryResult {
Expand Down Expand Up @@ -222,7 +223,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
public readonly onDidChangeEnvironment: Event<DidChangeEnvironmentEventArgs> = this._onDidChangeEnvironment.event;

public readonly name = 'inline-script';
public readonly displayName = l10n.t('Inline script environments');
public readonly displayName = l10n.t('Inline scripts');
public readonly preferredPackageManagerId = 'ms-python.python:pip';
public readonly description: string | undefined = undefined;
public readonly tooltip: string | MarkdownString = new MarkdownString(
Expand Down Expand Up @@ -374,6 +375,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
metadata,
selectedBase,
pendingCreation,
scriptUri,
});
pendingCreation.promise = creation;
this.pendingCreations.set(cacheKey, pendingCreation);
Expand Down Expand Up @@ -780,6 +782,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
this.api,
this,
this.baseManager,
'inlineScript',
);
} catch (error) {
this.log.warn(`Unable to resolve inline-script cache entry ${envDir.fsPath}: ${getErrorMessage(error)}`);
Expand Down Expand Up @@ -1132,6 +1135,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
this.api,
this,
this.baseManager,
'inlineScript',
);
if (!this.isCurrentAssociationRevision(scriptPath, revision)) {
return this.fsPathToEnv.get(scriptPath);
Expand Down Expand Up @@ -1304,6 +1308,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
this.api,
this,
this.baseManager,
'inlineScript',
);
} catch (error) {
this.log.warn(
Expand Down Expand Up @@ -1830,10 +1835,27 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
});
}

private async seedRoutingMetadataFromSavedFile(uri: Uri, scriptPath: string): Promise<void> {
if (this.routingRegistry.getMetadata(scriptPath) || this.isDocumentOpen(scriptPath)) {
return;
}
const metadata = await readInlineScriptMetadataFromFile(uri);
if (metadata && !this.routingRegistry.getMetadata(scriptPath)) {
this.routingRegistry.setMetadata(uri, metadata);
}
}

private isDocumentOpen(scriptPath: string): boolean {
return getOpenTextDocuments().some(
(document) => document.uri.scheme === 'file' && normalizePath(document.uri.fsPath) === scriptPath,
);
}

private initializePersistedAssociations(): Promise<void> {
return this.persistedAssociationsLoaded.then(async () => {
await Promise.all(
[...this.fsPathToPersistedAssociation.keys()].map(async (scriptPath) => {
await this.seedRoutingMetadataFromSavedFile(Uri.file(scriptPath), scriptPath);
const uri = this.routingRegistry.getUri(scriptPath);
const metadata = this.routingRegistry.getMetadata(scriptPath);
if (uri && metadata) {
Expand Down Expand Up @@ -2622,6 +2644,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
metadata,
selectedBase,
pendingCreation,
scriptUri,
}: CreateOrReuseEnvironmentOptions): Promise<PythonEnvironment | undefined> {
const dependencyCount = this.getTelemetryDependencyCount(packages);
const cacheRoot = getScriptEnvCacheRoot(this.globalStorageUri);
Expand Down Expand Up @@ -2650,7 +2673,14 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
}

const buildStartAtMs = Date.now();
const build = await this.buildCacheEntry(envDir, cacheRoot, packages, selectedBase, pendingCreation);
const build = await this.buildCacheEntry(
envDir,
cacheRoot,
packages,
selectedBase,
pendingCreation,
scriptUri,
);
if (build.retainLock) {
try {
await lock.retain();
Expand Down Expand Up @@ -2738,6 +2768,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
this.api,
this,
this.baseManager,
'inlineScript',
);
if (!environment) {
return { kind: 'uncertain' };
Expand Down Expand Up @@ -2784,6 +2815,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
packages: ReadonlyArray<string>,
selectedBase: SelectedBaseInterpreter,
pendingCreation: PendingCreationContext,
scriptUri: Uri,
): Promise<BuildCacheEntryResult> {
let result;
try {
Expand All @@ -2797,6 +2829,10 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
envDir.fsPath,
{ install: [...packages], uninstall: [] },
false, // trackUvEnvironment
{
progressTitle: l10n.t('Setting up environment for {0}', path.basename(scriptUri.fsPath)),
nameStyle: 'inlineScript',
},
);
} catch (error) {
this.log.error(`Failed to build inline-script environment: ${getErrorMessage(error)}`);
Expand Down
Loading
Loading