Skip to content

Commit bebf05d

Browse files
authored
Create env shows requirements files or pyproject.toml extras when available (#20524)
Closes #20277 Closes #20278
1 parent c545a36 commit bebf05d

File tree

11 files changed

+756
-125
lines changed

11 files changed

+756
-125
lines changed

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -1806,6 +1806,7 @@
18061806
"webpack": "webpack"
18071807
},
18081808
"dependencies": {
1809+
"@ltd/j-toml": "^1.37.0",
18091810
"@vscode/extension-telemetry": "^0.7.4-preview",
18101811
"@vscode/jupyter-lsp-middleware": "^0.2.50",
18111812
"arch": "^2.1.0",

pythonFiles/create_venv.py

+57-26
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pathlib
88
import subprocess
99
import sys
10-
from typing import Optional, Sequence, Union
10+
from typing import List, Optional, Sequence, Union
1111

1212
VENV_NAME = ".venv"
1313
CWD = pathlib.PurePath(os.getcwd())
@@ -19,12 +19,27 @@ class VenvError(Exception):
1919

2020
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
2121
parser = argparse.ArgumentParser()
22+
2223
parser.add_argument(
23-
"--install",
24-
action="store_true",
25-
default=False,
26-
help="Install packages into the virtual environment.",
24+
"--requirements",
25+
action="append",
26+
default=[],
27+
help="Install additional dependencies into the virtual environment.",
28+
)
29+
30+
parser.add_argument(
31+
"--toml",
32+
action="store",
33+
default=None,
34+
help="Install additional dependencies from sources like `pyproject.toml` into the virtual environment.",
2735
)
36+
parser.add_argument(
37+
"--extras",
38+
action="append",
39+
default=[],
40+
help="Install specific package groups from `pyproject.toml` into the virtual environment.",
41+
)
42+
2843
parser.add_argument(
2944
"--git-ignore",
3045
action="store_true",
@@ -71,30 +86,36 @@ def get_venv_path(name: str) -> str:
7186
return os.fspath(CWD / name / "bin" / "python")
7287

7388

74-
def install_packages(venv_path: str) -> None:
75-
requirements = os.fspath(CWD / "requirements.txt")
76-
pyproject = os.fspath(CWD / "pyproject.toml")
89+
def install_requirements(venv_path: str, requirements: List[str]) -> None:
90+
if not requirements:
91+
return
7792

93+
print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}")
94+
args = []
95+
for requirement in requirements:
96+
args += ["-r", requirement]
97+
run_process(
98+
[venv_path, "-m", "pip", "install"] + args,
99+
"CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS",
100+
)
101+
print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS")
102+
103+
104+
def install_toml(venv_path: str, extras: List[str]) -> None:
105+
args = "." if len(extras) == 0 else f".[{','.join(extras)}]"
106+
run_process(
107+
[venv_path, "-m", "pip", "install", "-e", args],
108+
"CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT",
109+
)
110+
print("CREATE_VENV.PIP_INSTALLED_PYPROJECT")
111+
112+
113+
def upgrade_pip(venv_path: str) -> None:
78114
run_process(
79115
[venv_path, "-m", "pip", "install", "--upgrade", "pip"],
80116
"CREATE_VENV.PIP_UPGRADE_FAILED",
81117
)
82118

83-
if file_exists(requirements):
84-
print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}")
85-
run_process(
86-
[venv_path, "-m", "pip", "install", "-r", requirements],
87-
"CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS",
88-
)
89-
print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS")
90-
elif file_exists(pyproject):
91-
print(f"VENV_INSTALLING_PYPROJECT: {pyproject}")
92-
run_process(
93-
[venv_path, "-m", "pip", "install", "-e", ".[extras]"],
94-
"CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT",
95-
)
96-
print("CREATE_VENV.PIP_INSTALLED_PYPROJECT")
97-
98119

99120
def add_gitignore(name: str) -> None:
100121
git_ignore = CWD / name / ".gitignore"
@@ -112,7 +133,9 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
112133
if not is_installed("venv"):
113134
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
114135

115-
if args.install and not is_installed("pip"):
136+
pip_installed = is_installed("pip")
137+
deps_needed = args.requirements or args.extras or args.toml
138+
if deps_needed and not pip_installed:
116139
raise VenvError("CREATE_VENV.PIP_NOT_FOUND")
117140

118141
if venv_exists(args.name):
@@ -128,8 +151,16 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
128151
if args.git_ignore:
129152
add_gitignore(args.name)
130153

