From 86ff0da4384e365a109edcadf5aae8d00fa61eb9 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Jul 2026 15:38:07 -0700 Subject: [PATCH 1/6] Generate published API entry point from src/api.ts The npm package `@vscode/python-environments` previously duplicated the entire API definition in `api/src/main.ts`, a near-copy of `src/api.ts` that was hand-maintained and had drifted. Make `src/api.ts` the single source of truth by folding the publish-only runtime helper (`PythonEnvironments.api()` and `EXTENSION_ID`) into it, and stop committing `api/src/main.ts`. The publish pipeline now copies `src/api.ts` to `api/src/main.ts` before compiling the package. - src/api.ts: add the `PythonEnvironments.api()` helper and `EXTENSION_ID` - api/src/main.ts: remove (now generated); ignore via api/.gitignore - build/azure-pipeline.npm.yml: copy src/api.ts before compile - CONTRIBUTING.md: document the copy model Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 8 + api/.gitignore | 4 + api/src/main.ts | 1434 ---------------------------------- build/azure-pipeline.npm.yml | 4 + src/api.ts | 28 +- 5 files changed, 43 insertions(+), 1435 deletions(-) create mode 100644 api/.gitignore delete mode 100644 api/src/main.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 042243b9..a2a3c550 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,6 +89,14 @@ This project requires contributors to sign a Contributor License Agreement (CLA) This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions. +## Public API package (`@vscode/python-environments`) + +The npm package under [`api/`](./api) is the public API facade other extensions consume. Its entry point, `api/src/main.ts`, is a **copy** of [`src/api.ts`](./src/api.ts) — the single source of truth — and is **not committed** (see [`api/.gitignore`](./api/.gitignore)). + +- Edit the API only in `src/api.ts`. This file contains the full public surface, including the runtime `PythonEnvironments.api()` helper and `EXTENSION_ID`. `api/src/main.ts` is a build artifact — never edit or commit it. +- `api/src/main.ts` is produced by the publish pipeline ([`build/azure-pipeline.npm.yml`](./build/azure-pipeline.npm.yml)), which copies `src/api.ts` to `api/src/main.ts` before compiling. The api package is therefore built in CI only; to build it locally, copy the file first (e.g. `cp src/api.ts api/src/main.ts`). +- `src/api.ts` itself is validated on every PR by the extension's own lint and TypeScript compile. + ## Questions or Issues? - **Questions**: Start a [discussion](https://github.com/microsoft/vscode-python/discussions/categories/q-a) diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 00000000..74d92889 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,4 @@ +# Copied from ../src/api.ts by the publish pipeline (build/azure-pipeline.npm.yml). +# This is the published package entry point; it is produced at publish time and +# intentionally NOT committed. src/api.ts is the single source of truth. +src/main.ts diff --git a/api/src/main.ts b/api/src/main.ts deleted file mode 100644 index 20a69eaa..00000000 --- a/api/src/main.ts +++ /dev/null @@ -1,1434 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import type { Pep440Version } from '@renovatebot/pep440'; -import { - Disposable, - Event, - FileChangeType, - LogOutputChannel, - MarkdownString, - RelativePattern, - TaskExecution, - Terminal, - TerminalOptions, - ThemeIcon, - Uri, - extensions, -} from 'vscode'; - -/* - * Do not introduce any breaking changes to this API. - * This is the public API for other extensions to interact with the Python Environments extension. - */ - -export type { Pep440Version } from '@renovatebot/pep440'; -/** - * The path to an icon, or a theme-specific configuration of icons. - */ -export type IconPath = - | Uri - | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - } - | ThemeIcon; - -/** - * Options for executing a Python executable. - */ -export interface PythonCommandRunConfiguration { - /** - * Path to the binary like `python.exe` or `python3` to execute. This should be an absolute path - * to an executable that can be spawned. - */ - executable: string; - - /** - * Arguments to pass to the python executable. These arguments will be passed on all execute calls. - * This is intended for cases where you might want to do interpreter specific flags. - */ - args?: string[]; -} - -/** - * Contains details on how to use a particular python environment - * - * Running In Terminal: - * 1. If {@link PythonEnvironmentExecutionInfo.activatedRun} is provided, then that will be used. - * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: - * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. - * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: - * - 'unknown' will be used if provided. - * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. - * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. - * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. - * - * Creating a Terminal: - * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. - * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. - * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: - * - 'unknown' will be used if provided. - * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. - * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. - * - */ -export interface PythonEnvironmentExecutionInfo { - /** - * Details on how to run the python executable. - */ - run: PythonCommandRunConfiguration; - - /** - * Details on how to run the python executable after activating the environment. - * If set this will overrides the {@link PythonEnvironmentExecutionInfo.run} command. - */ - activatedRun?: PythonCommandRunConfiguration; - - /** - * Details on how to activate an environment. - */ - activation?: PythonCommandRunConfiguration[]; - - /** - * Details on how to activate an environment using a shell specific command. - * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. - * 'unknown' is used if shell type is not known. - * If 'unknown' is not provided and shell type is not known then - * {@link PythonEnvironmentExecutionInfo.activation} if set. - */ - shellActivation?: Map; - - /** - * Details on how to deactivate an environment. - */ - deactivation?: PythonCommandRunConfiguration[]; - - /** - * Details on how to deactivate an environment using a shell specific command. - * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. - * 'unknown' is used if shell type is not known. - * If 'unknown' is not provided and shell type is not known then - * {@link PythonEnvironmentExecutionInfo.deactivation} if set. - */ - shellDeactivation?: Map; -} - -/** - * Interface representing the ID of a Python environment. - */ -export interface PythonEnvironmentId { - /** - * The unique identifier of the Python environment. - */ - id: string; - - /** - * The ID of the manager responsible for the Python environment. - */ - managerId: string; -} - -/** - * Display information for an environment group. - */ -export interface EnvironmentGroupInfo { - /** - * The name of the environment group. This is used as an identifier for the group. - * - * Note: The first instance of the group with the given name will be used in the UI. - */ - readonly name: string; - - /** - * The description of the environment group. - */ - readonly description?: string; - - /** - * The tooltip for the environment group, which can be a string or a Markdown string. - */ - readonly tooltip?: string | MarkdownString; - - /** - * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. - */ - readonly iconPath?: IconPath; -} - -/** - * Interface representing information about a Python environment. - */ -export interface PythonEnvironmentInfo { - /** - * The name of the Python environment. - */ - readonly name: string; - - /** - * The display name of the Python environment. - */ - readonly displayName: string; - - /** - * The short display name of the Python environment. - */ - readonly shortDisplayName?: string; - - /** - * The display path of the Python environment. - */ - readonly displayPath: string; - - /** - * The version of the Python environment. - */ - readonly version: string; - - /** - * Path to the python binary or environment folder. - */ - readonly environmentPath: Uri; - - /** - * The description of the Python environment. - */ - readonly description?: string; - - /** - * The tooltip for the Python environment, which can be a string or a Markdown string. - */ - readonly tooltip?: string | MarkdownString; - - /** - * The icon path for the Python environment, which can be a string, Uri, or an object with light and dark theme paths. - */ - readonly iconPath?: IconPath; - - /** - * Information on how to execute the Python environment. This is required for executing Python code in the environment. - */ - readonly execInfo: PythonEnvironmentExecutionInfo; - - /** - * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. - * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. - */ - readonly sysPrefix: string; - - /** - * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. - */ - readonly group?: string | EnvironmentGroupInfo; - - /** - * Error message if the environment is broken or invalid. - * When set, indicates this environment has issues (e.g., broken symlinks, missing Python executable). - * The UI should display a warning indicator and show this message to help users diagnose and fix the issue. - */ - readonly error?: string; -} - -/** - * Interface representing a Python environment. - */ -export interface PythonEnvironment extends PythonEnvironmentInfo { - /** - * The ID of the Python environment. - */ - readonly envId: PythonEnvironmentId; -} - -/** - * Type representing the scope for setting a Python environment. - * Can be undefined or a URI. - */ -export type SetEnvironmentScope = undefined | Uri | Uri[]; - -/** - * Type representing the scope for getting a Python environment. - * Can be undefined or a URI. - */ -export type GetEnvironmentScope = undefined | Uri; - -/** - * Type representing the scope for creating a Python environment. - * Can be a Python project or 'global'. - */ -export type CreateEnvironmentScope = Uri | Uri[] | 'global'; -/** - * The scope for which environments are to be refreshed. - * - `undefined`: Search for environments globally and workspaces. - * - {@link Uri}: Environments in the workspace/folder or associated with the Uri. - */ -export type RefreshEnvironmentsScope = Uri | undefined; - -/** - * The scope for which environments are required. - * - `"all"`: All environments. - * - `"global"`: Python installations that are usually a base for creating virtual environments. - * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. - */ -export type GetEnvironmentsScope = Uri | 'all' | 'global'; - -/** - * Event arguments for when the current Python environment changes. - */ -export type DidChangeEnvironmentEventArgs = { - /** - * The URI of the environment that changed. - */ - readonly uri: Uri | undefined; - - /** - * The old Python environment before the change. - */ - readonly old: PythonEnvironment | undefined; - - /** - * The new Python environment after the change. - */ - readonly new: PythonEnvironment | undefined; -}; - -/** - * Enum representing the kinds of environment changes. - */ -export enum EnvironmentChangeKind { - /** - * Indicates that an environment was added. - */ - add = 'add', - - /** - * Indicates that an environment was removed. - */ - remove = 'remove', -} - -/** - * Event arguments for when the list of Python environments changes. - */ -export type DidChangeEnvironmentsEventArgs = { - /** - * The kind of change that occurred (add or remove). - */ - kind: EnvironmentChangeKind; - - /** - * The Python environment that was added or removed. - */ - environment: PythonEnvironment; -}[]; - -/** - * Type representing the context for resolving a Python environment. - */ -export type ResolveEnvironmentContext = Uri; - -export interface QuickCreateConfig { - /** - * The description of the quick create step. - */ - readonly description: string; - - /** - * The detail of the quick create step. - */ - readonly detail?: string; -} - -/** - * Interface representing an environment manager. - * - * @remarks - * Methods on this interface are invoked both by the Python Environments extension itself - * (in response to UI actions, startup, terminal activation, script execution, and so on) - * and directly by other extensions that consume the published API. Any "called when…" - * notes on individual methods list representative triggers only — they are not - * exhaustive, and the precise set of call sites may evolve over time. Implementations - * should focus on the documented contract rather than any specific caller. - */ -export interface EnvironmentManager { - /** - * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). - */ - readonly name: string; - - /** - * The display name of the environment manager. - */ - readonly displayName?: string; - - /** - * The preferred package manager ID for the environment manager. This is a combination - * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. - * `.:` - * - * @example - * 'ms-python.python:pip' - */ - readonly preferredPackageManagerId: string; - - /** - * The description of the environment manager. - */ - readonly description?: string; - - /** - * The tooltip for the environment manager, which can be a string or a Markdown string. - */ - readonly tooltip?: string | MarkdownString | undefined; - - /** - * The icon path for the environment manager, which can be a string, Uri, or an object with light and dark theme paths. - */ - readonly iconPath?: IconPath; - - /** - * The log output channel for the environment manager. - */ - readonly log?: LogOutputChannel; - - /** - * The quick create details for the environment manager. Having this method also enables the quick create feature - * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. - */ - quickCreateConfig?(): QuickCreateConfig | undefined; - - /** - * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. If a manager does not support environment creation, do not implement this method; the UI disables "create" options when `this.manager.create === undefined`. - * @param scope - The scope within which to create the environment. - * @param options - Optional parameters for creating the Python environment. - * @returns A promise that resolves to the created Python environment, or undefined if creation failed. - * - * @remarks - * Invoked when an environment of this manager's type should be created for the given - * scope. Typical triggers include user-initiated environment-creation flows and - * programmatic creation via the API. - */ - create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; - - /** - * Removes the specified Python environment. - * @param environment - The Python environment to remove. - * @returns A promise that resolves when the environment is removed. - * - * @remarks - * Invoked to delete the given environment. Typical triggers include an explicit user - * action (such as a "Delete Environment" command) and programmatic removal via the API. - */ - remove?(environment: PythonEnvironment): Promise; - - /** - * Refreshes the list of Python environments within the specified scope. - * @param scope - The scope within which to refresh environments. - * @returns A promise that resolves when the refresh is complete. - * - * @remarks - * Forces the manager to re-discover environments for the given scope. Typically - * triggered by an explicit user "refresh" action. - */ - refresh(scope: RefreshEnvironmentsScope): Promise; - - /** - * Retrieves a list of Python environments within the specified scope. - * @param scope - The scope within which to retrieve environments. - * @returns A promise that resolves to an array of Python environments. - * - * @remarks - * Returns the environments known to this manager for the given scope. Called - * frequently by UI surfaces (tree views, pickers) and by other consumers of the API. - */ - getEnvironments(scope: GetEnvironmentsScope): Promise; - - /** - * Event that is fired when the list of Python environments changes. - */ - onDidChangeEnvironments?: Event; - - /** - * Sets the current Python environment within the specified scope. - * @param scope - The scope within which to set the environment. - * @param environment - The Python environment to set. If undefined, the environment is unset. - * @returns A promise that resolves when the environment is set. - * - * @remarks - * Invoked when the active environment for the given scope should change — for example - * after the user selects an environment in a picker, after a newly created environment - * is auto-selected, or programmatically via the API. - * - * Also invoked at extension startup to rehydrate the active environment from - * persisted state. - */ - set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; - - /** - * Retrieves the current Python environment within the specified scope. - * @param scope - The scope within which to retrieve the environment. - * @returns A promise that resolves to the current Python environment, or undefined if none is set. - * - * @remarks - * Returns the currently active environment for the given scope, or `undefined` if - * none is selected. Called very frequently — at startup, after {@link set}, when a - * terminal is opened, before running Python, by UI surfaces that display the active - * interpreter, and by other extensions consuming the API. - */ - get(scope: GetEnvironmentScope): Promise; - - /** - * Event that is fired when the current Python environment changes. - */ - onDidChangeEnvironment?: Event; - - /** - * Resolves the specified Python environment. The environment can be either a {@link PythonEnvironment} or a {@link Uri} context. - * - * This method is used to obtain a fully detailed {@link PythonEnvironment} object. The input can be: - * - A {@link PythonEnvironment} object, which might be missing key details such as {@link PythonEnvironment.execInfo}. - * - A {@link Uri} object, which typically represents either: - * - A folder that contains the Python environment. - * - The path to a Python executable. - * - * @param context - The context for resolving the environment, which can be a {@link PythonEnvironment} or a {@link Uri}. - * @returns A promise that resolves to the fully detailed {@link PythonEnvironment}, or `undefined` if the environment cannot be resolved. - * - * @remarks - * Called to turn a lightly-populated {@link PythonEnvironment} or a {@link Uri} - * pointing at an interpreter or environment folder into a fully-populated - * {@link PythonEnvironment} with complete {@link PythonEnvironment.execInfo}. Typical - * triggers include the user manually selecting an interpreter path, resolving - * `python.defaultInterpreterPath` at startup, and populating execution details before - * launching Python. - */ - resolve(context: ResolveEnvironmentContext): Promise; - - /** - * Clears the environment manager's cache. - * - * @returns A promise that resolves when the cache is cleared. - * - * @remarks - * Drops any cached environment data held by the manager so that subsequent calls to - * {@link EnvironmentManager.getEnvironments} or {@link EnvironmentManager.get} - * re-discover state from disk. Typically triggered by an explicit user "clear cache" - * action. - */ - clearCache?(): Promise; -} - -/** - * Interface representing a package ID. - */ -export interface PackageId { - /** - * The ID of the package. - */ - id: string; - - /** - * The ID of the package manager. - */ - managerId: string; - - /** - * The ID of the environment in which the package is installed. - */ - environmentId: string; -} - -/** - * Interface representing package information. - */ -export interface PackageInfo { - /** - * The name of the package. - */ - readonly name: string; - - /** - * The display name of the package. - */ - readonly displayName: string; - - /** - * The version of the package. - */ - readonly version?: string; - - /** - * The description of the package. - */ - readonly description?: string; - - /** - * The tooltip for the package, which can be a string or a Markdown string. - */ - readonly tooltip?: string | MarkdownString | undefined; - - /** - * The icon path for the package, which can be a string, Uri, or an object with light and dark theme paths. - */ - readonly iconPath?: IconPath; - - /** - * The URIs associated with the package. - */ - readonly uris?: readonly Uri[]; - - /** - * Whether the package is a transitive dependency. - */ - readonly isTransitive?: boolean; -} - -/** - * Interface representing a package. - */ -export interface Package extends PackageInfo { - /** - * The ID of the package. - */ - readonly pkgId: PackageId; -} - -/** - * Enum representing the kinds of package changes. - */ -export enum PackageChangeKind { - /** - * Indicates that a package was added. - */ - add = 'add', - - /** - * Indicates that a package was removed. - */ - remove = 'remove', -} - -/** - * Event arguments for when packages change. - */ -export interface DidChangePackagesEventArgs { - /** - * The Python environment in which the packages changed. - */ - environment: PythonEnvironment; - - /** - * The package manager responsible for the changes. - */ - manager: PackageManager; - - /** - * The list of changes, each containing the kind of change and the package affected. - */ - changes: { kind: PackageChangeKind; pkg: Package }[]; -} - -/** - * Interface representing a package manager. - */ -export interface PackageManager { - /** - * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). - */ - name: string; - - /** - * The display name of the package manager. - */ - displayName?: string; - - /** - * The description of the package manager. - */ - description?: string; - - /** - * The tooltip for the package manager, which can be a string or a Markdown string. - */ - tooltip?: string | MarkdownString | undefined; - - /** - * The icon path for the package manager, which can be a string, Uri, or an object with light and dark theme paths. - */ - iconPath?: IconPath; - - /** - * The log output channel for the package manager. - */ - log?: LogOutputChannel; - - /** - * Installs/Uninstall packages in the specified Python environment. - * @param environment - The Python environment in which to install packages. - * @param options - Options for managing packages. - * @returns A promise that resolves when the installation is complete. - */ - manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; - - /** - * Refreshes the package list for the specified Python environment. - * @param environment - The Python environment for which to refresh the package list. - * @returns A promise that resolves with the refreshed list of packages, or undefined. - */ - refresh(environment: PythonEnvironment): Promise; - - /** - * Retrieves the list of packages for the specified Python environment. - * @param environment - The Python environment for which to retrieve packages. - * @param options - Optional settings for package retrieval. - * @returns An array of packages, or undefined if the packages could not be retrieved. - */ - getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise; - - /** - * Returns additional filesystem patterns to watch for package install/uninstall changes. - * - * These patterns are appended to the default site-packages metadata locations. - * Implement this for manager-specific locations (for example, conda-meta). - * - * @param environment - The Python environment whose package paths should be watched. - * @returns Relative patterns to watch for package changes. - */ - getPackageWatchTargets?(environment: PythonEnvironment): RelativePattern[]; - /** - * Event that is fired when packages change. - */ - onDidChangePackages?: Event; - - /** - * Fetches the names of direct (non-transitive) packages for the specified Python environment. - * - * **Caveat:** Most package managers cannot track user install intent. For pip, this uses - * `pip list --not-required` which returns packages with no installed dependents (leaf packages), - * not necessarily packages the user explicitly installed. For example, if a user runs - * `pip install flask werkzeug`, werkzeug will still be reported as transitive because flask - * depends on it. This is a best-effort approximation. - * - * @param environment - The Python environment for which to fetch direct package names. - * @returns A promise that resolves to a set of package name strings, or undefined if not supported. - */ - getDirectPackageNames?(environment: PythonEnvironment): Promise | undefined>; - - /** - * Clears the package manager's cache. - * @returns A promise that resolves when the cache is cleared. - */ - clearCache?(): Promise; - - /** - * Returns the version of the underlying package management tool (e.g., pip, uv, conda). - * @param environment - The Python environment context. - * @returns A promise that resolves to a {@link Pep440Version} object, or `undefined` if not available. - */ - getVersion?(environment: PythonEnvironment): Promise; - - /** - * Retrieves the list of available versions for a given package. - * @param environment - The Python environment context for the lookup. - * @param packageName - The name of the package to look up. - * @returns A promise that resolves to an array of {@link Pep440Version} objects (newest first), - * or `undefined` if this manager does not support version listing. - */ - getPackageAvailableVersions?( - environment: PythonEnvironment, - packageName: string, - ): Promise; - - /** - * Formats a versioned install specification for this package manager. - * - * Different package managers use different syntax (e.g. pip uses `name==version`, - * conda uses `name=version`). Implement this method to return the correct format. - * When absent, callers should default to `name==version`. - * - * @param packageName - The name of the package. - * @param version - The version string. - * @returns The install specification string (e.g. `"requests==2.31.0"` or `"requests=2.31.0"`). - */ - formatInstallSpec?(packageName: string, version: string): string; -} - -/** - * Interface representing a Python project. - */ -export interface PythonProject { - /** - * The name of the Python project. - */ - readonly name: string; - - /** - * The URI of the Python project. - */ - readonly uri: Uri; - - /** - * The description of the Python project. - */ - readonly description?: string; - - /** - * The tooltip for the Python project, which can be a string or a Markdown string. - */ - readonly tooltip?: string | MarkdownString; -} - -/** - * Options for creating a Python project. - */ -export interface PythonProjectCreatorOptions { - /** - * The name of the Python project. - */ - name: string; - - /** - * Path provided as the root for the project. - */ - rootUri: Uri; - - /** - * Boolean indicating whether the project should be created without any user input. - */ - quickCreate?: boolean; -} - -/** - * Interface representing a creator for Python projects. - */ -export interface PythonProjectCreator { - /** - * The name of the Python project creator. - */ - readonly name: string; - - /** - * The display name of the Python project creator. - */ - readonly displayName?: string; - - /** - * The description of the Python project creator. - */ - readonly description?: string; - - /** - * The tooltip for the Python project creator, which can be a string or a Markdown string. - */ - readonly tooltip?: string | MarkdownString; - - /** - * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. - * Anything that needs its own python environment constitutes a project. - * @param options Optional parameters for creating the Python project. - * @returns A promise that resolves to one of the following: - * - PythonProject or PythonProject[]: when a single or multiple projects are created. - * - Uri or Uri[]: when files are created that do not constitute a project. - * - undefined: if project creation fails. - */ - create(options?: PythonProjectCreatorOptions): Promise; - - /** - * A flag indicating whether the project creator supports quick create where no user input is required. - */ - readonly supportsQuickCreate?: boolean; -} - -/** - * Event arguments for when Python projects change. - */ -export interface DidChangePythonProjectsEventArgs { - /** - * The list of Python projects that were added. - */ - added: PythonProject[]; - - /** - * The list of Python projects that were removed. - */ - removed: PythonProject[]; -} - -/** - * Options for retrieving packages from a package manager. - */ -export interface GetPackagesOptions { - /** - * When `true`, bypasses the cache and fetches the latest packages from the underlying tool. - * Defaults to `false`. - */ - skipCache?: boolean; -} - -export type PackageManagementOptions = - | { - /** - * Upgrade the packages if they are already installed. - */ - upgrade?: boolean; - - /** - * Show option to skip package installation or uninstallation. - */ - showSkipOption?: boolean; - /** - * The list of packages to install. - */ - install: string[]; - - /** - * The list of packages to uninstall. - */ - uninstall?: string[]; - } - | { - /** - * Upgrade the packages if they are already installed. - */ - upgrade?: boolean; - - /** - * Show option to skip package installation or uninstallation. - */ - showSkipOption?: boolean; - /** - * The list of packages to install. - */ - install?: string[]; - - /** - * The list of packages to uninstall. - */ - uninstall: string[]; - }; - -/** - * Options for creating a Python environment. - */ -export interface CreateEnvironmentOptions { - /** - * Provides some context about quick create based on user input. - * - if true, the environment should be created without any user input or prompts. - * - if false, the environment creation can show user input or prompts. - * This also means user explicitly skipped the quick create option. - * - if undefined, the environment creation can show user input or prompts. - * You can show quick create option to the user if you support it. - */ - quickCreate?: boolean; - /** - * Packages to install in addition to the automatically picked packages as a part of creating environment. - */ - additionalPackages?: string[]; -} - -/** - * Object representing the process started using run in background API. - */ -export interface PythonProcess { - /** - * The process ID of the Python process. - */ - readonly pid?: number; - - /** - * The standard input of the Python process. - */ - readonly stdin: NodeJS.WritableStream; - - /** - * The standard output of the Python process. - */ - readonly stdout: NodeJS.ReadableStream; - - /** - * The standard error of the Python process. - */ - readonly stderr: NodeJS.ReadableStream; - - /** - * Kills the Python process. - */ - kill(): void; - - /** - * Event that is fired when the Python process exits. - */ - onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; -} - -export interface PythonEnvironmentManagerRegistrationApi { - /** - * Register an environment manager implementation. - * - * @param manager Environment Manager implementation to register. - * @param options Optional registration options. - * @param options.extensionId The extension ID of the calling extension. When this is not specified, - * or when the specified extension cannot be found, the extension ID will be automatically detected. - * @returns A disposable that can be used to unregister the environment manager. - * @see {@link EnvironmentManager} - */ - registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable; -} - -export interface PythonEnvironmentItemApi { - /** - * Create a Python environment item from the provided environment info. This item is used to interact - * with the environment. - * - * @param info Some details about the environment like name, version, etc. needed to interact with the environment. - * @param manager The environment manager to associate with the environment. - * @returns The Python environment. - */ - createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; -} - -export interface PythonEnvironmentManagementApi { - /** - * Create a Python environment using environment manager associated with the scope. - * - * @param scope Where the environment is to be created. - * @param options Optional parameters for creating the Python environment. - * @returns The Python environment created. `undefined` if not created. - */ - createEnvironment( - scope: CreateEnvironmentScope, - options?: CreateEnvironmentOptions, - ): Promise; - - /** - * Remove a Python environment. - * - * @param environment The Python environment to remove. - * @returns A promise that resolves when the environment has been removed. - */ - removeEnvironment(environment: PythonEnvironment): Promise; -} - -export interface PythonEnvironmentsApi { - /** - * Initiates a refresh of Python environments within the specified scope. - * @param scope - The scope within which to search for environments. - * @returns A promise that resolves when the search is complete. - */ - refreshEnvironments(scope: RefreshEnvironmentsScope): Promise; - - /** - * Retrieves a list of Python environments within the specified scope. - * @param scope - The scope within which to retrieve environments. - * @returns A promise that resolves to an array of Python environments. - */ - getEnvironments(scope: GetEnvironmentsScope): Promise; - - /** - * Event that is fired when the list of Python environments changes. - * @see {@link DidChangeEnvironmentsEventArgs} - */ - onDidChangeEnvironments: Event; - - /** - * This method is used to get the details missing from a PythonEnvironment. Like - * {@link PythonEnvironment.execInfo} and other details. - * - * @param context : The PythonEnvironment or Uri for which details are required. - */ - resolveEnvironment(context: ResolveEnvironmentContext): Promise; -} - -export interface PythonProjectEnvironmentApi { - /** - * Sets the current Python environment within the specified scope. - * @param scope - The scope within which to set the environment. - * @param environment - The Python environment to set. If undefined, the environment is unset. - */ - setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; - - /** - * Retrieves the current Python environment within the specified scope. - * @param scope - The scope within which to retrieve the environment. - * @returns A promise that resolves to the current Python environment, or undefined if none is set. - */ - getEnvironment(scope: GetEnvironmentScope): Promise; - - /** - * Event that is fired when the selected Python environment changes for Project, Folder or File. - * @see {@link DidChangeEnvironmentEventArgs} - */ - onDidChangeEnvironment: Event; -} - -export interface PythonEnvironmentManagerApi - extends - PythonEnvironmentManagerRegistrationApi, - PythonEnvironmentItemApi, - PythonEnvironmentManagementApi, - PythonEnvironmentsApi, - PythonProjectEnvironmentApi {} - -export interface PythonPackageManagerRegistrationApi { - /** - * Register a package manager implementation. - * - * @param manager Package Manager implementation to register. - * @param options Optional registration options. - * @param options.extensionId The extension ID of the calling extension. When this is not specified, - * or when the specified extension cannot be found, the extension ID will be automatically detected. - * @returns A disposable that can be used to unregister the package manager. - * @see {@link PackageManager} - */ - registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable; -} - -export interface PythonPackageGetterApi { - /** - * Refresh the list of packages in a Python Environment. - * - * @param environment The Python Environment for which the list of packages is to be refreshed. - * @returns A promise that resolves with the refreshed list of packages, or undefined. - */ - refreshPackages(environment: PythonEnvironment): Promise; - - /** - * Get the list of packages in a Python Environment. - * - * @param environment The Python Environment for which the list of packages is required. - * @param options Optional settings for package retrieval. - * @returns The list of packages in the Python Environment. - */ - getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise; - - /** - * Event raised when the list of packages in a Python Environment changes. - * @see {@link DidChangePackagesEventArgs} - */ - onDidChangePackages: Event; -} - -export interface PythonPackageItemApi { - /** - * Create a package item from the provided package info. - * - * @param info The package info. - * @param environment The Python Environment in which the package is installed. - * @param manager The package manager that installed the package. - * @returns The package item. - */ - createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; -} - -export interface PythonPackageManagementApi { - /** - * Install/Uninstall packages into a Python Environment. - * - * @param environment The Python Environment into which packages are to be installed. - * @param packages The packages to install. - * @param options Options for installing packages. - */ - managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; -} - -export interface PythonPackageManagerApi - extends - PythonPackageManagerRegistrationApi, - PythonPackageGetterApi, - PythonPackageManagementApi, - PythonPackageItemApi {} - -export interface PythonProjectCreationApi { - /** - * Register a Python project creator. - * - * @param creator The project creator to register. - * @returns A disposable that can be used to unregister the project creator. - * @see {@link PythonProjectCreator} - */ - registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; -} -export interface PythonProjectGetterApi { - /** - * Get all python projects. - */ - getPythonProjects(): readonly PythonProject[]; - - /** - * Get the python project for a given URI. - * - * @param uri The URI of the project - * @returns The project or `undefined` if not found. - */ - getPythonProject(uri: Uri): PythonProject | undefined; -} - -export interface PythonProjectModifyApi { - /** - * Add a python project or projects to the list of projects. - * - * @param projects The project or projects to add. - */ - addPythonProject(projects: PythonProject | PythonProject[]): void; - - /** - * Remove a python project from the list of projects. - * - * @param project The project to remove. - */ - removePythonProject(project: PythonProject): void; - - /** - * Event raised when python projects are added or removed. - * @see {@link DidChangePythonProjectsEventArgs} - */ - onDidChangePythonProjects: Event; -} - -/** - * The API for interacting with Python projects. A project in python is any folder or file that is a contained - * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, - * or just python files can be treated as a project. All this allows you to do is set a python environment for that project. - * - * By default all `vscode.workspace.workspaceFolders` are treated as projects. - */ -export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} - -export interface PythonTerminalCreateOptions extends TerminalOptions { - /** - * Whether to disable activation on create. - */ - disableActivation?: boolean; -} - -export interface PythonTerminalCreateApi { - /** - * Creates a terminal and activates any (activatable) environment for the terminal. - * - * @param environment The Python environment to activate. - * @param options Options for creating the terminal. - * - * Note: Non-activatable environments have no effect on the terminal. - */ - createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; -} - -/** - * Options for running a Python script or module in a terminal. - * - * Example: - * * Running Script: `python myscript.py --arg1` - * ```typescript - * { - * args: ["myscript.py", "--arg1"] - * } - * ``` - * * Running a module: `python -m my_module --arg1` - * ```typescript - * { - * args: ["-m", "my_module", "--arg1"] - * } - * ``` - */ -export interface PythonTerminalExecutionOptions { - /** - * Current working directory for the terminal. This in only used to create the terminal. - */ - cwd: string | Uri; - - /** - * Arguments to pass to the python executable. - */ - args?: string[]; - - /** - * Set `true` to show the terminal. - */ - show?: boolean; -} - -export interface PythonTerminalRunApi { - /** - * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. - * If a terminal is available, it will be used to run the script or module. - * - * Note: - * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. - * - If you close the terminal, this will create a new terminal. - * - In cases of multi-root/project scenario, it will create a separate terminal for each project. - */ - runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; - - /** - * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. - * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, - * and selected based on the `terminalKey`. - * - * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. - */ - runInDedicatedTerminal( - terminalKey: Uri | string, - environment: PythonEnvironment, - options: PythonTerminalExecutionOptions, - ): Promise; -} - -/** - * Options for running a Python task. - * - * Example: - * * Running Script: `python myscript.py --arg1` - * ```typescript - * { - * args: ["myscript.py", "--arg1"] - * } - * ``` - * * Running a module: `python -m my_module --arg1` - * ```typescript - * { - * args: ["-m", "my_module", "--arg1"] - * } - * ``` - */ -export interface PythonTaskExecutionOptions { - /** - * Name of the task to run. - */ - name: string; - - /** - * Arguments to pass to the python executable. - */ - args: string[]; - - /** - * The Python project to use for the task. - */ - project?: PythonProject; - - /** - * Current working directory for the task. Default is the project directory for the script being run. - */ - cwd?: string; - - /** - * Environment variables to set for the task. - */ - env?: { [key: string]: string }; -} - -export interface PythonTaskRunApi { - /** - * Run a Python script or module as a task. - * - */ - runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; -} - -/** - * Options for running a Python script or module in the background. - */ -export interface PythonBackgroundRunOptions { - /** - * The Python environment to use for running the script or module. - */ - args: string[]; - - /** - * Current working directory for the script or module. Default is the project directory for the script being run. - */ - cwd?: string; - - /** - * Environment variables to set for the script or module. - */ - env?: { [key: string]: string | undefined }; -} -export interface PythonBackgroundRunApi { - /** - * Run a Python script or module in the background. This API will create a new process to run the script or module. - */ - runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; -} - -export interface PythonExecutionApi - extends PythonTerminalCreateApi, PythonTerminalRunApi, PythonTaskRunApi, PythonBackgroundRunApi {} - -/** - * Event arguments for when the monitored `.env` files or any other sources change. - */ -export interface DidChangeEnvironmentVariablesEventArgs { - /** - * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. - */ - uri?: Uri; - - /** - * The type of change that occurred. - */ - changeType: FileChangeType; -} - -export interface PythonEnvironmentVariablesApi { - /** - * Get environment variables for a workspace. This picks up `.env` file from the root of the - * workspace. - * - * Order of overrides: - * 1. `baseEnvVar` if given or `process.env` - * 2. `.env` file from the "python.envFile" setting in the workspace. - * 3. `.env` file at the root of the python project. - * 4. `overrides` in the order provided. - * - * @param uri The URI of the project, workspace or a file in a for which environment variables are required.If not provided, - * it fetches the environment variables for the global scope. - * @param overrides Additional environment variables to override the defaults. - * @param baseEnvVar The base environment variables that should be used as a starting point. - */ - getEnvironmentVariables( - uri: Uri | undefined, - overrides?: ({ [key: string]: string | undefined } | Uri)[], - baseEnvVar?: { [key: string]: string | undefined }, - ): Promise<{ [key: string]: string | undefined }>; - - /** - * Event raised when `.env` file changes or any other monitored source of env variable changes. - */ - onDidChangeEnvironmentVariables: Event; -} - -/** - * The API for interacting with Python environments, package managers, and projects. - */ -export interface PythonEnvironmentApi - extends - PythonEnvironmentManagerApi, - PythonPackageManagerApi, - PythonProjectApi, - PythonExecutionApi, - PythonEnvironmentVariablesApi {} - -export const EXTENSION_ID = 'ms-python.vscode-python-envs'; - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace PythonEnvironments { - /** - * Returns the API exposed by the Python Environments extension in VS Code. - */ - export async function api(): Promise { - const extension = extensions.getExtension(EXTENSION_ID); - if (extension === undefined) { - throw new Error(`Python Environments extension is not installed or is disabled`); - } - if (!extension.isActive) { - await extension.activate(); - } - const pythonEnvsApi: PythonEnvironmentApi = extension.exports; - return pythonEnvsApi; - } -} diff --git a/build/azure-pipeline.npm.yml b/build/azure-pipeline.npm.yml index 9237ab25..5c86a79f 100644 --- a/build/azure-pipeline.npm.yml +++ b/build/azure-pipeline.npm.yml @@ -75,6 +75,10 @@ extends: workingDirectory: $(Build.SourcesDirectory)/api displayName: Install package dependencies + - script: cp ../src/api.ts src/main.ts + workingDirectory: $(Build.SourcesDirectory)/api + displayName: Copy src/api.ts to API package entry point + - script: npm run compile workingDirectory: $(Build.SourcesDirectory)/api displayName: Compile TypeScript diff --git a/src/api.ts b/src/api.ts index 1a9155b5..26a8c003 100644 --- a/src/api.ts +++ b/src/api.ts @@ -15,9 +15,15 @@ import type { ThemeIcon, Uri, } from 'vscode'; +import { extensions } from 'vscode'; export type { Pep440Version } from '@renovatebot/pep440'; +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with the Python Environments extension. + */ + /** * The path to an icon, or a theme-specific configuration of icons. */ @@ -722,7 +728,8 @@ export interface PackageManager { clearCache?(): Promise; /** - * Returns the version of the underlying package management tool (e.g., pip, conda). + * Returns the version of the underlying package management tool (e.g., pip, uv, conda). + * @param environment - The Python environment context. * @returns A promise that resolves to a {@link Pep440Version} object, or `undefined` if not available. */ getVersion?(environment: PythonEnvironment): Promise; @@ -1407,3 +1414,22 @@ export interface PythonEnvironmentApi PythonProjectApi, PythonExecutionApi, PythonEnvironmentVariablesApi {} + +export const EXTENSION_ID = 'ms-python.vscode-python-envs'; + +export namespace PythonEnvironments { + /** + * Returns the API exposed by the Python Environments extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python Environments extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonEnvsApi: PythonEnvironmentApi = extension.exports; + return pythonEnvsApi; + } +} From 9a16a1bccc72e7cdeeb46ea44736da09e7fa4bcd Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Jul 2026 16:05:25 -0700 Subject: [PATCH 2/6] Enforce matching extension and API package versions Add scripts/compare_package_versions.py and a CI job to verify the extension (package.json) and published API package (api/package.json) share the same version. Bump api package to 1.37.0 to match. Add a check-for-changed-files gate requiring an api/package.json version bump when src/api.ts changes (bypass with the 'skip api version' label). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 25 ++++++++++ CONTRIBUTING.md | 1 + api/package-lock.json | 4 +- api/package.json | 2 +- scripts/compare_package_versions.py | 75 +++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 scripts/compare_package_versions.py diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index daa7f50c..58a5951e 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -41,3 +41,28 @@ jobs: src/**/*.unit.test.ts skip-label: 'skip tests' failure-message: 'TypeScript code was edited without also editing a test file (the ${skip-label} label can be used to pass this check)' + + - name: 'Public API changes require a version bump' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: 'src/api.ts' + file-pattern: 'api/package.json' + skip-label: 'skip api version' + failure-message: 'The public API (${prereq-pattern}) was changed without bumping the package version in ${file-pattern} (the ${skip-label} label can be used to pass this check)' + + version-match: + name: 'Extension and API package versions match' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: 'Compare package.json versions' + run: python scripts/compare_package_versions.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2a3c550..c8f749a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,6 +96,7 @@ The npm package under [`api/`](./api) is the public API facade other extensions - Edit the API only in `src/api.ts`. This file contains the full public surface, including the runtime `PythonEnvironments.api()` helper and `EXTENSION_ID`. `api/src/main.ts` is a build artifact — never edit or commit it. - `api/src/main.ts` is produced by the publish pipeline ([`build/azure-pipeline.npm.yml`](./build/azure-pipeline.npm.yml)), which copies `src/api.ts` to `api/src/main.ts` before compiling. The api package is therefore built in CI only; to build it locally, copy the file first (e.g. `cp src/api.ts api/src/main.ts`). - `src/api.ts` itself is validated on every PR by the extension's own lint and TypeScript compile. +- **Versioning:** the published package version in [`api/package.json`](./api/package.json) must always match the extension version in [`package.json`](./package.json). CI enforces this via [`scripts/compare_package_versions.py`](./scripts/compare_package_versions.py). Additionally, any PR that edits `src/api.ts` must bump `api/package.json` (use the `skip api version` label to bypass). When bumping, update both files so they stay in sync. ## Questions or Issues? diff --git a/api/package-lock.json b/api/package-lock.json index f8d4912d..d74fb7e2 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-environments", - "version": "1.0.0", + "version": "1.37.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vscode/python-environments", - "version": "1.0.0", + "version": "1.37.0", "license": "MIT", "dependencies": { "@renovatebot/pep440": "^3.1.0" diff --git a/api/package.json b/api/package.json index 042d6409..00d4aa84 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-environments", "description": "An API facade for the Python Environments extension in VS Code", - "version": "1.0.0", + "version": "1.37.0", "author": { "name": "Microsoft Corporation" }, diff --git a/scripts/compare_package_versions.py b/scripts/compare_package_versions.py new file mode 100644 index 00000000..fdf0faf6 --- /dev/null +++ b/scripts/compare_package_versions.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Verify that the extension and the public API npm package share the same version. + +The root ``package.json`` (the VS Code extension) and ``api/package.json`` (the +published ``@vscode/python-environments`` npm package) must always declare the same +``version``. This keeps the published API package in lock-step with the extension +that implements it. + +Exits with status 0 when the versions match, and status 1 (printing an error) when +they differ. Intended to be run from CI, but can also be run locally: + + python scripts/compare_package_versions.py +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +EXTENSION_PACKAGE = REPO_ROOT / "package.json" +API_PACKAGE = REPO_ROOT / "api" / "package.json" + + +def read_version(package_json: Path) -> str: + """Return the ``version`` field from the given package.json file. + + Args: + package_json: Path to a ``package.json`` file. + + Returns: + The declared version string. + + Raises: + SystemExit: If the file is missing, unparseable, or has no ``version``. + """ + try: + data = json.loads(package_json.read_text(encoding="utf-8")) + except FileNotFoundError: + sys.exit(f"::error::{package_json} not found") + except json.JSONDecodeError as exc: + sys.exit(f"::error::Failed to parse {package_json}: {exc}") + + version = data.get("version") + if not isinstance(version, str) or not version: + sys.exit(f"::error::{package_json} is missing a 'version' field") + return version + + +def main() -> int: + """Compare the extension and API package versions. + + Returns: + 0 when the versions match, 1 when they differ. + """ + extension_version = read_version(EXTENSION_PACKAGE) + api_version = read_version(API_PACKAGE) + + print(f"Extension version (package.json): {extension_version}") + print(f"API package version (api/package.json): {api_version}") + + if extension_version != api_version: + print( + f"::error::Version mismatch: package.json is {extension_version} but " + f"api/package.json is {api_version}. Update api/package.json to match." + ) + return 1 + + print("Versions match.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 5d23f5b0fb2b16341f62b33addd4fa6bece0e10e Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Jul 2026 16:15:55 -0700 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/compare_package_versions.py | 2 +- src/api.ts | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/compare_package_versions.py b/scripts/compare_package_versions.py index fdf0faf6..855ac8ba 100644 --- a/scripts/compare_package_versions.py +++ b/scripts/compare_package_versions.py @@ -63,7 +63,7 @@ def main() -> int: if extension_version != api_version: print( f"::error::Version mismatch: package.json is {extension_version} but " - f"api/package.json is {api_version}. Update api/package.json to match." + f"api/package.json is {api_version}. Update package.json and/or api/package.json so both versions match." ) return 1 diff --git a/src/api.ts b/src/api.ts index 26a8c003..3a36c326 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1421,15 +1421,13 @@ export namespace PythonEnvironments { /** * Returns the API exposed by the Python Environments extension in VS Code. */ - export async function api(): Promise { - const extension = extensions.getExtension(EXTENSION_ID); - if (extension === undefined) { - throw new Error(`Python Environments extension is not installed or is disabled`); - } - if (!extension.isActive) { - await extension.activate(); - } - const pythonEnvsApi: PythonEnvironmentApi = extension.exports; - return pythonEnvsApi; +export async function api(): Promise { + const extension = extensions.getExtension(EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python Environments extension (${EXTENSION_ID}) is not installed or is disabled`); } + if (!extension.isActive) { + await extension.activate(); + } + return extension.exports; } From dcf5db0ac7a39cd193d170ed12d33fad8caece34 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Jul 2026 16:18:30 -0700 Subject: [PATCH 4/6] Fix missing closing brace for PythonEnvironments namespace Re-indent the api() function back inside the namespace and restore the namespace's closing brace that was dropped during a manual edit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/api.ts b/src/api.ts index 3a36c326..48f5789a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1421,13 +1421,14 @@ export namespace PythonEnvironments { /** * Returns the API exposed by the Python Environments extension in VS Code. */ -export async function api(): Promise { - const extension = extensions.getExtension(EXTENSION_ID); - if (extension === undefined) { - throw new Error(`Python Environments extension (${EXTENSION_ID}) is not installed or is disabled`); + export async function api(): Promise { + const extension = extensions.getExtension(EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python Environments extension (${EXTENSION_ID}) is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + return extension.exports; } - if (!extension.isActive) { - await extension.activate(); - } - return extension.exports; } From 8b514f9c752a654b789dd7550323e0a109f03965 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 1 Jul 2026 16:26:05 -0700 Subject: [PATCH 5/6] Require an API changelog entry when the public API changes Add api/CHANGELOG.md for the published @vscode/python-environments package and a check-for-changed-files gate requiring a changelog entry whenever src/api.ts changes (bypass with the 'skip api changelog' label). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 8 ++++++++ CONTRIBUTING.md | 2 +- api/CHANGELOG.md | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 api/CHANGELOG.md diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 58a5951e..1450ee6f 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -50,6 +50,14 @@ jobs: skip-label: 'skip api version' failure-message: 'The public API (${prereq-pattern}) was changed without bumping the package version in ${file-pattern} (the ${skip-label} label can be used to pass this check)' + - name: 'Public API changes require a changelog entry' + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 + with: + prereq-pattern: 'src/api.ts' + file-pattern: 'api/CHANGELOG.md' + skip-label: 'skip api changelog' + failure-message: 'The public API (${prereq-pattern}) was changed without a changelog entry in ${file-pattern} (the ${skip-label} label can be used to pass this check)' + version-match: name: 'Extension and API package versions match' runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8f749a3..cf9baed3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ The npm package under [`api/`](./api) is the public API facade other extensions - Edit the API only in `src/api.ts`. This file contains the full public surface, including the runtime `PythonEnvironments.api()` helper and `EXTENSION_ID`. `api/src/main.ts` is a build artifact — never edit or commit it. - `api/src/main.ts` is produced by the publish pipeline ([`build/azure-pipeline.npm.yml`](./build/azure-pipeline.npm.yml)), which copies `src/api.ts` to `api/src/main.ts` before compiling. The api package is therefore built in CI only; to build it locally, copy the file first (e.g. `cp src/api.ts api/src/main.ts`). - `src/api.ts` itself is validated on every PR by the extension's own lint and TypeScript compile. -- **Versioning:** the published package version in [`api/package.json`](./api/package.json) must always match the extension version in [`package.json`](./package.json). CI enforces this via [`scripts/compare_package_versions.py`](./scripts/compare_package_versions.py). Additionally, any PR that edits `src/api.ts` must bump `api/package.json` (use the `skip api version` label to bypass). When bumping, update both files so they stay in sync. +- **Versioning:** the published package version in [`api/package.json`](./api/package.json) must always match the extension version in [`package.json`](./package.json). CI enforces this via [`scripts/compare_package_versions.py`](./scripts/compare_package_versions.py). Additionally, any PR that edits `src/api.ts` must bump `api/package.json` (use the `skip api version` label to bypass) and add an entry to [`api/CHANGELOG.md`](./api/CHANGELOG.md) (use the `skip api changelog` label to bypass). When bumping, update both `package.json` files so they stay in sync. ## Questions or Issues? diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md new file mode 100644 index 00000000..aedaa39a --- /dev/null +++ b/api/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to the `@vscode/python-environments` API package are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.37.0] + +- Aligned the API package version with the Python Environments extension version. From d537e80beb7348a5c5a49b1cc3c7b871d34b3cbc Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 2 Jul 2026 10:47:45 -0700 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/api.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api.ts b/src/api.ts index 48f5789a..17e18669 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1422,13 +1422,19 @@ export namespace PythonEnvironments { * Returns the API exposed by the Python Environments extension in VS Code. */ export async function api(): Promise { - const extension = extensions.getExtension(EXTENSION_ID); + const extension = extensions.getExtension(EXTENSION_ID); if (extension === undefined) { throw new Error(`Python Environments extension (${EXTENSION_ID}) is not installed or is disabled`); } if (!extension.isActive) { await extension.activate(); } - return extension.exports; + const api = extension.exports; + if (!api) { + throw new Error( + `Python Environments extension (${EXTENSION_ID}) did not expose its API. Ensure "python.useEnvironmentsExtension" is enabled and reload the window.`, + ); + } + return api; } }