Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ There are a few ways to add a Python Project from the Python Environments panel:

#### Create New Project from Template

The **Python Envs: Create New Project from Template** command simplifies the process of starting a new Python project by scaffolding it for you. Whether in a new workspace or an existing one, this command configures the environment and boilerplate file structure, so you don’t have to worry about the initial setup, and only the code you want to write. There are currently two project types supported:
The **Python Envs: Create New Project from Template** command simplifies the process of starting a new Python project by scaffolding it for you. Whether in a new workspace or an existing one, this command configures the environment and boilerplate file structure, so you don’t have to worry about the initial setup, and only the code you want to write. There are currently three project types supported:

- Package: A structured Python package with files like `__init__.py` and setup configurations.
- FastAPI: A FastAPI web API with `pyproject.toml`, an application module, tests, and a debug configuration that launches Uvicorn.
- Script: A simple project for standalone Python scripts, ideal for quick tasks or just to get you started.

## Command Reference
Expand Down
23 changes: 21 additions & 2 deletions docs/managing-python-projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,12 @@ Use this to scaffold a brand new Python project with the correct structure and f
2. Run **Python Envs: Create New Project from Template**.
3. Choose a template type:
- **Package**: A structured Python package with `pyproject.toml`, tests folder, and package directory
- **FastAPI**: A FastAPI web API with `pyproject.toml`, an application module, and tests
- **Script**: A simple standalone Python file using PEP 723 inline metadata
4. Enter a name for your project.
5. If you're creating a package, choose whether to create a virtual environment.
5. If you're creating a package or a FastAPI project, choose whether to create a virtual environment.

The extension creates the project structure and adds it to your workspace. For packages, it can also create a virtual environment.
The extension creates the project structure and adds it to your workspace. For packages and FastAPI projects, it can also create a virtual environment.

#### Package template structure

