Skip to content

Commit 1dbc9c8

Browse files
karthiknadigeleanorjboyd
authored andcommitted
Create environment using venv or conda (microsoft#19848)
Closes microsoft#19676 Closes microsoft#19850
1 parent c06ec90 commit 1dbc9c8

32 files changed

+2007
-31
lines changed

package.json

+11
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@
271271
"command": "python.createTerminal",
272272
"title": "%python.command.python.createTerminal.title%"
273273
},
274+
{
275+
"category": "Python",
276+
"command": "python.createEnvironment",
277+
"title": "%python.command.python.createEnvironment.title%"
278+
},
274279
{
275280
"category": "Python",
276281
"command": "python.enableLinting",
@@ -1540,6 +1545,12 @@
15401545
"title": "%python.command.python.configureTests.title%",
15411546
"when": "!virtualWorkspace && shellExecutionSupported"
15421547
},
1548+
{
1549+
"category": "Python",
1550+
"command": "python.createEnvironment",
1551+
"title": "%python.command.python.createEnvironment.title%",
1552+
"when": "!virtualWorkspace && shellExecutionSupported"
1553+
},
15431554
{
15441555
"category": "Python",
15451556
"command": "python.createTerminal",

package.nls.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"python.command.python.sortImports.title": "Sort Imports",
33
"python.command.python.startREPL.title": "Start REPL",
4+
"python.command.python.createEnvironment.title": "Create Environment",
45
"python.command.python.createTerminal.title": "Create Terminal",
56
"python.command.python.execInTerminal.title": "Run Python File in Terminal",
67
"python.command.python.debugInTerminal.title": "Debug Python File",
@@ -24,15 +25,15 @@
2425
"python.command.python.launchTensorBoard.title": "Launch TensorBoard",
2526
"python.command.python.refreshTensorBoard.title": "Refresh TensorBoard",
2627
"python.menu.createNewFile.title": "Python File",
27-
"python.autoComplete.extraPaths.description":"List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.",
28+
"python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.",
2829
"python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).",
2930
"python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See https://aka.ms/AAfekmf to understand when this is used",
3031
"python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.",
3132
"python.envFile.description": "Absolute path to a file containing environment variable definitions.",
3233
"python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.",
3334
"python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.",
3435
"python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.",
35-
"python.formatting.autopep8Args.description":"Arguments passed in. Each argument is a separate item in the array.",
36+
"python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.",
3637
"python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.",
3738
"python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.",
3839
"python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.",

pythonFiles/create_conda.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import argparse
5+
import os
6+
import pathlib
7+
import subprocess
8+
import sys
9+
from typing import Optional, Sequence, Union
10+
11+
CONDA_ENV_NAME = ".conda"
12+
CWD = pathlib.PurePath(os.getcwd())
13+
14+
15+
class VenvError(Exception):
16+
pass
17+
18+
19+
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
20+
parser = argparse.ArgumentParser()
21+
parser.add_argument(
22+
"--python",
23+
action="store",
24+
help="Python version to install in the virtual environment.",
25+
default=f"{sys.version_info.major}.{sys.version_info.minor}",
26+
)
27+
parser.add_argument(
28+
"--install",
29+
action="store_true",
30+
default=False,
31+
help="Install packages into the virtual environment.",
32+
)
33+
parser.add_argument(
34+
"--git-ignore",
35+
action="store_true",
36+
default=False,
37+
help="Add .gitignore to the newly created virtual environment.",
38+
)
39+
parser.add_argument(
40+
"--name",
41+
default=CONDA_ENV_NAME,
42+
type=str,
43+
help="Name of the virtual environment.",
44+
metavar="NAME",
45+
action="store",
46+
)
47+
return parser.parse_args(argv)
48+
49+
50+
def file_exists(path: Union[str, pathlib.PurePath]) -> bool:
51+
return os.path.exists(path)
52+
53+
54+
def conda_env_exists(name: Union[str, pathlib.PurePath]) -> bool:
55+
return os.path.exists(CWD / name)
56+
57+
58+
def run_process(args: Sequence[str], error_message: str) -> None:
59+
try:
60+
print("Running: " + " ".join(args))
61+
subprocess.run(args, cwd=os.getcwd(), check=True)
62+
except subprocess.CalledProcessError:
63+
raise VenvError(error_message)
64+
65+
66+
def get_conda_env_path(name: str) -> str:
67+
return os.fspath(CWD / name)
68+
69+
70+
def install_packages(env_path: str) -> None:
71+
yml = os.fspath(CWD / "environment.yml")
72+
if file_exists(yml):
73+
print(f"CONDA_INSTALLING_YML: {yml}")
74+
run_process(
75+
[
76+
sys.executable,
77+
"-m",
78+
"conda",
79+
"env",
80+
"update",
81+
"--prefix",
82+
env_path,
83+
"--file",
84+
yml,
85+
],
86+
"CREATE_CONDA.FAILED_INSTALL_YML",
87+
)
88+
89+
90+
def add_gitignore(name: str) -> None:
91+
git_ignore = os.fspath(CWD / name / ".gitignore")
92+
if not file_exists(git_ignore):
93+
print(f"Creating: {git_ignore}")
94+
with open(git_ignore, "w") as f:
95+
f.write("*")
96+
97+
98+
def main(argv: Optional[Sequence[str]] = None) -> None:
99+
if argv is None:
100+
argv = []
101+
args = parse_args(argv)
102+
103+
if not conda_env_exists(args.name):
104+
run_process(
105+
[
106+
sys.executable,
107+
"-m",
108+
"conda",
109+
"create",
110+
"--yes",
111+
"--prefix",
112+
args.name,
113+
f"python={args.python}",
114+
],
115+
"CREATE_CONDA.ENV_FAILED_CREATION",
116+
)
117+
if args.git_ignore:
118+
add_gitignore(args.name)
119+
120+
env_path = get_conda_env_path(args.name)
121+
print(f"CREATED_CONDA_ENV:{env_path}")
122+
123+
if args.install:
124+
install_packages(env_path)
125+
126+
127+
if __name__ == "__main__":
128+
main(sys.argv[1:])

pythonFiles/create_venv.py

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import argparse
5+
import importlib.util as import_util
6+
import os
7+
import pathlib
8+
import subprocess
9+
import sys
10+
from typing import Optional, Sequence, Union
11+
12+
VENV_NAME = ".venv"
13+
CWD = pathlib.PurePath(os.getcwd())
14+
15+
16+
class VenvError(Exception):
17+
pass
18+
19+
20+
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
21+
parser = argparse.ArgumentParser()
22+
parser.add_argument(
23+
"--install",
24+
action="store_true",
25+
default=False,
26+
help="Install packages into the virtual environment.",
27+
)
28+
parser.add_argument(
29+
"--git-ignore",
30+
action="store_true",
31+
default=False,
32+
help="Add .gitignore to the newly created virtual environment.",
33+
)
34+
parser.add_argument(
35+
"--name",
36+
default=VENV_NAME,
37+
type=str,
38+
help="Name of the virtual environment.",
39+
metavar="NAME",
40+
action="store",
41+
)
42+
return parser.parse_args(argv)
43+
44+
45+
def is_installed(module: str) -> bool:
46+
return import_util.find_spec(module) is not None
47+
48+
49+
def file_exists(path: Union[str, pathlib.PurePath]) -> bool:
50+
return os.path.exists(path)
51+
52+
53+
def venv_exists(name: str) -> bool:
54+
return os.path.exists(CWD / name)
55+
56+
57+
def run_process(args: Sequence[str], error_message: str) -> None:
58+
try:
59+
print("Running: " + " ".join(args))
60+
subprocess.run(args, cwd=os.getcwd(), check=True)
61+
except subprocess.CalledProcessError:
62+
raise VenvError(error_message)
63+
64+
65+
def get_venv_path(name: str) -> str:
66+
# See `venv` doc here for more details on binary location:
67+
# https://docs.python.org/3/library/venv.html#creating-virtual-environments
68+
if sys.platform == "win32":
69+
return os.fspath(CWD / name / "Scripts" / "python.exe")
70+
else:
71+
return os.fspath(CWD / name / "bin" / "python")
72+
73+
74+
def install_packages(venv_path: str) -> None:
75+
if not is_installed("pip"):
76+
raise VenvError("CREATE_VENV.PIP_NOT_FOUND")
77+
78+
requirements = os.fspath(CWD / "requirements.txt")
79+
pyproject = os.fspath(CWD / "pyproject.toml")
80+
81+
run_process(
82+
[venv_path, "-m", "pip", "install", "--upgrade", "pip"],
83+
"CREATE_VENV.PIP_UPGRADE_FAILED",
84+
)
85+
86+
if file_exists(requirements):
87+
print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}")
88+
run_process(
89+
[venv_path, "-m", "pip", "install", "-r", requirements],
90+
"CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS",
91+
)
92+
elif file_exists(pyproject):
93+
print(f"VENV_INSTALLING_PYPROJECT: {pyproject}")
94+
run_process(
95+
[venv_path, "-m", "pip", "install", "-e", ".[extras]"],
96+
"CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT",
97+
)
98+
99+
100+
def add_gitignore(name: str) -> None:
101+
git_ignore = CWD / name / ".gitignore"
102+
if not file_exists(git_ignore):
103+
print("Creating: " + os.fspath(git_ignore))
104+
with open(git_ignore, "w") as f:
105+
f.write("*")
106+
107+
108+
def main(argv: Optional[Sequence[str]] = None) -> None:
109+
if argv is None:
110+
argv = []
111+
args = parse_args(argv)
112+
113+
if is_installed("venv"):
114+
if not venv_exists(args.name):
115+
run_process(
116+
[sys.executable, "-m", "venv", args.name],
117+
"CREATE_VENV.VENV_FAILED_CREATION",
118+
)
119+
if args.git_ignore:
120+
add_gitignore(args.name)
121+
venv_path = get_venv_path(args.name)
122+
print(f"CREATED_VENV:{venv_path}")
123+
if args.install:
124+
install_packages(venv_path)
125+
else:
126+
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
127+
128+
129+
if __name__ == "__main__":
130+
main(sys.argv[1:])
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import importlib
5+
import sys
6+
7+
import create_conda
8+
import pytest
9+
10+
11+
@pytest.mark.parametrize("env_exists", [True, False])
12+
@pytest.mark.parametrize("git_ignore", [True, False])
13+
@pytest.mark.parametrize("install", [True, False])
14+
@pytest.mark.parametrize("python", [True, False])
15+
def test_create_env(env_exists, git_ignore, install, python):
16+
importlib.reload(create_conda)
17+
create_conda.conda_env_exists = lambda _n: env_exists
18+
19+
install_packages_called = False
20+
21+
def install_packages(_name):
22+
nonlocal install_packages_called
23+
install_packages_called = True
24+
25+
create_conda.install_packages = install_packages
26+
27+
run_process_called = False
28+
29+
def run_process(args, error_message):
30+
nonlocal run_process_called
31+
run_process_called = True
32+
version = (
33+
"12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}"
34+
)
35+
if not env_exists:
36+
assert args == [
37+
sys.executable,
38+
"-m",
39+
"conda",
40+
"create",
41+
"--yes",
42+
"--prefix",
43+
create_conda.CONDA_ENV_NAME,
44+
f"python={version}",
45+
]
46+
assert error_message == "CREATE_CONDA.ENV_FAILED_CREATION"
47+
48+
create_conda.run_process = run_process
49+
50+
add_gitignore_called = False
51+
52+
def add_gitignore(_name):
53+
nonlocal add_gitignore_called
54+
add_gitignore_called = True
55+
56+
create_conda.add_gitignore = add_gitignore
57+
58+
args = []
59+
if git_ignore:
60+
args.append("--git-ignore")
61+
if install:
62+
args.append("--install")
63+
if python:
64+
args.extend(["--python", "12345"])
65+
create_conda.main(args)
66+
assert install_packages_called == install
67+
68+
# run_process is called when the venv does not exist
69+
assert run_process_called != env_exists
70+
71+
# add_gitignore is called when new venv is created and git_ignore is True
72+
assert add_gitignore_called == (not env_exists and git_ignore)

0 commit comments

Comments
 (0)