Commit 92db1f2c by finswimmer Committed by GitHub

Merge pull request #2064 from TheButlah/master

Keep empty in-project venv dir when recreating venv to allow for docker volumes
parents 65e50687 54870697
...@@ -15,6 +15,7 @@ from typing import Dict ...@@ -15,6 +15,7 @@ from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
from typing import Union
import tomlkit import tomlkit
...@@ -403,7 +404,7 @@ class EnvManager(object): ...@@ -403,7 +404,7 @@ class EnvManager(object):
if venv.path.name == python: if venv.path.name == python:
# Exact virtualenv name # Exact virtualenv name
if not envs_file.exists(): if not envs_file.exists():
self.remove_venv(str(venv.path)) self.remove_venv(venv.path)
return venv return venv
...@@ -413,7 +414,7 @@ class EnvManager(object): ...@@ -413,7 +414,7 @@ class EnvManager(object):
current_env = envs.get(base_env_name) current_env = envs.get(base_env_name)
if not current_env: if not current_env:
self.remove_venv(str(venv.path)) self.remove_venv(venv.path)
return venv return venv
...@@ -421,7 +422,7 @@ class EnvManager(object): ...@@ -421,7 +422,7 @@ class EnvManager(object):
del envs[base_env_name] del envs[base_env_name]
envs_file.write(envs) envs_file.write(envs)
self.remove_venv(str(venv.path)) self.remove_venv(venv.path)
return venv return venv
...@@ -475,7 +476,7 @@ class EnvManager(object): ...@@ -475,7 +476,7 @@ class EnvManager(object):
del envs[base_env_name] del envs[base_env_name]
envs_file.write(envs) envs_file.write(envs)
self.remove_venv(str(venv)) self.remove_venv(venv)
return VirtualEnv(venv) return VirtualEnv(venv)
...@@ -621,7 +622,7 @@ class EnvManager(object): ...@@ -621,7 +622,7 @@ class EnvManager(object):
"Creating virtualenv <c1>{}</> in {}".format(name, str(venv_path)) "Creating virtualenv <c1>{}</> in {}".format(name, str(venv_path))
) )
self.build_venv(str(venv), executable=executable) self.build_venv(venv, executable=executable)
else: else:
if force: if force:
if not env.is_sane(): if not env.is_sane():
...@@ -633,8 +634,8 @@ class EnvManager(object): ...@@ -633,8 +634,8 @@ class EnvManager(object):
io.write_line( io.write_line(
"Recreating virtualenv <c1>{}</> in {}".format(name, str(venv)) "Recreating virtualenv <c1>{}</> in {}".format(name, str(venv))
) )
self.remove_venv(str(venv)) self.remove_venv(venv)
self.build_venv(str(venv), executable=executable) self.build_venv(venv, executable=executable)
elif io.is_very_verbose(): elif io.is_very_verbose():
io.write_line("Virtualenv <c1>{}</> already exists.".format(name)) io.write_line("Virtualenv <c1>{}</> already exists.".format(name))
...@@ -657,7 +658,9 @@ class EnvManager(object): ...@@ -657,7 +658,9 @@ class EnvManager(object):
return VirtualEnv(venv) return VirtualEnv(venv)
@classmethod @classmethod
def build_venv(cls, path, executable=None): def build_venv(
cls, path, executable=None
): # type: (Union[Path,str], Optional[str]) -> ()
if executable is not None: if executable is not None:
# Create virtualenv by using an external executable # Create virtualenv by using an external executable
try: try:
...@@ -682,21 +685,41 @@ class EnvManager(object): ...@@ -682,21 +685,41 @@ class EnvManager(object):
use_symlinks = True use_symlinks = True
builder = EnvBuilder(with_pip=True, symlinks=use_symlinks) builder = EnvBuilder(with_pip=True, symlinks=use_symlinks)
builder.create(path) builder.create(str(path))
except ImportError: except ImportError:
try: try:
# We fallback on virtualenv for Python 2.7 # We fallback on virtualenv for Python 2.7
from virtualenv import create_environment from virtualenv import create_environment
create_environment(path) create_environment(str(path))
except ImportError: except ImportError:
# since virtualenv>20 we have to use cli_run # since virtualenv>20 we have to use cli_run
from virtualenv import cli_run from virtualenv import cli_run
cli_run([path]) cli_run([str(path)])
def remove_venv(self, path): # type: (str) -> None @classmethod
shutil.rmtree(path) def remove_venv(cls, path): # type: (Union[Path,str]) -> None
if isinstance(path, str):
path = Path(path)
assert path.is_dir()
try:
shutil.rmtree(str(path))
return
except OSError as e:
# Continue only if e.errno == 16
if e.errno != 16: # ERRNO 16: Device or resource busy
raise e
# Delete all files and folders but the toplevel one. This is because sometimes
# the venv folder is mounted by the OS, such as in a docker volume. In such
# cases, an attempt to delete the folder itself will result in an `OSError`.
# See https://github.com/python-poetry/poetry/pull/2064
for file_path in path.iterdir():
if file_path.is_file() or file_path.is_symlink():
file_path.unlink()
elif file_path.is_dir():
shutil.rmtree(str(file_path))
def get_base_prefix(self): # type: () -> Path def get_base_prefix(self): # type: () -> Path
if hasattr(sys, "real_prefix"): if hasattr(sys, "real_prefix"):
......
import os import os
import shutil
import sys import sys
from typing import Optional
from typing import Union
import tomlkit import tomlkit
from cleo.testers import CommandTester from cleo.testers import CommandTester
...@@ -16,12 +18,8 @@ from poetry.utils.toml_file import TomlFile ...@@ -16,12 +18,8 @@ from poetry.utils.toml_file import TomlFile
CWD = Path(__file__).parent.parent / "fixtures" / "simple_project" CWD = Path(__file__).parent.parent / "fixtures" / "simple_project"
def build_venv(path, executable=None): def build_venv(path, executable=None): # type: (Union[Path,str], Optional[str]) -> ()
os.mkdir(path) os.mkdir(str(path))
def remove_venv(path):
shutil.rmtree(path)
def check_output_wrapper(version=Version.parse("3.7.1")): def check_output_wrapper(version=Version.parse("3.7.1")):
...@@ -62,7 +60,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(app, tmp_dir, m ...@@ -62,7 +60,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(app, tmp_dir, m
) )
m.assert_called_with( m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7" Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7"
) )
envs_file = TomlFile(Path(tmp_dir) / "envs.toml") envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
......
...@@ -2,6 +2,9 @@ import os ...@@ -2,6 +2,9 @@ import os
import shutil import shutil
import sys import sys
from typing import Optional
from typing import Union
import pytest import pytest
import tomlkit import tomlkit
...@@ -83,12 +86,8 @@ def test_env_get_in_project_venv(manager, poetry): ...@@ -83,12 +86,8 @@ def test_env_get_in_project_venv(manager, poetry):
shutil.rmtree(str(venv.path)) shutil.rmtree(str(venv.path))
def build_venv(path, executable=None): def build_venv(path, executable=None): # type: (Union[Path,str], Optional[str]) -> ()
os.mkdir(path) os.mkdir(str(path))
def remove_venv(path):
shutil.rmtree(path)
def check_output_wrapper(version=Version.parse("3.7.1")): def check_output_wrapper(version=Version.parse("3.7.1")):
...@@ -125,7 +124,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( ...@@ -125,7 +124,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
venv_name = EnvManager.generate_env_name("simple-project", str(poetry.file.parent)) venv_name = EnvManager.generate_env_name("simple-project", str(poetry.file.parent))
m.assert_called_with( m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7" Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7"
) )
envs_file = TomlFile(Path(tmp_dir) / "envs.toml") envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
...@@ -243,7 +242,7 @@ def test_activate_activates_different_virtualenv_with_envs_file( ...@@ -243,7 +242,7 @@ def test_activate_activates_different_virtualenv_with_envs_file(
env = manager.activate("python3.6", NullIO()) env = manager.activate("python3.6", NullIO())
m.assert_called_with( m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.6".format(venv_name)), executable="python3.6" Path(tmp_dir) / "{}-py3.6".format(venv_name), executable="python3.6"
) )
assert envs_file.exists() assert envs_file.exists()
...@@ -289,17 +288,15 @@ def test_activate_activates_recreates_for_different_patch( ...@@ -289,17 +288,15 @@ def test_activate_activates_recreates_for_different_patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=build_venv "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv
) )
remove_venv_m = mocker.patch( remove_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv
) )
env = manager.activate("python3.7", NullIO()) env = manager.activate("python3.7", NullIO())
build_venv_m.assert_called_with( build_venv_m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7" Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7"
)
remove_venv_m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name))
) )
remove_venv_m.assert_called_with(Path(tmp_dir) / "{}-py3.7".format(venv_name))
assert envs_file.exists() assert envs_file.exists()
envs = envs_file.read() envs = envs_file.read()
...@@ -340,7 +337,7 @@ def test_activate_does_not_recreate_when_switching_minor( ...@@ -340,7 +337,7 @@ def test_activate_does_not_recreate_when_switching_minor(
"poetry.utils.env.EnvManager.build_venv", side_effect=build_venv "poetry.utils.env.EnvManager.build_venv", side_effect=build_venv
) )
remove_venv_m = mocker.patch( remove_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv "poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv
) )
env = manager.activate("python3.6", NullIO()) env = manager.activate("python3.6", NullIO())
...@@ -535,6 +532,54 @@ def test_remove_also_deactivates(tmp_dir, manager, poetry, config, mocker): ...@@ -535,6 +532,54 @@ def test_remove_also_deactivates(tmp_dir, manager, poetry, config, mocker):
assert venv_name not in envs assert venv_name not in envs
def test_remove_keeps_dir_if_not_deleteable(tmp_dir, manager, poetry, config, mocker):
# Ensure we empty rather than delete folder if its is an active mount point.
# See https://github.com/python-poetry/poetry/pull/2064
config.merge({"virtualenvs": {"path": str(tmp_dir)}})
venv_name = manager.generate_env_name("simple-project", str(poetry.file.parent))
venv_path = Path(tmp_dir) / "{}-py3.6".format(venv_name)
venv_path.mkdir()
folder1_path = venv_path / "folder1"
folder1_path.mkdir()
file1_path = folder1_path / "file1"
file1_path.touch(exist_ok=False)
file2_path = venv_path / "file2"
file2_path.touch(exist_ok=False)
mocker.patch(
"poetry.utils._compat.subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
original_rmtree = shutil.rmtree
def err_on_rm_venv_only(path, *args, **kwargs):
print(path)
if path == str(venv_path):
raise OSError(16, "Test error") # ERRNO 16: Device or resource busy
else:
original_rmtree(path)
m = mocker.patch("shutil.rmtree", side_effect=err_on_rm_venv_only)
venv = manager.remove("{}-py3.6".format(venv_name))
m.assert_any_call(str(venv_path))
assert venv_path == venv.path
assert venv_path.exists()
assert not folder1_path.exists()
assert not file1_path.exists()
assert not file2_path.exists()
m.side_effect = original_rmtree # Avoid teardown using `err_on_rm_venv_only`
def test_env_has_symlinks_on_nix(tmp_dir, tmp_venv): def test_env_has_symlinks_on_nix(tmp_dir, tmp_venv):
venv_available = False venv_available = False
try: try:
...@@ -584,7 +629,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ ...@@ -584,7 +629,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_
manager.create_venv(NullIO()) manager.create_venv(NullIO())
m.assert_called_with( m.assert_called_with(
str(Path("/foo/virtualenvs/{}-py3.7".format(venv_name))), executable="python3" Path("/foo/virtualenvs/{}-py3.7".format(venv_name)), executable="python3"
) )
...@@ -608,7 +653,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific ...@@ -608,7 +653,7 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific
manager.create_venv(NullIO()) manager.create_venv(NullIO())
m.assert_called_with( m.assert_called_with(
str(Path("/foo/virtualenvs/{}-py3.8".format(venv_name))), executable="python3.8" Path("/foo/virtualenvs/{}-py3.8".format(venv_name)), executable="python3.8"
) )
...@@ -691,11 +736,9 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( ...@@ -691,11 +736,9 @@ def test_create_venv_uses_patch_version_to_detect_compatibility(
assert not check_output.called assert not check_output.called
m.assert_called_with( m.assert_called_with(
str( Path(
Path( "/foo/virtualenvs/{}-py{}.{}".format(
"/foo/virtualenvs/{}-py{}.{}".format( venv_name, version.major, version.minor
venv_name, version.major, version.minor
)
) )
), ),
executable=None, executable=None,
...@@ -730,11 +773,9 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( ...@@ -730,11 +773,9 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable(
assert check_output.called assert check_output.called
m.assert_called_with( m.assert_called_with(
str( Path(
Path( "/foo/virtualenvs/{}-py{}.{}".format(
"/foo/virtualenvs/{}-py{}.{}".format( venv_name, version.major, version.minor - 1
venv_name, version.major, version.minor - 1
)
) )
), ),
executable="python{}.{}".format(version.major, version.minor - 1), executable="python{}.{}".format(version.major, version.minor - 1),
...@@ -768,9 +809,7 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( ...@@ -768,9 +809,7 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir(
manager.activate("python3.7", NullIO()) manager.activate("python3.7", NullIO())
m.assert_called_with( m.assert_called_with(poetry.file.parent / ".venv", executable="python3.7")
os.path.join(str(poetry.file.parent), ".venv"), executable="python3.7"
)
envs_file = TomlFile(Path(tmp_dir) / "virtualenvs" / "envs.toml") envs_file = TomlFile(Path(tmp_dir) / "virtualenvs" / "envs.toml")
assert not envs_file.exists() assert not envs_file.exists()
......
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