Expand All @@ -95,6 +96,24 @@ my_package_project/
└── __init__.py
```

#### FastAPI template structure

When you create a FastAPI project named `my_api`, the extension generates:

```
my_api_project/
├── pyproject.toml # Project metadata, FastAPI, and Uvicorn
├── dev-requirements.txt # Development dependencies, including pytest and httpx
├── my_api/ # Application package
│ ├── __init__.py
│ ├── main.py # FastAPI app
│ └── __main__.py # `python -m my_api` runs Uvicorn
└── tests/
└── test_my_api.py
```

The workspace `launch.json` gains a **Python: FastAPI (my_api)** configuration that debugs `uvicorn my_api.main:app`.

#### Script template

When you create a script, the extension generates a single `.py` file with PEP 723 inline script metadata, which allows you to specify dependencies directly in the file.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copilot Instructions for <package_name>

- The FastAPI project `<package_name>` is a Python Project located in the folder `<package_name>_project`.
- You need to call the `Get Python Environment Information` tool on the `<package_name>_project` path to get the Python executable details.
- Substitute the Python executable you get from the `Get Python Environment Information` tool anywhere you see `<python>` in these instructions.
- Run command for `<package_name>`: `<python> -m uvicorn <package_name>.main:app --reload`
- Command to run tests for `<package_name>`: `<python> -m pytest`
- To run an editable install for the package `<package_name>`, use the `Install Python Package` tool with the `<package_name>_project` path and arguments `['-e', '.']`.
- In the workspace `launch.json` file, configurations related to this project have the prefix `Python: FastAPI (<package_name>)`.
- The project `<package_name>` has a defined `pyproject.toml` file that you should use and keep up to date. Runtime dependencies include FastAPI and Uvicorn.
5 changes: 5 additions & 0 deletions files/templates/newFastApiTemplate/dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pytest
httpx
setuptools
wheel
# TODO: update the necessary requirements to develop this FastAPI project
Empty file.
11 changes: 11 additions & 0 deletions files/templates/newFastApiTemplate/package_name/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import uvicorn

from package_name.main import app


def main() -> None:
uvicorn.run(app, host="127.0.0.1", port=8000)


if __name__ == "__main__":
main()
8 changes: 8 additions & 0 deletions files/templates/newFastApiTemplate/package_name/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root() -> dict[str, str]:
return {"message": "Hello World"}
21 changes: 21 additions & 0 deletions files/templates/newFastApiTemplate/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[project]
name = "package_name"
version = "1.0.0"
description = "" #TODO: Add a short description of your FastAPI project
authors = [{name = "Your Name", email = "your@email.com"}] #TODO: Add your name and email
requires-python = ">=3.9"
dependencies = [
"fastapi",
"uvicorn[standard]",
]
dynamic = ["optional-dependencies"]

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools.dynamic]
optional-dependencies = {dev = {file = ["dev-requirements.txt"]}} # add packages for development in the dev-requirements.txt file

[project.scripts]
package_name = "package_name.__main__:main"
11 changes: 11 additions & 0 deletions files/templates/newFastApiTemplate/tests/test_package_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi.testclient import TestClient

from package_name.main import app

client = TestClient(app)


def test_read_root() -> None:
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
15 changes: 6 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Uri,
window,
} from 'vscode';
import { PackageManagementOptions, PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api';
import { PackageManagementOptions, PythonEnvironment, PythonEnvironmentApi } from './api';
import { ENVS_EXTENSION_ID } from './common/constants';
import { ensureCorrectVersion } from './common/extVersion';
import { InlineScriptPackagesNotManagedError } from './common/inlineScript/errors';
Expand Down Expand Up @@ -42,10 +42,11 @@ import { getPythonApi, setPythonApi } from './extensionApi';
import { createManagerReady } from './features/common/managerReady';
import { AutoFindProjects } from './features/creators/autoFindProjects';
import { ExistingProjects } from './features/creators/existingProjects';
import { NewFastApiProject } from './features/creators/newFastApiProject';
import { NewPackageProject } from './features/creators/newPackageProject';
import { NewScriptProject } from './features/creators/newScriptProject';
import type { ProjectCreators } from './features/creators/projectCreators';
import { ProjectCreatorsImpl } from './features/creators/projectCreators';
import { creatorNameForQuickCreateProjectType, ProjectCreatorsImpl } from './features/creators/projectCreators';
import {
addPythonProjectCommand,
clearEnvironmentCachesCommand,
Expand Down Expand Up @@ -206,6 +207,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
projectCreators.registerPythonProjectCreator(new ExistingProjects(projectManager)),
projectCreators.registerPythonProjectCreator(new AutoFindProjects(projectManager, envManagers)),
projectCreators.registerPythonProjectCreator(new NewPackageProject(envManagers, projectManager)),
projectCreators.registerPythonProjectCreator(new NewFastApiProject(envManagers, projectManager)),
projectCreators.registerPythonProjectCreator(new NewScriptProject(projectManager)),
);

Expand Down Expand Up @@ -587,13 +589,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
throw new Error('Project type, name, and path are required for quick create.');
}
const creators = projectCreators.getProjectCreators();
let selected: PythonProjectCreator | undefined;
if (projectType === 'python-package') {
selected = creators.find((c) => c.name === 'newPackage');
}
if (projectType === 'python-script') {
selected = creators.find((c) => c.name === 'newScript');
}
const creatorName = creatorNameForQuickCreateProjectType(projectType);
const selected = creatorName ? creators.find((creator) => creator.name === creatorName) : undefined;
if (!selected) {
throw new Error(`Project creator for type "${projectType}" not found.`);
}
Expand Down
210 changes: 210 additions & 0 deletions src/features/creators/newFastApiProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { commands, l10n, MarkdownString, QuickInputButtons, Uri, window, workspace } from 'vscode';
import { PythonEnvironment, PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
import { NEW_PROJECT_TEMPLATES_FOLDER, VENV_MANAGER_ID } from '../../common/constants';
import { traceError } from '../../common/logging';
import { showInputBoxWithButtons } from '../../common/window.apis';
import type { EnvironmentManagers } from '../envManagers';
import type { PythonProjectManager } from '../projectManager';
import {
isCopilotInstalled,
manageCopilotInstructionsFile,
manageLaunchJsonFile,
promptForVenv,
replaceInFilesAndNames,
} from './creationHelpers';

/** Quick-create project type accepted by `python-envs.createNewProjectFromTemplate`. */
export const FAST_API_PROJECT_TYPE = 'python-fastapi';

/** Stable creator id. Not localized, so command lookup keeps working in every locale. */
export const FAST_API_CREATOR_NAME = 'newFastAPI';

const FAST_API_TEMPLATE_DIRECTORY = 'newFastApiTemplate';
const FAST_API_COPILOT_INSTRUCTIONS = 'fastapi-copilot-instructions.md';

/**
* Validates a FastAPI project name using the same PEP 508-style rules as the package template.
* The name is also the Python package imported by the generated application.
* @param value - The project name entered by the user or supplied to quick create.
* @returns An error message when the name is invalid, otherwise `null`.
*/
export function validateFastApiProjectName(value: string): string | null {
// following PyPI (PEP 508) rules for package names
if (!/^([a-z_]|[a-z0-9_][a-z0-9._-]*[a-z0-9_])$/i.test(value)) {
return l10n.t(
'Invalid project name. Use only letters, numbers, underscores, hyphens, or periods. Must start and end with a letter or number.',
);
}
if (/^[-._0-9]$/i.test(value)) {
return l10n.t('Single-character project names cannot be a number, hyphen, or period.');
}
return null;
}

/**
* Scaffolds a FastAPI project from `files/templates/newFastApiTemplate`.
* Interactive creation asks for a name and whether to create a virtual environment.
* Quick create requires a name and root, then creates the environment without prompts.
*/
export class NewFastApiProject implements PythonProjectCreator {
public readonly name = FAST_API_CREATOR_NAME;
public readonly displayName = l10n.t('FastAPI');
public readonly description = l10n.t('Creates a FastAPI project in your current workspace');
public readonly tooltip = new MarkdownString(l10n.t('Create a new FastAPI project'));
public readonly supportsQuickCreate = true;

constructor(
private readonly envManagers: EnvironmentManagers,
private readonly projectManager: PythonProjectManager,
) {}

async create(options?: PythonProjectCreatorOptions): Promise<PythonProject | Uri | undefined> {
let packageName = options?.name;
let createVenv: boolean | undefined;
let createCopilotInstructions: boolean | undefined;
if (options?.quickCreate === true) {
if (!packageName) {
throw new Error('FastAPI project name is required in quickCreate mode.');
}
createVenv = true;
createCopilotInstructions = true;
} else if (!packageName) {
try {
packageName = await showInputBoxWithButtons({
prompt: l10n.t('What is the name of the FastAPI project? (e.g. my_api)'),
ignoreFocusOut: true,
showBackButton: true,
validateInput: validateFastApiProjectName,
});
} catch (ex) {
if (ex === QuickInputButtons.Back) {
await commands.executeCommand('python-envs.createNewProjectFromTemplate');
}
}
if (!packageName) {
return undefined;
}
const callback = () => {
return this.create(options);
};
createVenv = await promptForVenv(callback);
if (createVenv === undefined) {
return undefined;
}
if (isCopilotInstalled()) {
createCopilotInstructions = true;
}
}

const validationError = validateFastApiProjectName(packageName);
if (validationError) {
if (options?.quickCreate === true) {
throw new Error(validationError);
}
window.showErrorMessage(validationError);
return undefined;
}

const templateFolder = path.join(NEW_PROJECT_TEMPLATES_FOLDER, FAST_API_TEMPLATE_DIRECTORY);
if (!(await fs.pathExists(templateFolder))) {
window.showErrorMessage(l10n.t('Template folder does not exist, aborting creation.'));
traceError(`FastAPI template folder not found at: ${templateFolder}`);
return undefined;
}

let destRoot = options?.rootUri?.fsPath;
if (!destRoot) {
const workspaceFolders = workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
window.showErrorMessage(l10n.t('No workspace folder is open or provided, aborting creation.'));
traceError('No workspace folder is open for FastAPI project creation.');
return undefined;
}
destRoot = workspaceFolders[0].uri.fsPath;
}

const projectDestinationFolder = path.join(destRoot, `${packageName}_project`);
if (await fs.pathExists(projectDestinationFolder)) {
window.showErrorMessage(
l10n.t(
'A project folder by that name already exists, aborting creation. Please retry with a unique project name given your workspace.',
),
);
return undefined;
}
await fs.copy(templateFolder, projectDestinationFolder);
await replaceInFilesAndNames(projectDestinationFolder, 'package_name', packageName);

const createdProject: PythonProject = {
name: packageName,
uri: Uri.file(projectDestinationFolder),
};
await this.projectManager.add(createdProject);

let createdEnv: PythonEnvironment | undefined;
if (createVenv) {
const en = this.envManagers.getEnvironmentManager(undefined);
if (en?.supportsQuickCreate) {
createdEnv = await en.create(Uri.file(projectDestinationFolder), { quickCreate: true });
} else if (!options?.quickCreate && en?.supportsCreate) {
createdEnv = await en.create(Uri.file(projectDestinationFolder), {});
} else {
const venvManager = this.envManagers.managers.find(
(manager) => manager.id === VENV_MANAGER_ID || manager.supportsQuickCreate,
);
if (venvManager) {
createdEnv = await venvManager.create(Uri.file(projectDestinationFolder), { quickCreate: true });
} else {
const action = await window.showErrorMessage(
l10n.t(
'A virtual environment could not be created for the new FastAPI project "{0}" because your default environment manager does not support this operation and no alternative was available.',
packageName,
),
l10n.t('Create custom environment'),
);
if (action === l10n.t('Create custom environment')) {
await commands.executeCommand('python-envs.createAny', {
uri: createdProject.uri,
selectEnvironment: true,
});
createdEnv = await this.envManagers.getEnvironment(createdProject.uri);
}
}
}
}

createdEnv = createdEnv || (await this.envManagers.getEnvironment(Uri.file(projectDestinationFolder)));
if (!createdEnv) {
window.showErrorMessage(
l10n.t('Project created but unable to be correlated to correct Python environment.'),
);
return undefined;
}
await this.envManagers.setEnvironment(createdProject.uri, createdEnv);

if (createCopilotInstructions) {
const instructionsPath = path.join(
NEW_PROJECT_TEMPLATES_FOLDER,
'copilot-instructions-text',
FAST_API_COPILOT_INSTRUCTIONS,
);
await manageCopilotInstructionsFile(destRoot, instructionsPath, [
{ searchValue: '<package_name>', replaceValue: packageName },
]);
}

const launchJsonConfig = {
name: `Python: FastAPI (${packageName})`,
type: 'debugpy',
request: 'launch',
module: 'uvicorn',
args: [`${packageName}.main:app`],
jinja: true,
};
await manageLaunchJsonFile(destRoot, JSON.stringify(launchJsonConfig));

return createdProject;
}
}
Loading