131-
if args.install:
132-
install_packages(venv_path)
154+
if pip_installed:
155+
upgrade_pip(venv_path)
156+
157+
if args.requirements:
158+
print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}")
159+
install_requirements(venv_path, args.requirements)
160+
161+
if args.toml:
162+
print(f"VENV_INSTALLING_PYPROJECT: {args.toml}")
163+
install_toml(venv_path, args.extras)
133164

134165

135166
if __name__ == "__main__":

pythonFiles/tests/test_create_venv.py

+96-18
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,44 @@ def test_venv_not_installed():
1616
assert str(e.value) == "CREATE_VENV.VENV_NOT_FOUND"
1717

1818

19-
def test_pip_not_installed():
19+
@pytest.mark.parametrize("install", ["requirements", "toml"])
20+
def test_pip_not_installed(install):
2021
importlib.reload(create_venv)
2122
create_venv.venv_exists = lambda _n: True
2223
create_venv.is_installed = lambda module: module != "pip"
2324
create_venv.run_process = lambda _args, _error_message: None
2425
with pytest.raises(create_venv.VenvError) as e:
25-
create_venv.main(["--install"])
26+
if install == "requirements":
27+
create_venv.main(["--requirements", "requirements-for-test.txt"])
28+
elif install == "toml":
29+
create_venv.main(["--toml", "pyproject.toml", "--extras", "test"])
2630
assert str(e.value) == "CREATE_VENV.PIP_NOT_FOUND"
2731

2832

29-
@pytest.mark.parametrize("env_exists", [True, False])
30-
@pytest.mark.parametrize("git_ignore", [True, False])
31-
@pytest.mark.parametrize("install", [True, False])
33+
@pytest.mark.parametrize("env_exists", ["hasEnv", "noEnv"])
34+
@pytest.mark.parametrize("git_ignore", ["useGitIgnore", "skipGitIgnore"])
35+
@pytest.mark.parametrize("install", ["requirements", "toml", "skipInstall"])
3236
def test_create_env(env_exists, git_ignore, install):
3337
importlib.reload(create_venv)
3438
create_venv.is_installed = lambda _x: True
35-
create_venv.venv_exists = lambda _n: env_exists
39+
create_venv.venv_exists = lambda _n: env_exists == "hasEnv"
40+
create_venv.upgrade_pip = lambda _x: None
3641

3742
install_packages_called = False
3843

39-
def install_packages(_name):
44+
def install_packages(_env, _name):
4045
nonlocal install_packages_called
4146
install_packages_called = True
4247

43-
create_venv.install_packages = install_packages
48+
create_venv.install_requirements = install_packages
49+
create_venv.install_toml = install_packages
4450

4551
run_process_called = False
4652

4753
def run_process(args, error_message):
4854
nonlocal run_process_called
4955
run_process_called = True
50-
if not env_exists:
56+
if env_exists == "noEnv":
5157
assert args == [sys.executable, "-m", "venv", create_venv.VENV_NAME]
5258
assert error_message == "CREATE_VENV.VENV_FAILED_CREATION"
5359

@@ -62,18 +68,23 @@ def add_gitignore(_name):
6268
create_venv.add_gitignore = add_gitignore
6369

6470
args = []
65-
if git_ignore:
66-
args.append("--git-ignore")
67-
if install:
68-
args.append("--install")
71+
if git_ignore == "useGitIgnore":
72+
args += ["--git-ignore"]
73+
if install == "requirements":
74+
args += ["--requirements", "requirements-for-test.txt"]
75+
elif install == "toml":
76+
args += ["--toml", "pyproject.toml", "--extras", "test"]
77+
6978
create_venv.main(args)
70-
assert install_packages_called == install
79+
assert install_packages_called == (install != "skipInstall")
7180

7281
# run_process is called when the venv does not exist
73-
assert run_process_called != env_exists
82+
assert run_process_called == (env_exists == "noEnv")
7483

7584
# add_gitignore is called when new venv is created and git_ignore is True
76-
assert add_gitignore_called == (not env_exists and git_ignore)
85+
assert add_gitignore_called == (
86+
(env_exists == "noEnv") and (git_ignore == "useGitIgnore")
87+
)
7788

7889

7990
@pytest.mark.parametrize("install_type", ["requirements", "pyproject"])
@@ -93,12 +104,79 @@ def run_process(args, error_message):
93104
elif args[1:-1] == ["-m", "pip", "install", "-r"]:
94105
installing = "requirements"
95106
assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS"
96-
elif args[1:] == ["-m", "pip", "install", "-e", ".[extras]"]:
107+
elif args[1:] == ["-m", "pip", "install", "-e", ".[test]"]:
97108
installing = "pyproject"
98109
assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT"
99110

