Skip to content

Commit

Permalink
Better handling of conflicting --copies and --symlinks
Browse files Browse the repository at this point in the history
Introduce priority of where the option is set to follow the order:
cli, env var, file, hard coded. If both set at same level prefer copy
over symlink.

Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Apr 23, 2020
1 parent a5f4721 commit efffb7f
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 7 deletions.
3 changes: 3 additions & 0 deletions docs/changelog/1784.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Better handling of conflicting :conf:`copies` and :conf:`symlinks`. Introduce priority of where the option is set to
follow the order: CLI, env var, file, hardcoded. If both set at same level prefers copy over symlink. - by
user:`gaborbernat`.
35 changes: 31 additions & 4 deletions src/virtualenv/config/cli/parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import absolute_import, unicode_literals

from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser
from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from collections import OrderedDict
from contextlib import contextmanager

from virtualenv.config.convert import get_type

Expand All @@ -28,6 +29,7 @@ def __init__(self, options=None, *args, **kwargs):
self._options = options
self._interpreter = None
self._app_data = None
self._source = {}

def _fix_defaults(self):
for action in self._actions:
Expand All @@ -52,19 +54,44 @@ def _fix_default(self, action):
break
if outcome is not None:
action.default, action.default_source = outcome
else:
outcome = action.default, "default"
if outcome[1].startswith("env var"):
outcome = outcome[0], "env var"
self._source[action.dest] = outcome

def enable_help(self):
self._fix_defaults()
self.add_argument("-h", "--help", action="help", default=SUPPRESS, help="show this help message and exit")

def parse_known_args(self, args=None, namespace=None):
self._fix_defaults()
return super(VirtualEnvConfigParser, self).parse_known_args(args, namespace=namespace)
with self._record_arg_src(namespace) as namespace:
return super(VirtualEnvConfigParser, self).parse_known_args(args, namespace=namespace)

def parse_args(self, args=None, namespace=None):
self._fix_defaults()
# this delegates to parse_known_args
return super(VirtualEnvConfigParser, self).parse_args(args, namespace=namespace)

@contextmanager
def _record_arg_src(self, namespace):
inst = object()
self._fix_defaults()
if namespace is None:
namespace = Namespace()
for key in self._source:
setattr(namespace, key, inst)
try:
yield namespace
finally:
source = self._source.copy()
for key, default in self._source.items():
value = getattr(namespace, key)
if value is inst:
setattr(namespace, key, default[0])
else:
source[key] = (value, "cli")
namespace.key_to_source = source


class HelpFormatter(ArgumentDefaultsHelpFormatter):
def __init__(self, prog):
Expand Down
24 changes: 21 additions & 3 deletions src/virtualenv/create/via_global_ref/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,27 @@ def can_symlink(self):
class ViaGlobalRefApi(Creator):
def __init__(self, options, interpreter):
super(ViaGlobalRefApi, self).__init__(options, interpreter)
copies = getattr(options, "copies", False)
symlinks = getattr(options, "symlinks", False)
self.symlinks = symlinks is True and copies is False
self.symlinks = self._should_symlink(options)
self.enable_system_site_package = options.system_site

@staticmethod
def _should_symlink(options):
# Priority of where the option is set to follow the order: CLI, env var, file, hardcoded.
# If both set at same level prefers copy over symlink.
copies_opt, symlinks_opt = options.key_to_source.get("copies"), options.key_to_source.get("symlinks")
symlinks, sym_src = symlinks_opt if symlinks_opt is not None else (False, None)
copies, copy_src = copies_opt if copies_opt is not None else (False, None)
for level in ["cli", "env var", "file", "default"]:
s_opt = symlinks if sym_src == level else None
c_opt = copies if copy_src == level else None
if s_opt is True and c_opt is True:
return False
if s_opt is True:
return True
if c_opt is True:
return False
return False # fallback to copy

@classmethod
def add_parser_arguments(cls, parser, interpreter, meta, app_data):
super(ViaGlobalRefApi, cls).add_parser_arguments(parser, interpreter, meta, app_data)
Expand All @@ -50,6 +66,8 @@ def add_parser_arguments(cls, parser, interpreter, meta, app_data):
help="give the virtual environment access to the system site-packages dir",
)
group = parser.add_mutually_exclusive_group()
if not meta.can_symlink and not meta.can_copy:
raise RuntimeError("neither symlink or copy method supported")
if meta.can_symlink:
group.add_argument(
"--symlinks",
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/config/test_ini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from textwrap import dedent

from tests.unit.config.test_env_var import parse_cli
from virtualenv.util.six import ensure_str


def test_ini_can_be_overwritten_by_flag(tmp_path, monkeypatch):
custom_ini = tmp_path / "conf.ini"
custom_ini.write_text(
dedent(
"""
[virtualenv]
copies = True
"""
)
)
monkeypatch.setenv(ensure_str("VIRTUALENV_CONFIG_FILE"), str(custom_ini))

result = parse_cli(["venv", "--symlinks"])

symlinks = result.creator.symlinks
assert symlinks is True

0 comments on commit efffb7f

Please sign in to comment.