Commit cb34c3d0 by finswimmer Committed by GitHub

config: support virtualenv `--always-copy` option

Mitigates: #3134
parent 9c8155c6
...@@ -34,6 +34,7 @@ which will give you something similar to this: ...@@ -34,6 +34,7 @@ which will give you something similar to this:
cache-dir = "/path/to/cache/directory" cache-dir = "/path/to/cache/directory"
virtualenvs.create = true virtualenvs.create = true
virtualenvs.in-project = null virtualenvs.in-project = null
virtualenvs.options.always-copy = true
virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs virtualenvs.path = "{cache-dir}/virtualenvs" # /path/to/cache/directory/virtualenvs
``` ```
...@@ -128,6 +129,12 @@ If not set explicitly (default), `poetry` will use the virtualenv from the `.ven ...@@ -128,6 +129,12 @@ If not set explicitly (default), `poetry` will use the virtualenv from the `.ven
Directory where virtual environments will be created. Directory where virtual environments will be created.
Defaults to `{cache-dir}/virtualenvs` (`{cache-dir}\virtualenvs` on Windows). Defaults to `{cache-dir}/virtualenvs` (`{cache-dir}\virtualenvs` on Windows).
### `virtualenvs.options.always-copy`: boolean
If set to `true` the `--always-copy` parameter is passed to `virtualenv` on creation of the venv. Thus all needed files are copied into the venv instead of symlinked.
Defaults to `false`.
### `repositories.<name>`: string ### `repositories.<name>`: string
Set a new alternative repository. See [Repositories](/docs/repositories/) for more information. Set a new alternative repository. See [Repositories](/docs/repositories/) for more information.
...@@ -36,6 +36,7 @@ class Config(object): ...@@ -36,6 +36,7 @@ class Config(object):
"create": True, "create": True,
"in-project": None, "in-project": None,
"path": os.path.join("{cache-dir}", "virtualenvs"), "path": os.path.join("{cache-dir}", "virtualenvs"),
"options": {"always-copy": False},
}, },
"experimental": {"new-installer": True}, "experimental": {"new-installer": True},
} }
...@@ -87,7 +88,11 @@ class Config(object): ...@@ -87,7 +88,11 @@ class Config(object):
for key in config: for key in config:
value = self.get(parent_key + key) value = self.get(parent_key + key)
if isinstance(value, dict): if isinstance(value, dict):
all_[key] = _all(config[key], parent_key=key + ".") if parent_key != "":
current_parent = parent_key + key + "."
else:
current_parent = key + "."
all_[key] = _all(config[key], parent_key=current_parent)
continue continue
all_[key] = value all_[key] = value
...@@ -131,14 +136,22 @@ class Config(object): ...@@ -131,14 +136,22 @@ class Config(object):
return re.sub(r"{(.+?)}", lambda m: self.get(m.group(1)), value) return re.sub(r"{(.+?)}", lambda m: self.get(m.group(1)), value)
def _get_validator(self, name): # type: (str) -> Callable def _get_validator(self, name): # type: (str) -> Callable
if name in {"virtualenvs.create", "virtualenvs.in-project"}: if name in {
"virtualenvs.create",
"virtualenvs.in-project",
"virtualenvs.options.always-copy",
}:
return boolean_validator return boolean_validator
if name == "virtualenvs.path": if name == "virtualenvs.path":
return str return str
def _get_normalizer(self, name): # type: (str) -> Callable def _get_normalizer(self, name): # type: (str) -> Callable
if name in {"virtualenvs.create", "virtualenvs.in-project"}: if name in {
"virtualenvs.create",
"virtualenvs.in-project",
"virtualenvs.options.always-copy",
}:
return boolean_normalizer return boolean_normalizer
if name == "virtualenvs.path": if name == "virtualenvs.path":
......
...@@ -64,6 +64,11 @@ To remove a repository (repo is a short alias for repositories): ...@@ -64,6 +64,11 @@ To remove a repository (repo is a short alias for repositories):
boolean_normalizer, boolean_normalizer,
True, True,
), ),
"virtualenvs.options.always-copy": (
boolean_validator,
boolean_normalizer,
False,
),
} }
return unique_config_values return unique_config_values
......
...@@ -642,7 +642,11 @@ class EnvManager(object): ...@@ -642,7 +642,11 @@ class EnvManager(object):
"Creating virtualenv <c1>{}</> in {}".format(name, str(venv_path)) "Creating virtualenv <c1>{}</> in {}".format(name, str(venv_path))
) )
self.build_venv(venv, executable=executable) self.build_venv(
venv,
executable=executable,
flags=self._poetry.config.get("virtualenvs.options"),
)
else: else:
if force: if force:
if not env.is_sane(): if not env.is_sane():
...@@ -655,7 +659,11 @@ class EnvManager(object): ...@@ -655,7 +659,11 @@ class EnvManager(object):
"Recreating virtualenv <c1>{}</> in {}".format(name, str(venv)) "Recreating virtualenv <c1>{}</> in {}".format(name, str(venv))
) )
self.remove_venv(venv) self.remove_venv(venv)
self.build_venv(venv, executable=executable) self.build_venv(
venv,
executable=executable,
flags=self._poetry.config.get("virtualenvs.options"),
)
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))
...@@ -679,19 +687,26 @@ class EnvManager(object): ...@@ -679,19 +687,26 @@ class EnvManager(object):
@classmethod @classmethod
def build_venv( def build_venv(
cls, path, executable=None cls, path, executable=None, flags=None
): # type: (Union[Path,str], Optional[Union[str, Path]]) -> virtualenv.run.session.Session ): # type: (Union[Path,str], Optional[Union[str, Path]], Dict[str, bool]) -> virtualenv.run.session.Session
flags = flags or {}
if isinstance(executable, Path): if isinstance(executable, Path):
executable = executable.resolve().as_posix() executable = executable.resolve().as_posix()
return virtualenv.cli_run(
[ args = [
"--no-download", "--no-download",
"--no-periodic-update", "--no-periodic-update",
"--python", "--python",
executable or sys.executable, executable or sys.executable,
str(path), str(path),
] ]
)
for flag, value in flags.items():
if value is True:
args.insert(0, "--{}".format(flag))
return virtualenv.cli_run(args)
@classmethod @classmethod
def remove_venv(cls, path): # type: (Union[Path,str]) -> None def remove_venv(cls, path): # type: (Union[Path,str]) -> None
......
...@@ -5,7 +5,9 @@ from poetry.core.semver import Version ...@@ -5,7 +5,9 @@ from poetry.core.semver import Version
from poetry.utils._compat import Path from poetry.utils._compat import Path
def build_venv(path, executable=None): # type: (Union[Path,str], Optional[str]) -> () def build_venv(
path, executable=None, flags=None
): # type: (Union[Path,str], Optional[str], bool) -> ()
Path(path).mkdir(parents=True, exist_ok=True) Path(path).mkdir(parents=True, exist_ok=True)
......
...@@ -50,7 +50,9 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( ...@@ -50,7 +50,9 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
tester.execute("3.7") tester.execute("3.7")
venv_py37 = venv_cache / "{}-py3.7".format(venv_name) venv_py37 = venv_cache / "{}-py3.7".format(venv_name)
mock_build_env.assert_called_with(venv_py37, executable="python3.7") mock_build_env.assert_called_with(
venv_py37, executable="python3.7", flags={"always-copy": False}
)
envs_file = TOMLFile(venv_cache / "envs.toml") envs_file = TOMLFile(venv_cache / "envs.toml")
assert envs_file.exists() assert envs_file.exists()
......
...@@ -30,6 +30,7 @@ def test_list_displays_default_value_if_not_set(tester, config): ...@@ -30,6 +30,7 @@ def test_list_displays_default_value_if_not_set(tester, config):
experimental.new-installer = true experimental.new-installer = true
virtualenvs.create = true virtualenvs.create = true
virtualenvs.in-project = null virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.path = {path} # /foo{sep}virtualenvs virtualenvs.path = {path} # /foo{sep}virtualenvs
""".format( """.format(
path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep
...@@ -47,6 +48,7 @@ def test_list_displays_set_get_setting(tester, config): ...@@ -47,6 +48,7 @@ def test_list_displays_set_get_setting(tester, config):
experimental.new-installer = true experimental.new-installer = true
virtualenvs.create = false virtualenvs.create = false
virtualenvs.in-project = null virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.path = {path} # /foo{sep}virtualenvs virtualenvs.path = {path} # /foo{sep}virtualenvs
""".format( """.format(
path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep
...@@ -86,6 +88,7 @@ def test_list_displays_set_get_local_setting(tester, config): ...@@ -86,6 +88,7 @@ def test_list_displays_set_get_local_setting(tester, config):
experimental.new-installer = true experimental.new-installer = true
virtualenvs.create = false virtualenvs.create = false
virtualenvs.in-project = null virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.path = {path} # /foo{sep}virtualenvs virtualenvs.path = {path} # /foo{sep}virtualenvs
""".format( """.format(
path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep path=json.dumps(os.path.join("{cache-dir}", "virtualenvs")), sep=os.path.sep
......
...@@ -118,7 +118,9 @@ def test_env_get_venv_with_venv_folder_present( ...@@ -118,7 +118,9 @@ def test_env_get_venv_with_venv_folder_present(
assert venv.path == in_project_venv_dir assert venv.path == in_project_venv_dir
def build_venv(path, executable=None): # type: (Union[Path,str], Optional[str]) -> () def build_venv(
path, executable=None, flags=None
): # type: (Union[Path,str], Optional[str], bool) -> ()
os.mkdir(str(path)) os.mkdir(str(path))
...@@ -156,7 +158,9 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file( ...@@ -156,7 +158,9 @@ 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(
Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7" Path(tmp_dir) / "{}-py3.7".format(venv_name),
executable="python3.7",
flags={"always-copy": False},
) )
envs_file = TOMLFile(Path(tmp_dir) / "envs.toml") envs_file = TOMLFile(Path(tmp_dir) / "envs.toml")
...@@ -274,7 +278,9 @@ def test_activate_activates_different_virtualenv_with_envs_file( ...@@ -274,7 +278,9 @@ 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(
Path(tmp_dir) / "{}-py3.6".format(venv_name), executable="python3.6" Path(tmp_dir) / "{}-py3.6".format(venv_name),
executable="python3.6",
flags={"always-copy": False},
) )
assert envs_file.exists() assert envs_file.exists()
...@@ -326,7 +332,9 @@ def test_activate_activates_recreates_for_different_patch( ...@@ -326,7 +332,9 @@ def test_activate_activates_recreates_for_different_patch(
env = manager.activate("python3.7", NullIO()) env = manager.activate("python3.7", NullIO())
build_venv_m.assert_called_with( build_venv_m.assert_called_with(
Path(tmp_dir) / "{}-py3.7".format(venv_name), executable="python3.7" Path(tmp_dir) / "{}-py3.7".format(venv_name),
executable="python3.7",
flags={"always-copy": False},
) )
remove_venv_m.assert_called_with(Path(tmp_dir) / "{}-py3.7".format(venv_name)) remove_venv_m.assert_called_with(Path(tmp_dir) / "{}-py3.7".format(venv_name))
...@@ -654,7 +662,9 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ ...@@ -654,7 +662,9 @@ 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(
Path("/foo/virtualenvs/{}-py3.7".format(venv_name)), executable="python3" Path("/foo/virtualenvs/{}-py3.7".format(venv_name)),
executable="python3",
flags={"always-copy": False},
) )
...@@ -678,7 +688,9 @@ def test_create_venv_tries_to_find_a_compatible_python_executable_using_specific ...@@ -678,7 +688,9 @@ 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(
Path("/foo/virtualenvs/{}-py3.9".format(venv_name)), executable="python3.9" Path("/foo/virtualenvs/{}-py3.9".format(venv_name)),
executable="python3.9",
flags={"always-copy": False},
) )
...@@ -767,6 +779,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility( ...@@ -767,6 +779,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility(
) )
), ),
executable=None, executable=None,
flags={"always-copy": False},
) )
...@@ -804,6 +817,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable( ...@@ -804,6 +817,7 @@ def test_create_venv_uses_patch_version_to_detect_compatibility_with_executable(
) )
), ),
executable="python{}.{}".format(version.major, version.minor - 1), executable="python{}.{}".format(version.major, version.minor - 1),
flags={"always-copy": False},
) )
...@@ -834,7 +848,11 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir( ...@@ -834,7 +848,11 @@ 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(poetry.file.parent / ".venv", executable="python3.7") m.assert_called_with(
poetry.file.parent / ".venv",
executable="python3.7",
flags={"always-copy": False},
)
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