Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add microvenv support for non-windows platforms #20985

Merged
merged 1 commit into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pythonFiles/create_conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Optional, Sequence, Union

CONDA_ENV_NAME = ".conda"
CWD = pathlib.PurePath(os.getcwd())
CWD = pathlib.Path.cwd()


class VenvError(Exception):
Expand Down
97 changes: 97 additions & 0 deletions pythonFiles/create_microvenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import argparse
import os
import pathlib
import subprocess
import sys
import urllib.request as url_lib
from typing import Optional, Sequence

VENV_NAME = ".venv"
LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python"
CWD = pathlib.Path.cwd()


class MicroVenvError(Exception):
pass


def run_process(args: Sequence[str], error_message: str) -> None:
try:
print("Running: " + " ".join(args))
subprocess.run(args, cwd=os.getcwd(), check=True)
except subprocess.CalledProcessError:
raise MicroVenvError(error_message)


def parse_args(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser()

parser.add_argument(
"--install-pip",
action="store_true",
default=False,
help="Install pip into the virtual environment.",
)

parser.add_argument(
"--name",
default=VENV_NAME,
type=str,
help="Name of the virtual environment.",
metavar="NAME",
action="store",
)
return parser.parse_args(argv)


def create_microvenv(name: str):
run_process(
[sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name],
"CREATE_MICROVENV.MICROVENV_FAILED_CREATION",
)


def download_pip_pyz(name: str):
url = "https://bootstrap.pypa.io/pip/pip.pyz"
print("CREATE_MICROVENV.DOWNLOADING_PIP")

try:
with url_lib.urlopen(url) as response:
pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
with open(pip_pyz_path, "wb") as out_file:
data = response.read()
out_file.write(data)
out_file.flush()
except Exception:
raise MicroVenvError("CREATE_MICROVENV.DOWNLOAD_PIP_FAILED")


def install_pip(name: str):
pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
executable = os.fspath(CWD / name / "bin" / "python")
print("CREATE_MICROVENV.INSTALLING_PIP")
run_process(
[executable, pip_pyz_path, "install", "pip"],
"CREATE_MICROVENV.INSTALL_PIP_FAILED",
)


def main(argv: Optional[Sequence[str]] = None) -> None:
if argv is None:
argv = []
args = parse_args(argv)

print("CREATE_MICROVENV.CREATING_MICROVENV")
Copy link

Choose a reason for hiding this comment

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

Just noticed, sorry. Looks good.

Copy link
Member Author

Choose a reason for hiding this comment

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

It also does this:
image

which prints out the full command we run:

> .venv/bin/python /<extension>/microvenv.py .venv

create_microvenv(args.name)
print("CREATE_MICROVENV.CREATED_MICROVENV")

if args.install_pip:
download_pip_pyz(args.name)
install_pip(args.name)


if __name__ == "__main__":
main(sys.argv[1:])
32 changes: 25 additions & 7 deletions pythonFiles/create_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from typing import List, Optional, Sequence, Union

VENV_NAME = ".venv"
CWD = pathlib.PurePath(os.getcwd())
CWD = pathlib.Path.cwd()
MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py"


class VenvError(Exception):
Expand Down Expand Up @@ -130,22 +131,39 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
argv = []
args = parse_args(argv)

use_micro_venv = False
if not is_installed("venv"):
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
if sys.platform == "win32":
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
else:
use_micro_venv = True

pip_installed = is_installed("pip")
deps_needed = args.requirements or args.extras or args.toml
if deps_needed and not pip_installed:
if deps_needed and not pip_installed and not use_micro_venv:
raise VenvError("CREATE_VENV.PIP_NOT_FOUND")

if venv_exists(args.name):
venv_path = get_venv_path(args.name)
print(f"EXISTING_VENV:{venv_path}")
else:
run_process(
[sys.executable, "-m", "venv", args.name],
"CREATE_VENV.VENV_FAILED_CREATION",
)
if use_micro_venv:
run_process(
[
sys.executable,
os.fspath(MICROVENV_SCRIPT_PATH),
"--install-pip",
"--name",
args.name,
],
"CREATE_VENV.MICROVENV_FAILED_CREATION",
)
pip_installed = True
else:
run_process(
[sys.executable, "-m", "venv", args.name],
"CREATE_VENV.VENV_FAILED_CREATION",
)
venv_path = get_venv_path(args.name)
print(f"CREATED_VENV:{venv_path}")
if args.git_ignore:
Expand Down
60 changes: 60 additions & 0 deletions pythonFiles/tests/test_create_microvenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import importlib
import os
import sys

import create_microvenv
import pytest


def test_create_microvenv():
importlib.reload(create_microvenv)
run_process_called = False

def run_process(args, error_message):
nonlocal run_process_called
run_process_called = True
assert args == [
sys.executable,
os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"),
create_microvenv.VENV_NAME,
]
assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION"

create_microvenv.run_process = run_process

create_microvenv.main()
assert run_process_called == True


def test_create_microvenv_with_pip():
importlib.reload(create_microvenv)

download_pip_pyz_called = False

def download_pip_pyz(name):
nonlocal download_pip_pyz_called
download_pip_pyz_called = True
assert name == create_microvenv.VENV_NAME

create_microvenv.download_pip_pyz = download_pip_pyz

run_process_called = False

def run_process(args, error_message):
if "install" in args and "pip" in args:
nonlocal run_process_called
run_process_called = True
pip_pyz_path = os.fspath(
create_microvenv.CWD / create_microvenv.VENV_NAME / "pip.pyz"
)
executable = os.fspath(
create_microvenv.CWD / create_microvenv.VENV_NAME / "bin" / "python"
)
assert args == [executable, pip_pyz_path, "install", "pip"]
assert error_message == "CREATE_MICROVENV.INSTALL_PIP_FAILED"

create_microvenv.run_process = run_process
create_microvenv.main(["--install-pip"])
35 changes: 34 additions & 1 deletion pythonFiles/tests/test_create_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,46 @@
# Licensed under the MIT License.

import importlib
import os
import sys

import create_venv
import pytest


def test_venv_not_installed():
@pytest.mark.skipif(
sys.platform == "win32", reason="Windows does not have micro venv fallback."
)
def test_venv_not_installed_unix():
importlib.reload(create_venv)
create_venv.is_installed = lambda module: module != "venv"
run_process_called = False

def run_process(args, error_message):
nonlocal run_process_called
if "--install-pip" in args:
run_process_called = True
assert args == [
sys.executable,
os.fspath(create_venv.MICROVENV_SCRIPT_PATH),
"--install-pip",
"--name",
".test_venv",
]
assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION"

create_venv.run_process = run_process

create_venv.main(["--name", ".test_venv"])

# run_process is called when the venv does not exist
assert run_process_called == True


@pytest.mark.skipif(
sys.platform != "win32", reason="Windows does not have microvenv fallback."
)
def test_venv_not_installed_windows():
importlib.reload(create_venv)
create_venv.is_installed = lambda module: module != "venv"
with pytest.raises(create_venv.VenvError) as e:
Expand Down
3 changes: 3 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@

# Unittest test adapter
typing-extensions==4.5.0

# Fallback env creator for debian
microvenv
8 changes: 6 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#
# This file is autogenerated by pip-compile with python 3.7
# To update, run:
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# pip-compile --generate-hashes requirements.in
#
microvenv==2023.2.0 \
--hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \
--hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3
# via -r requirements.in
typing-extensions==4.5.0 \
--hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \
--hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4
Expand Down
4 changes: 4 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,11 @@ export namespace CreateEnv {

export namespace Venv {
export const creating = l10n.t('Creating venv...');
export const creatingMicrovenv = l10n.t('Creating microvenv...');
export const created = l10n.t('Environment created...');
export const existing = l10n.t('Using existing environment...');
export const downloadingPip = l10n.t('Downloading pip...');
export const installingPip = l10n.t('Installing pip...');
export const upgradingPip = l10n.t('Upgrading pip...');
export const installingPackages = l10n.t('Installing packages...');
export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.');
Expand Down
Loading