100111
create_venv.run_process = run_process
101112

102-
create_venv.main(["--install"])
113+
if install_type == "requirements":
114+
create_venv.main(["--requirements", "requirements-for-test.txt"])
115+
elif install_type == "pyproject":
116+
create_venv.main(["--toml", "pyproject.toml", "--extras", "test"])
117+
103118
assert pip_upgraded
104119
assert installing == install_type
120+
121+
122+
@pytest.mark.parametrize(
123+
("extras", "expected"),
124+
[
125+
([], ["-m", "pip", "install", "-e", "."]),
126+
(["test"], ["-m", "pip", "install", "-e", ".[test]"]),
127+
(["test", "doc"], ["-m", "pip", "install", "-e", ".[test,doc]"]),
128+
],
129+
)
130+
def test_toml_args(extras, expected):
131+
importlib.reload(create_venv)
132+
133+
actual = []
134+
135+
def run_process(args, error_message):
136+
nonlocal actual
137+
actual = args[1:]
138+
139+
create_venv.run_process = run_process
140+
141+
create_venv.install_toml(sys.executable, extras)
142+
143+
assert actual == expected
144+
145+
146+
@pytest.mark.parametrize(
147+
("extras", "expected"),
148+
[
149+
([], None),
150+
(
151+
["requirements/test.txt"],
152+
[sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"],
153+
),
154+
(
155+
["requirements/test.txt", "requirements/doc.txt"],
156+
[
157+
sys.executable,
158+
"-m",
159+
"pip",
160+
"install",
161+
"-r",
162+
"requirements/test.txt",
163+
"-r",
164+
"requirements/doc.txt",
165+
],
166+
),
167+
],
168+
)
169+
def test_requirements_args(extras, expected):
170+
importlib.reload(create_venv)
171+
172+
actual = None
173+
174+
def run_process(args, error_message):
175+
nonlocal actual
176+
actual = args
177+
178+
create_venv.run_process = run_process
179+
180+
create_venv.install_requirements(sys.executable, extras)
181+
182+
assert actual == expected

src/client/common/utils/localize.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ export namespace CreateEnv {
437437
export const selectPythonQuickPickTitle = l10n.t('Select a python to use for environment creation');
438438
export const providerDescription = l10n.t('Creates a `.venv` virtual environment in the current workspace');
439439
export const error = l10n.t('Creating virtual environment failed with error.');
440+
export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml');
441+
export const requirementsQuickPickTitle = l10n.t('Select dependencies to install');
440442
}
441443

442444
export namespace Conda {
@@ -454,13 +456,11 @@ export namespace CreateEnv {
454456

455457
export namespace ToolsExtensions {
456458
export const flake8PromptMessage = l10n.t(
457-
'toolsExt.flake8.message',
458459
'Use the Flake8 extension to enable easier configuration and new features such as quick fixes.',
459460
);
460461
export const pylintPromptMessage = l10n.t(
461-
'toolsExt.pylint.message',
462462
'Use the Pylint extension to enable easier configuration and new features such as quick fixes.',
463463
);
464-
export const installPylintExtension = l10n.t('toolsExt.install.pylint', 'Install Pylint extension');
465-
export const installFlake8Extension = l10n.t('toolsExt.install.flake8', 'Install Flake8 extension');
464+
export const installPylintExtension = l10n.t('Install Pylint extension');
465+
export const installFlake8Extension = l10n.t('Install Flake8 extension');
466466
}

src/client/common/vscodeApis/workspaceApis.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { ConfigurationScope, workspace, WorkspaceConfiguration, WorkspaceEdit, WorkspaceFolder } from 'vscode';
4+
import {
5+
CancellationToken,
6+
ConfigurationScope,
7+
GlobPattern,
8+
Uri,
9+
workspace,
10+
WorkspaceConfiguration,
11+
WorkspaceEdit,
12+
WorkspaceFolder,
13+
} from 'vscode';
514
import { Resource } from '../types';
615

716
export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined {
@@ -23,3 +32,12 @@ export function getConfiguration(section?: string, scope?: ConfigurationScope |
2332
export function applyEdit(edit: WorkspaceEdit): Thenable<boolean> {
2433
return workspace.applyEdit(edit);
2534
}
35+
36+
export function findFiles(
37+
include: GlobPattern,
38+
exclude?: GlobPattern | null,
39+
maxResults?: number,
40+
token?: CancellationToken,
41+
): Thenable<Uri[]> {
42+
return workspace.findFiles(include, exclude, maxResults, token);
43+
}

0 commit comments

Comments
 (0)