Commit 7f5acc3f by Sébastien Eustace Committed by GitHub

Improve environment management (#1477)

* Refactor the environment management code

* Improve executable selection when current Python is incompatible
parent 92c460ef
......@@ -103,7 +103,7 @@ class DebugResolveCommand(InitCommand):
return 0
env = EnvManager(self.poetry.config).get(self.poetry.file.parent)
env = EnvManager(self.poetry).get()
current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info)
)
......
......@@ -13,8 +13,7 @@ class EnvInfoCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
env = EnvManager(poetry.config).get(cwd=poetry.file.parent)
env = EnvManager(self.poetry).get()
if self.option("path"):
if not env.is_venv():
......
......@@ -13,11 +13,10 @@ class EnvListCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
current_env = manager.get(self.poetry.file.parent)
manager = EnvManager(self.poetry)
current_env = manager.get()
for venv in manager.list(self.poetry.file.parent):
for venv in manager.list():
name = venv.path.name
if self.option("full-path"):
name = str(venv.path)
......
......@@ -15,8 +15,7 @@ class EnvRemoveCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
venv = manager.remove(self.argument("python"), poetry.file.parent)
manager = EnvManager(self.poetry)
venv = manager.remove(self.argument("python"))
self.line("Deleted virtualenv: <comment>{}</comment>".format(venv.path))
......@@ -13,14 +13,13 @@ class EnvUseCommand(Command):
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
manager = EnvManager(self.poetry)
if self.argument("python") == "system":
manager.deactivate(poetry.file.parent, self._io)
manager.deactivate(self._io)
return
env = manager.activate(self.argument("python"), poetry.file.parent, self._io)
env = manager.activate(self.argument("python"), self._io)
self.line("Using virtualenv: <comment>{}</>".format(env.path))
......@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import os
import re
import sys
from typing import Dict
from typing import List
......@@ -15,7 +16,6 @@ from tomlkit import inline_table
from poetry.utils._compat import Path
from poetry.utils._compat import OrderedDict
from poetry.utils._compat import urlparse
from poetry.utils.helpers import temporary_directory
from .command import Command
from .env_command import EnvCommand
......@@ -63,7 +63,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig
if (Path.cwd() / "pyproject.toml").exists():
......@@ -126,7 +126,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
question.set_validator(self._validate_license)
license = self.ask(question)
current_env = EnvManager().get(Path.cwd())
current_env = SystemEnv(Path(sys.executable))
default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2])
)
......
......@@ -28,10 +28,7 @@ class ApplicationConfig(BaseApplicationConfig):
self.add_event_listener(ConsoleEvents.PRE_HANDLE.value, self.set_env)
def register_command_loggers(
self,
event, # type: PreHandleEvent
event_name, # type: str
_,
self, event, event_name, _ # type: PreHandleEvent # type: str
): # type: (...) -> None
command = event.command.config.handler
if not isinstance(command, Command):
......@@ -70,25 +67,8 @@ class ApplicationConfig(BaseApplicationConfig):
io = event.io
poetry = command.poetry
env_manager = EnvManager(poetry.config)
# Checking compatibility of the current environment with
# the python dependency specified in pyproject.toml
current_env = env_manager.get(poetry.file.parent)
supported_python = poetry.package.python_constraint
current_python = parse_constraint(
".".join(str(v) for v in current_env.version_info[:3])
)
if not supported_python.allows(current_python):
raise RuntimeError(
"The current Python version ({}) is not supported by the project ({})\n"
"Please activate a compatible Python version.".format(
current_python, poetry.package.python_versions
)
)
env = env_manager.create_venv(poetry.file.parent, io, poetry.package.name)
env_manager = EnvManager(poetry)
env = env_manager.create_venv(io)
if env.is_venv() and io.is_verbose():
io.write_line("Using virtualenv: <comment>{}</>".format(env.path))
......
......@@ -27,7 +27,7 @@ AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?
class Package(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"}
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7", "3.8"}
def __init__(self, name, version, pretty_version=None):
"""
......
......@@ -38,6 +38,7 @@ from poetry.utils.helpers import safe_rmtree
from poetry.utils.helpers import temporary_directory
from poetry.utils.env import EnvManager
from poetry.utils.env import EnvCommandError
from poetry.utils.env import VirtualEnv
from poetry.utils.inspector import Inspector
from poetry.utils.setup_reader import SetupReader
from poetry.utils.toml_file import TomlFile
......@@ -326,9 +327,10 @@ class Provider:
os.chdir(str(directory))
try:
cwd = directory
venv = EnvManager().get(cwd)
venv.run("python", "setup.py", "egg_info")
with temporary_directory() as tmp_dir:
EnvManager.build_venv(tmp_dir)
venv = VirtualEnv(Path(tmp_dir), Path(tmp_dir))
venv.run("python", "setup.py", "egg_info")
except EnvCommandError:
result = SetupReader.read_from_directory(directory)
if not result["name"]:
......
......@@ -20,8 +20,9 @@ from typing import Tuple
from clikit.api.io import IO
from poetry.config.config import Config
from poetry.locations import CACHE_DIR
from poetry.poetry import Poetry
from poetry.semver import parse_constraint
from poetry.semver.version import Version
from poetry.utils._compat import CalledProcessError
from poetry.utils._compat import Path
......@@ -127,6 +128,26 @@ class EnvCommandError(EnvError):
super(EnvCommandError, self).__init__(message)
class NoCompatiblePythonVersionFound(EnvError):
def __init__(self, expected, given=None):
if given:
message = (
"The specified Python version ({}) "
"is not supported by the project ({}).\n"
"Please choose a compatible version "
"or loosen the python constraint specified "
"in the pyproject.toml file.".format(given, expected)
)
else:
message = (
"Poetry was unable to find a compatible version. "
"If you have one, you can explicitly use it "
'via the "env use" command.'
)
super(NoCompatiblePythonVersionFound, self).__init__(message)
class EnvManager(object):
"""
Environments manager
......@@ -136,19 +157,18 @@ class EnvManager(object):
ENVS_FILE = "envs.toml"
def __init__(self, config=None): # type: (Config) -> None
if config is None:
config = Config()
def __init__(self, poetry): # type: (Poetry) -> None
self._poetry = poetry
self._config = config
def activate(self, python, cwd, io): # type: (str, Optional[Path], IO) -> Env
venv_path = self._config.get("virtualenvs.path")
def activate(self, python, io): # type: (str, IO) -> Env
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
cwd = self._poetry.file.parent
envs_file = TomlFile(venv_path / self.ENVS_FILE)
try:
......@@ -182,7 +202,7 @@ class EnvManager(object):
create = False
envs = tomlkit.document()
base_env_name = self.generate_env_name(cwd.name, str(cwd))
base_env_name = self.generate_env_name(self._poetry.package.name, str(cwd))
if envs_file.exists():
envs = envs_file.read()
current_env = envs.get(base_env_name)
......@@ -211,23 +231,23 @@ class EnvManager(object):
if patch != current_patch:
create = True
self.create_venv(cwd, io, executable=python, force=create)
self.create_venv(io, executable=python, force=create)
# Activate
envs[base_env_name] = {"minor": minor, "patch": patch}
envs_file.write(envs)
return self.get(cwd, reload=True)
return self.get(reload=True)
def deactivate(self, cwd, io): # type: (Optional[Path], IO) -> None
venv_path = self._config.get("virtualenvs.path")
def deactivate(self, io): # type: (IO) -> None
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
name = cwd.name
name = self.generate_env_name(name, str(cwd))
name = self._poetry.package.name
name = self.generate_env_name(name, str(self._poetry.file.parent))
envs_file = TomlFile(venv_path / self.ENVS_FILE)
if envs_file.exists():
......@@ -243,21 +263,22 @@ class EnvManager(object):
envs_file.write(envs)
def get(self, cwd, reload=False): # type: (Path, bool) -> Env
def get(self, reload=False): # type: (bool) -> Env
if self._env is not None and not reload:
return self._env
python_minor = ".".join([str(v) for v in sys.version_info[:2]])
venv_path = self._config.get("virtualenvs.path")
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
cwd = self._poetry.file.parent
envs_file = TomlFile(venv_path / self.ENVS_FILE)
env = None
base_env_name = self.generate_env_name(cwd.name, str(cwd))
base_env_name = self.generate_env_name(self._poetry.package.name, str(cwd))
if envs_file.exists():
envs = envs_file.read()
env = envs.get(base_env_name)
......@@ -274,12 +295,12 @@ class EnvManager(object):
return VirtualEnv(venv)
create_venv = self._config.get("virtualenvs.create", True)
create_venv = self._poetry.config.get("virtualenvs.create", True)
if not create_venv:
return SystemEnv(Path(sys.prefix))
venv_path = self._config.get("virtualenvs.path")
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
......@@ -303,13 +324,13 @@ class EnvManager(object):
return VirtualEnv(prefix, base_prefix)
def list(self, cwd, name=None): # type: (Path, Optional[str]) -> List[VirtualEnv]
def list(self, name=None): # type: (Optional[str]) -> List[VirtualEnv]
if name is None:
name = cwd.name
name = self._poetry.package.name
venv_name = self.generate_env_name(name, str(cwd))
venv_name = self.generate_env_name(name, str(self._poetry.file.parent))
venv_path = self._config.get("virtualenvs.path")
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
......@@ -320,18 +341,19 @@ class EnvManager(object):
for p in sorted(venv_path.glob("{}-py*".format(venv_name)))
]
def remove(self, python, cwd): # type: (str, Optional[Path]) -> Env
venv_path = self._config.get("virtualenvs.path")
def remove(self, python): # type: (str) -> Env
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
cwd = self._poetry.file.parent
envs_file = TomlFile(venv_path / self.ENVS_FILE)
base_env_name = self.generate_env_name(cwd.name, str(cwd))
base_env_name = self.generate_env_name(self._poetry.package.name, str(cwd))
if python.startswith(base_env_name):
venvs = self.list(cwd)
venvs = self.list()
for venv in venvs:
if venv.path.name == python:
# Exact virtualenv name
......@@ -413,20 +435,21 @@ class EnvManager(object):
return VirtualEnv(venv)
def create_venv(
self, cwd, io, name=None, executable=None, force=False
): # type: (Path, IO, Optional[str], Optional[str], bool) -> Env
self, io, name=None, executable=None, force=False
): # type: (IO, Optional[str], Optional[str], bool) -> Env
if self._env is not None and not force:
return self._env
env = self.get(cwd, reload=True)
cwd = self._poetry.file.parent
env = self.get(reload=True)
if env.is_venv() and not force:
# Already inside a virtualenv.
return env
create_venv = self._config.get("virtualenvs.create")
root_venv = self._config.get("virtualenvs.in-project")
create_venv = self._poetry.config.get("virtualenvs.create")
root_venv = self._poetry.config.get("virtualenvs.in-project")
venv_path = self._config.get("virtualenvs.path")
venv_path = self._poetry.config.get("virtualenvs.path")
if root_venv:
venv_path = cwd / ".venv"
elif venv_path is None:
......@@ -435,8 +458,9 @@ class EnvManager(object):
venv_path = Path(venv_path)
if not name:
name = cwd.name
name = self._poetry.package.name
python_patch = ".".join([str(v) for v in sys.version_info[:3]])
python_minor = ".".join([str(v) for v in sys.version_info[:2]])
if executable:
python_minor = decode(
......@@ -449,9 +473,84 @@ class EnvManager(object):
]
),
shell=True,
).strip()
)
supported_python = self._poetry.package.python_constraint
if not supported_python.allows(Version.parse(python_minor)):
# The currently activated or chosen Python version
# is not compatible with the Python constraint specified
# for the project.
# If an executable has been specified, we stop there
# and notify the user of the incompatibility.
# Otherwise, we try to find a compatible Python version.
if executable:
raise NoCompatiblePythonVersionFound(
self._poetry.package.python_versions, python_minor
)
io.write_line(
"<warning>The currently activated Python version {} "
"is not supported by the project ({}).\n"
"Trying to find and use a compatible version.</warning> ".format(
python_patch, self._poetry.package.python_versions
)
)
for python_to_try in reversed(
sorted(
self._poetry.package.AVAILABLE_PYTHONS,
key=lambda v: (v.startswith("3"), -len(v), v),
)
):
if len(python_to_try) == 1:
if not parse_constraint("^{}.0".format(python_to_try)).allows_any(
supported_python
):
continue
elif not supported_python.allows_all(
parse_constraint(python_to_try + ".*")
):
continue
python = "python" + python_to_try
if io.is_debug():
io.write_line("<debug>Trying {}</debug>".format(python))
try:
python_patch = decode(
subprocess.check_output(
" ".join(
[
python,
"-c",
"\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"",
]
),
stderr=subprocess.STDOUT,
shell=True,
).strip()
)
except CalledProcessError:
continue
if not python_patch:
continue
if supported_python.allows(Version.parse(python_patch)):
io.write_line(
"Using <info>{}</info> ({})".format(python, python_patch)
)
executable = python
python_minor = ".".join(python_patch.split(".")[:2])
break
if not executable:
raise NoCompatiblePythonVersionFound(
self._poetry.package.python_versions
)
if root_venv:
venv = venv_path
else:
......@@ -503,7 +602,8 @@ class EnvManager(object):
return VirtualEnv(venv)
def build_venv(self, path, executable=None):
@classmethod
def build_venv(cls, path, executable=None):
if executable is not None:
# Create virtualenv by using an external executable
try:
......
import collections
import os
import re
import shutil
import stat
import tempfile
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
from contextlib import contextmanager
from typing import List
from typing import Optional
......@@ -144,11 +148,7 @@ def safe_rmtree(path):
def merge_dicts(d1, d2):
for k, v in d2.items():
if (
k in d1
and isinstance(d1[k], dict)
and isinstance(d2[k], collections.Mapping)
):
if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], Mapping):
merge_dicts(d1[k], d2[k])
else:
d1[k] = d2[k]
......@@ -11,7 +11,7 @@ def test_none_activated(app, tmp_dir):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -34,7 +34,7 @@ def test_activated(app, tmp_dir):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......
......@@ -11,7 +11,7 @@ def test_remove_by_python_version(app, tmp_dir, mocker):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -39,7 +39,7 @@ def test_remove_by_name(app, tmp_dir):
app.poetry.config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......
......@@ -57,7 +57,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(app, tmp_dir, m
tester.execute("3.7")
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
m.assert_called_with(
......@@ -88,7 +88,7 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2])
......@@ -130,7 +130,7 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
"simple-project", str(app.poetry.file.parent)
)
current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2])
......
......@@ -88,6 +88,7 @@ def test_get_metadata_content():
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
]
......
......@@ -216,6 +216,7 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
......@@ -318,6 +319,7 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
......
......@@ -94,6 +94,7 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
......
......@@ -118,7 +118,10 @@ def test_search_for_vcs_read_setup_with_extras(provider, mocker):
def test_search_for_vcs_read_setup_raises_error_if_no_version(provider, mocker):
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
mocker.patch(
"poetry.utils.env.VirtualEnv.run",
side_effect=EnvCommandError(CalledProcessError(1, "python", output="")),
)
dependency = VCSDependency("demo", "git", "https://github.com/demo/no-version.git")
......
......@@ -107,6 +107,7 @@ def test_create_poetry():
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
]
......
......@@ -6,9 +6,11 @@ import tomlkit
from clikit.io import NullIO
from poetry.semver import Version
from poetry.factory import Factory
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from poetry.utils.env import EnvCommandError
from poetry.utils.env import NoCompatiblePythonVersionFound
from poetry.utils.env import VirtualEnv
from poetry.utils.toml_file import TomlFile
......@@ -25,32 +27,56 @@ print("nullpackage loaded"),
"""
def test_virtualenvs_with_spaces_in_their_path_work_as_expected(tmp_dir, config):
@pytest.fixture()
def poetry(config):
poetry = Factory().create_poetry(
Path(__file__).parent.parent / "fixtures" / "simple_project"
)
poetry.set_config(config)
return poetry
@pytest.fixture()
def manager(poetry):
return EnvManager(poetry)
@pytest.fixture
def tmp_venv(tmp_dir, manager):
venv_path = Path(tmp_dir) / "venv"
manager.build_venv(str(venv_path))
venv = VirtualEnv(venv_path)
yield venv
shutil.rmtree(str(venv.path))
def test_virtualenvs_with_spaces_in_their_path_work_as_expected(tmp_dir, manager):
venv_path = Path(tmp_dir) / "Virtual Env"
EnvManager(config).build_venv(str(venv_path))
manager.build_venv(str(venv_path))
venv = VirtualEnv(venv_path)
assert venv.run("python", "-V", shell=True).startswith("Python")
def test_env_get_in_project_venv(tmp_dir, config):
def test_env_get_in_project_venv(manager, poetry):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
(Path(tmp_dir) / ".venv").mkdir()
(poetry.file.parent / ".venv").mkdir()
venv = EnvManager(config).get(Path(tmp_dir))
venv = manager.get()
assert venv.path == Path(tmp_dir) / ".venv"
assert venv.path == poetry.file.parent / ".venv"
shutil.rmtree(str(venv.path))
CWD = Path(__file__).parent.parent / "fixtures" / "simple_project"
def build_venv(path, executable=None):
os.mkdir(path)
......@@ -72,7 +98,7 @@ def check_output_wrapper(version=Version.parse("3.7.1")):
def test_activate_activates_non_existing_virtualenv_no_envs_file(
tmp_dir, config, mocker
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
......@@ -89,8 +115,8 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
env = EnvManager(config).activate("python3.7", CWD, NullIO())
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
env = manager.activate("python3.7", NullIO())
venv_name = EnvManager.generate_env_name("simple-project", str(poetry.file.parent))
m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7"
......@@ -106,11 +132,13 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
assert env.base == Path("/prefix")
def test_activate_activates_existing_virtualenv_no_envs_file(tmp_dir, config, mocker):
def test_activate_activates_existing_virtualenv_no_envs_file(
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
......@@ -126,7 +154,7 @@ def test_activate_activates_existing_virtualenv_no_envs_file(tmp_dir, config, mo
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
env = EnvManager(config).activate("python3.7", CWD, NullIO())
env = manager.activate("python3.7", NullIO())
m.assert_not_called()
......@@ -140,11 +168,13 @@ def test_activate_activates_existing_virtualenv_no_envs_file(tmp_dir, config, mo
assert env.base == Path("/prefix")
def test_activate_activates_same_virtualenv_with_envs_file(tmp_dir, config, mocker):
def test_activate_activates_same_virtualenv_with_envs_file(
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
......@@ -165,7 +195,7 @@ def test_activate_activates_same_virtualenv_with_envs_file(tmp_dir, config, mock
)
m = mocker.patch("poetry.utils.env.EnvManager.create_venv")
env = EnvManager(config).activate("python3.7", CWD, NullIO())
env = manager.activate("python3.7", NullIO())
m.assert_not_called()
......@@ -179,12 +209,12 @@ def test_activate_activates_same_virtualenv_with_envs_file(tmp_dir, config, mock
def test_activate_activates_different_virtualenv_with_envs_file(
tmp_dir, config, mocker
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"}
......@@ -204,7 +234,7 @@ def test_activate_activates_different_virtualenv_with_envs_file(
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
env = EnvManager(config).activate("python3.6", CWD, NullIO())
env = manager.activate("python3.6", NullIO())
m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.6".format(venv_name)), executable="python3.6"
......@@ -219,11 +249,13 @@ def test_activate_activates_different_virtualenv_with_envs_file(
assert env.base == Path("/prefix")
def test_activate_activates_recreates_for_different_patch(tmp_dir, config, mocker):
def test_activate_activates_recreates_for_different_patch(
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"}
......@@ -254,7 +286,7 @@ def test_activate_activates_recreates_for_different_patch(tmp_dir, config, mocke
"poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv
)
env = EnvManager(config).activate("python3.7", CWD, NullIO())
env = manager.activate("python3.7", NullIO())
build_venv_m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7"
......@@ -273,11 +305,13 @@ def test_activate_activates_recreates_for_different_patch(tmp_dir, config, mocke
assert (Path(tmp_dir) / "{}-py3.7".format(venv_name)).exists()
def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker):
def test_activate_does_not_recreate_when_switching_minor(
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"}
......@@ -303,7 +337,7 @@ def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker
"poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv
)
env = EnvManager(config).activate("python3.6", CWD, NullIO())
env = manager.activate("python3.6", NullIO())
build_venv_m.assert_not_called()
remove_venv_m.assert_not_called()
......@@ -318,11 +352,13 @@ def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def test_deactivate_non_activated_but_existing(tmp_dir, config, mocker):
def test_deactivate_non_activated_but_existing(
tmp_dir, manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
(
Path(tmp_dir)
......@@ -336,8 +372,8 @@ def test_deactivate_non_activated_but_existing(tmp_dir, config, mocker):
side_effect=check_output_wrapper(),
)
EnvManager(config).deactivate(CWD, NullIO())
env = EnvManager(config).get(CWD)
manager.deactivate(NullIO())
env = manager.get()
assert env.path == Path(tmp_dir) / "{}-py{}".format(
venv_name, ".".join(str(c) for c in sys.version_info[:2])
......@@ -345,11 +381,11 @@ def test_deactivate_non_activated_but_existing(tmp_dir, config, mocker):
assert Path("/prefix")
def test_deactivate_activated(tmp_dir, config, mocker):
def test_deactivate_activated(tmp_dir, manager, poetry, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
version = Version.parse(".".join(str(c) for c in sys.version_info[:3]))
other_version = Version.parse("3.4") if version.major == 2 else version.next_minor
(
......@@ -375,8 +411,8 @@ def test_deactivate_activated(tmp_dir, config, mocker):
side_effect=check_output_wrapper(),
)
EnvManager(config).deactivate(CWD, NullIO())
env = EnvManager(config).get(CWD)
manager.deactivate(NullIO())
env = manager.get()
assert env.path == Path(tmp_dir) / "{}-py{}.{}".format(
venv_name, version.major, version.minor
......@@ -388,11 +424,11 @@ def test_deactivate_activated(tmp_dir, config, mocker):
def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
tmp_dir, config, mocker
tmp_dir, manager, poetry, config, mocker
):
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
config.merge({"virtualenvs": {"path": str(tmp_dir)}})
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
......@@ -411,30 +447,30 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
side_effect=[("/prefix", None)],
)
env = EnvManager(config).get(CWD)
env = manager.get()
assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name)
assert env.base == Path("/prefix")
def test_list(tmp_dir, config):
def test_list(tmp_dir, manager, poetry, config):
config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
venvs = EnvManager(config).list(CWD)
venvs = manager.list()
assert 2 == len(venvs)
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venvs[0].path
assert (Path(tmp_dir) / "{}-py3.7".format(venv_name)) == venvs[1].path
def test_remove_by_python_version(tmp_dir, config, mocker):
def test_remove_by_python_version(tmp_dir, manager, poetry, config, mocker):
config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -443,18 +479,16 @@ def test_remove_by_python_version(tmp_dir, config, mocker):
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
manager = EnvManager(config)
venv = manager.remove("3.6", CWD)
venv = manager.remove("3.6")
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def test_remove_by_name(tmp_dir, config, mocker):
def test_remove_by_name(tmp_dir, manager, poetry, config, mocker):
config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -463,18 +497,16 @@ def test_remove_by_name(tmp_dir, config, mocker):
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
manager = EnvManager(config)
venv = manager.remove("{}-py3.6".format(venv_name), CWD)
venv = manager.remove("{}-py3.6".format(venv_name))
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def test_remove_also_deactivates(tmp_dir, config, mocker):
def test_remove_also_deactivates(tmp_dir, manager, poetry, config, mocker):
config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
......@@ -488,9 +520,7 @@ def test_remove_also_deactivates(tmp_dir, config, mocker):
doc[venv_name] = {"minor": "3.6", "patch": "3.6.6"}
envs_file.write(doc)
manager = EnvManager(config)
venv = manager.remove("python3.6", CWD)
venv = manager.remove("python3.6")
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
......@@ -499,18 +529,6 @@ def test_remove_also_deactivates(tmp_dir, config, mocker):
assert venv_name not in envs
@pytest.fixture
def tmp_venv(tmp_dir, config, request):
venv_path = Path(tmp_dir) / "venv"
EnvManager(config).build_venv(str(venv_path))
venv = VirtualEnv(venv_path)
yield venv
shutil.rmtree(str(venv.path))
def test_env_has_symlinks_on_nix(tmp_dir, tmp_venv):
venv_available = False
try:
......@@ -533,8 +551,110 @@ def test_run_with_input(tmp_dir, tmp_venv):
def test_run_with_input_non_zero_return(tmp_dir, tmp_venv):
with pytest.raises(EnvCommandError) as processError:
# Test command that will return non-zero returncode.
result = tmp_venv.run("python", "-", input_=ERRORING_SCRIPT)
tmp_venv.run("python", "-", input_=ERRORING_SCRIPT)
assert processError.value.e.returncode == 1
def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ones_first(
manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
poetry.package.python_versions = "^3.6"
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
mocker.patch("sys.version_info", (2, 7, 16))
mocker.patch(
"poetry.utils._compat.subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.7.5")),
)
m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: ""
)
manager.create_venv(NullIO())
m.assert_called_with(
str(Path("/foo/virtualenvs/{}-py3.7".format(venv_name))), executable="python3"
)
def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific_ones(
manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
poetry.package.python_versions = "^3.6"
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
mocker.patch("sys.version_info", (2, 7, 16))
mocker.patch(
"poetry.utils._compat.subprocess.check_output", side_effect=["3.5.3", "3.8.0"]
)
m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: ""
)
manager.create_venv(NullIO())
m.assert_called_with(
str(Path("/foo/virtualenvs/{}-py3.8".format(venv_name))), executable="python3.8"
)
def test_create_venv_fails_if_no_compatible_python_version_could_be_found(
manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
poetry.package.python_versions = "^4.8"
mocker.patch(
"poetry.utils._compat.subprocess.check_output", side_effect=["", "", "", ""]
)
m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: ""
)
with pytest.raises(NoCompatiblePythonVersionFound) as e:
manager.create_venv(NullIO())
expected_message = (
"Poetry was unable to find a compatible version. "
"If you have one, you can explicitly use it "
'via the "env use" command.'
)
assert expected_message == str(e.value)
assert 0 == m.call_count
def test_create_venv_does_not_try_to_find_compatible_versions_with_executable(
manager, poetry, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
poetry.package.python_versions = "^4.8"
mocker.patch("poetry.utils._compat.subprocess.check_output", side_effect=["3.8.0"])
m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=lambda *args, **kwargs: ""
)
with pytest.raises(NoCompatiblePythonVersionFound) as e:
manager.create_venv(NullIO(), executable="3.8")
expected_message = (
"The specified Python version (3.8.0) is not supported by the project (^4.8).\n"
"Please choose a compatible version or loosen the python constraint "
"specified in the pyproject.toml file."
)
assert expected_message == str(e.value)
assert 0 == m.call_count
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment