Repository: pypa/virtualenv

Issue:
Cannot override virtualenv.ini --copies values on commandline
**Issue**

If I set the `--copies` parameter in `~/.config/virtualenv/virtualenv.ini` I cannot override it later with `--symlinks` on the commandline.

**Steps to Reproduce**

Create `virtualenv.ini` file containing

```ini
[virtualenv]
copies = True
```

then run

```console
$ virtualenv default
$ ls -lah default/bin/python*
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 default/bin/python
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 default/bin/python3
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 default/bin/python3.8
$ virtualenv --copies copies
$ ls -lah copies/bin/python*
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 copies/bin/python
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 copies/bin/python3
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 copies/bin/python3.8
$ virtualenv --symlinks symlinks
$ ls -lah symlinks/bin/python*
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 symlinks/bin/python
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 symlinks/bin/python3
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:35 symlinks/bin/python3.8
```

As you can see, there are no symlinks in any of the three virtualenvs. I would expect the last virtualenv to have symlinks instead of copies.

Remove `virtualenv.ini` and retry

```console
$ virtualenv default
$ ls -lah default/bin/python*
lrwxrwxrwx 1 nils nils 15 22. Apr 09:39 default/bin/python -> /usr/bin/python
lrwxrwxrwx 1 nils nils  6 22. Apr 09:39 default/bin/python3 -> python
lrwxrwxrwx 1 nils nils  6 22. Apr 09:39 default/bin/python3.8 -> python
$ virtualenv --copies copies
$ ls -lah copies/bin/python*
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:39 copies/bin/python
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:39 copies/bin/python3
-rwxr-xr-x 1 nils nils 14K 22. Apr 09:39 copies/bin/python3.8
$ virtualenv --symlinks symlinks
$ ls -lah symlinks/bin/python*
lrwxrwxrwx 1 nils nils 15 22. Apr 09:39 symlinks/bin/python -> /usr/bin/python
lrwxrwxrwx 1 nils nils  6 22. Apr 09:39 symlinks/bin/python3 -> python
lrwxrwxrwx 1 nils nils  6 22. Apr 09:39 symlinks/bin/python3.8 -> python
```

and the expected behaviour shows.

**Environment**

Provide at least:
 - OS: Arch Linux Latest
 - `pip list` of the host python where `virtualenv` is installed:

```console
$ pip list
Package             Version    
------------------- -----------
appdirs             1.4.3      
asn1crypto          1.3.0      
attrs               19.3.0     
btrfsutil           1.2.0      
CacheControl        0.12.6     
cachy               0.3.0      
cffi                1.14.0     
chardet             3.0.4      
cleo                0.7.6      
clikit              0.4.2      
colorama            0.4.3      
contextlib2         0.6.0.post1
cryptography        2.8        
distlib             0.3.0      
distro              1.4.0      
docopt              0.6.2      
docutils            0.14       
entrypoints         0.3        
feedparser          5.2.1      
filelock            3.0.12     
html2text           2019.8.11  
html5lib            1.0.1      
idna                2.9        
importlib-metadata  1.5.0      
jeepney             0.4.3      
Jinja2              2.11.2     
jsonschema          3.2.0      
keyring             21.2.0     
lit                 0.10.0.dev0
lockfile            0.12.2     
louis               3.13.0     
MarkupSafe          1.1.1      
meld                3.20.2     
more-itertools      8.2.0      
msgpack             1.0.0      
ordered-set         3.1.1      
packaging           20.3       
pastel              0.2.0      
pep517              0.8.2      
pexpect             4.8.0      
pip                 20.0.2     
pkginfo             1.5.0.1    
ply                 3.11       
poetry              1.0.5      
powerline-gitstatus 1.3.1      
progress            1.5        
ptyprocess          0.6.0      
pwquality           1.4.2      
pycairo             1.19.1     
pycodestyle         2.5.0      
pycparser           2.20       
Pygments            2.6.1      
PyGObject           3.36.0     
pylev               1.3.0      
pyparsing           2.4.7      
pyPEG2              2.15.2     
PyQt5               5.14.2     
PyQt5-sip           12.7.2     
PyQtWebEngine       5.14.0     
pyrsistent          0.16.0     
python-dateutil     2.8.1      
pytoml              0.1.21     
pyxdg               0.26       
PyYAML              5.3.1      
qutebrowser         1.10.2     
requests            2.23.0     
requests-toolbelt   0.9.1      
retrying            1.3.3      
SecretStorage       3.1.2      
setuptools          46.1.3     
shellingham         1.3.1      
six                 1.14.0     
team                1.0        
toml                0.10.0     
tomlkit             0.5.11     
urllib3             1.25.9     
virtualenv          20.0.15    
webencodings        0.5.1      
wheel               0.34.2     
zipp                3.1.0      
  ```

**Output of the virtual environment creation**

Make sure to run the creation with `-vvv --with-traceback`:

```console
$ virtualenv -vvv --with-traceback --symlinks symlinks
23 setup logging to NOTSET [DEBUG report:43]
39 find interpreter for spec PythonSpec(path=/usr/bin/python) [INFO builtin:44]
39 proposed PythonInfo(spec=CPython3.8.2.final.0-64, exe=/usr/bin/python, platform=linux, version='3.8.2 (default, Apr  8 2020, 14:31:25) \n[GCC 9.3.0]', encoding_fs_io=utf-8-utf-8) [INFO builtin:50]
40 accepted PythonInfo(spec=CPython3.8.2.final.0-64, exe=/usr/bin/python, platform=linux, version='3.8.2 (default, Apr  8 2020, 14:31:25) \n[GCC 9.3.0]', encoding_fs_io=utf-8-utf-8) [DEBUG builtin:52]
41 filesystem is case-sensitive [DEBUG info:28]
73 create virtual environment via CPython3Posix(dest=/home/nils/Projekte/hydrogen/symlinks, clear=False, global=False) [INFO session:52]
73 write /home/nils/Projekte/hydrogen/symlinks/pyvenv.cfg [DEBUG pyenv_cfg:34]
73 	home = /usr [DEBUG pyenv_cfg:38]
73 	implementation = CPython [DEBUG pyenv_cfg:38]
73 	version_info = 3.8.2.final.0 [DEBUG pyenv_cfg:38]
73 	virtualenv = 20.0.15 [DEBUG pyenv_cfg:38]
73 	include-system-site-packages = false [DEBUG pyenv_cfg:38]
73 	base-prefix = /usr [DEBUG pyenv_cfg:38]
73 	base-exec-prefix = /usr [DEBUG pyenv_cfg:38]
73 	base-executable = /usr/bin/python [DEBUG pyenv_cfg:38]
74 remove file /home/nils/Projekte/hydrogen/symlinks/bin/python [DEBUG _sync:34]
74 copy /usr/bin/python to /home/nils/Projekte/hydrogen/symlinks/bin/python [DEBUG _sync:48]
74 copy /usr/bin/python to /home/nils/Projekte/hydrogen/symlinks/bin/python3 [DEBUG _sync:48]
74 copy /usr/bin/python to /home/nils/Projekte/hydrogen/symlinks/bin/python3.8 [DEBUG _sync:48]
74 create virtualenv import hook file /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/_virtualenv.pth [DEBUG api:76]
75 create /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/_virtualenv.py [DEBUG api:79]
75 ============================== target debug ============================== [DEBUG session:54]
75 debug via /home/nils/Projekte/hydrogen/symlinks/bin/python /usr/lib/python3.8/site-packages/virtualenv/create/debug.py [DEBUG creator:187]
75 {
  "sys": {
    "executable": "/home/nils/Projekte/hydrogen/symlinks/bin/python",
    "_base_executable": "/home/nils/Projekte/hydrogen/symlinks/bin/python",
    "prefix": "/home/nils/Projekte/hydrogen/symlinks",
    "base_prefix": "/usr",
    "real_prefix": null,
    "exec_prefix": "/home/nils/Projekte/hydrogen/symlinks",
    "base_exec_prefix": "/usr",
    "path": [
      "/usr/lib/python38.zip",
      "/usr/lib/python3.8",
      "/usr/lib/python3.8/lib-dynload",
      "/home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages"
    ],
    "meta_path": [
      "<class '_virtualenv._Finder'>",
      "<class '_frozen_importlib.BuiltinImporter'>",
      "<class '_frozen_importlib.FrozenImporter'>",
      "<class '_frozen_importlib_external.PathFinder'>"
    ],
    "fs_encoding": "utf-8",
    "io_encoding": "utf-8"
  },
  "version": "3.8.2 (default, Apr  8 2020, 14:31:25) \n[GCC 9.3.0]",
  "os": "<module 'os' from '/usr/lib/python3.8/os.py'>",
  "site": "<module 'site' from '/usr/lib/python3.8/site.py'>",
  "datetime": "<module 'datetime' from '/usr/lib/python3.8/datetime.py'>",
  "math": "<module 'math' from '/usr/lib/python3.8/lib-dynload/math.cpython-38-x86_64-linux-gnu.so'>",
  "json": "<module 'json' from '/usr/lib/python3.8/json/__init__.py'>"
} [DEBUG session:55]
102 add seed packages via FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/home/nils/.local/share/virtualenv/seed-app-data/v1.0.1) [INFO session:59]
102 Attempting to acquire lock 140676577462016 on /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels.lock [DEBUG filelock:270]
102 Lock 140676577462016 acquired on /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels.lock [INFO filelock:274]
103 get bundled wheel /usr/lib/python3.8/site-packages/virtualenv/seed/embed/wheels/pip-20.0.2-py2.py3-none-any.whl [DEBUG acquire:51]
104 get bundled wheel /usr/lib/python3.8/site-packages/virtualenv/seed/embed/wheels/setuptools-46.1.3-py3-none-any.whl [DEBUG acquire:51]
104 get bundled wheel /usr/lib/python3.8/site-packages/virtualenv/seed/embed/wheels/wheel-0.34.2-py2.py3-none-any.whl [DEBUG acquire:51]
105 install wheel from wheel /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels/wheel-0.34.2-py2.py3-none-any.whl via CopyPipInstall [DEBUG via_app_data:46]
106 install setuptools from wheel /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels/setuptools-46.1.3-py3-none-any.whl via CopyPipInstall [DEBUG via_app_data:46]
106 install pip from wheel /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels/pip-20.0.2-py2.py3-none-any.whl via CopyPipInstall [DEBUG via_app_data:46]
107 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/wheel-0.34.2-py2.py3-none-any/wheel-0.34.2.dist-info to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/wheel-0.34.2.dist-info [DEBUG _sync:48]
107 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/setuptools-46.1.3-py3-none-any/setuptools-46.1.3.dist-info to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/setuptools-46.1.3.dist-info [DEBUG _sync:48]
114 copy /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/wheel-0.34.2-py2.py3-none-any/wheel-0.34.2.virtualenv to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/wheel-0.34.2.virtualenv [DEBUG _sync:48]
114 copy /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/setuptools-46.1.3-py3-none-any/setuptools-46.1.3.virtualenv to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/setuptools-46.1.3.virtualenv [DEBUG _sync:48]
115 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/wheel-0.34.2-py2.py3-none-any/wheel to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/wheel [DEBUG _sync:48]
115 copy /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/setuptools-46.1.3-py3-none-any/easy_install.py to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/easy_install.py [DEBUG _sync:48]
123 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/setuptools-46.1.3-py3-none-any/pkg_resources to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/pkg_resources [DEBUG _sync:48]
127 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/wheel to 755 [INFO util:566]
127 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/wheel-3.8 to 755 [INFO util:566]
127 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/wheel3 to 755 [INFO util:566]
127 generated console scripts wheel-3.8 wheel wheel3 [DEBUG base:53]
130 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/setuptools-46.1.3-py3-none-any/setuptools to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/setuptools [DEBUG _sync:48]
133 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/pip-20.0.2-py2.py3-none-any/pip to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/pip [DEBUG _sync:48]
144 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/easy_install-3.8 to 755 [INFO util:566]
144 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/easy_install to 755 [INFO util:566]
144 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/easy_install3 to 755 [INFO util:566]
144 generated console scripts easy_install easy_install-3.8 easy_install3 [DEBUG base:53]
168 copy /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/pip-20.0.2-py2.py3-none-any/pip-20.0.2.virtualenv to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/pip-20.0.2.virtualenv [DEBUG _sync:48]
168 copy directory /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/image/CopyPipInstall/pip-20.0.2-py2.py3-none-any/pip-20.0.2.dist-info to /home/nils/Projekte/hydrogen/symlinks/lib/python3.8/site-packages/pip-20.0.2.dist-info [DEBUG _sync:48]
169 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/pip-3.8 to 755 [INFO util:566]
169 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/pip to 755 [INFO util:566]
169 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/pip3 to 755 [INFO util:566]
170 changing mode of /home/nils/Projekte/hydrogen/symlinks/bin/pip3.8 to 755 [INFO util:566]
170 generated console scripts pip3 pip3.8 pip-3.8 pip [DEBUG base:53]
170 Attempting to release lock 140676577462016 on /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels.lock [DEBUG filelock:315]
170 Lock 140676577462016 released on /home/nils/.local/share/virtualenv/seed-app-data/v1.0.1/3.8/wheels.lock [INFO filelock:318]
170 add activators for Bash, CShell, Fish, PowerShell, Python, Xonsh [INFO session:64]
172 write /home/nils/Projekte/hydrogen/symlinks/pyvenv.cfg [DEBUG pyenv_cfg:34]
172 	home = /usr [DEBUG pyenv_cfg:38]
172 	implementation = CPython [DEBUG pyenv_cfg:38]
172 	version_info = 3.8.2.final.0 [DEBUG pyenv_cfg:38]
172 	virtualenv = 20.0.15 [DEBUG pyenv_cfg:38]
172 	include-system-site-packages = false [DEBUG pyenv_cfg:38]
172 	base-prefix = /usr [DEBUG pyenv_cfg:38]
172 	base-exec-prefix = /usr [DEBUG pyenv_cfg:38]
172 	base-executable = /usr/bin/python [DEBUG pyenv_cfg:38]
172 created virtual environment CPython3.8.2.final.0-64 in 150ms
  creator CPython3Posix(dest=/home/nils/Projekte/hydrogen/symlinks, clear=False, global=False)
  seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/home/nils/.local/share/virtualenv/seed-app-data/v1.0.1)
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator [WARNING __main__:21]
```


Discussion:

Patch:
diff --git a/docs/changelog/1784.bugfix.rst b/docs/changelog/1784.bugfix.rst
new file mode 100644
index 00000000..9171bb8b
--- /dev/null
+++ b/docs/changelog/1784.bugfix.rst
@@ -0,0 +1,3 @@
+Better handling of optionlicting :option:`copies` and :option:`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`.
diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py
index edc7f951..e5a54f6b 100644
--- a/src/virtualenv/__main__.py
+++ b/src/virtualenv/__main__.py
@@ -1,11 +1,11 @@
 from __future__ import absolute_import, print_function, unicode_literals
 
-import argparse
 import logging
 import os
 import sys
 from datetime import datetime
 
+from virtualenv.config.cli.parser import VirtualEnvOptions
 from virtualenv.util.six import ensure_text
 
 
@@ -46,7 +46,7 @@ class LogSession(object):
 
 
 def run_with_catch(args=None):
-    options = argparse.Namespace()
+    options = VirtualEnvOptions()
     try:
         run(args, options)
     except (KeyboardInterrupt, Exception) as exception:
diff --git a/src/virtualenv/config/cli/parser.py b/src/virtualenv/config/cli/parser.py
index ef753942..1511deb9 100644
--- a/src/virtualenv/config/cli/parser.py
+++ b/src/virtualenv/config/cli/parser.py
@@ -1,6 +1,6 @@
 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 virtualenv.config.convert import get_type
@@ -9,6 +9,38 @@ from ..env_var import get_env_var
 from ..ini import IniConfig
 
 
+class VirtualEnvOptions(Namespace):
+    def __init__(self, **kwargs):
+        super(VirtualEnvOptions, self).__init__(**kwargs)
+        self._src = None
+        self._sources = {}
+
+    def set_src(self, key, value, src):
+        setattr(self, key, value)
+        if src.startswith("env var"):
+            src = "env var"
+        self._sources[key] = src
+
+    def __setattr__(self, key, value):
+        if getattr(self, "_src", None) is not None:
+            self._sources[key] = self._src
+        super(VirtualEnvOptions, self).__setattr__(key, value)
+
+    def get_source(self, key):
+        return self._sources.get(key)
+
+    @property
+    def verbosity(self):
+        if not hasattr(self, "verbose") and not hasattr(self, "quiet"):
+            return None
+        return max(self.verbose - self.quiet, 0)
+
+    def __repr__(self):
+        return "{}({})".format(
+            type(self).__name__, ", ".join("{}={}".format(k, v) for k, v in vars(self).items() if not k.startswith("_"))
+        )
+
+
 class VirtualEnvConfigParser(ArgumentParser):
     """
     Custom option parser which updates its defaults by checking the configuration files and environmental variables
@@ -24,8 +56,9 @@ class VirtualEnvConfigParser(ArgumentParser):
         super(VirtualEnvConfigParser, self).__init__(*args, **kwargs)
         self._fixed = set()
         self._elements = None
-        self._verbosity = None
-        self._options = options
+        if options is not None and not isinstance(options, VirtualEnvOptions):
+            raise TypeError("options must be of type VirtualEnvOptions")
+        self.options = VirtualEnvOptions() if options is None else options
         self._interpreter = None
         self._app_data = None
 
@@ -52,18 +85,25 @@ class VirtualEnvConfigParser(ArgumentParser):
                         break
             if outcome is not None:
                 action.default, action.default_source = outcome
+            else:
+                outcome = action.default, "default"
+            self.options.set_src(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):
+        if namespace is None:
+            namespace = self.options
+        elif namespace is not self.options:
+            raise ValueError("can only pass in parser.options")
         self._fix_defaults()
-        return super(VirtualEnvConfigParser, self).parse_known_args(args, namespace=namespace)
-
-    def parse_args(self, args=None, namespace=None):
-        self._fix_defaults()
-        return super(VirtualEnvConfigParser, self).parse_args(args, namespace=namespace)
+        self.options._src = "cli"
+        try:
+            return super(VirtualEnvConfigParser, self).parse_known_args(args, namespace=namespace)
+        finally:
+            self.options._src = None
 
 
 class HelpFormatter(ArgumentDefaultsHelpFormatter):
diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py
index 237a5125..a99b077d 100644
--- a/src/virtualenv/create/via_global_ref/api.py
+++ b/src/virtualenv/create/via_global_ref/api.py
@@ -34,11 +34,26 @@ class ViaGlobalRefMeta(CreatorMeta):
 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, symlinks = getattr(options, "copies", False), getattr(options, "symlinks", False)
+        copy_src, sym_src = options.get_source("copies"), options.get_source("symlinks")
+        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)
@@ -50,6 +65,8 @@ class ViaGlobalRefApi(Creator):
             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",
diff --git a/src/virtualenv/report.py b/src/virtualenv/report.py
index e03ccf97..7ae2f241 100644
--- a/src/virtualenv/report.py
+++ b/src/virtualenv/report.py
@@ -18,8 +18,7 @@ MAX_LEVEL = max(LEVELS.keys())
 LOGGER = logging.getLogger()
 
 
-def setup_report(verbose, quiet):
-    verbosity = max(verbose - quiet, 0)
+def setup_report(verbosity):
     _clean_handlers(LOGGER)
     if verbosity > MAX_LEVEL:
         verbosity = MAX_LEVEL  # pragma: no cover
diff --git a/src/virtualenv/run/__init__.py b/src/virtualenv/run/__init__.py
index 6983e33e..7c63ab9a 100644
--- a/src/virtualenv/run/__init__.py
+++ b/src/virtualenv/run/__init__.py
@@ -1,6 +1,5 @@
 from __future__ import absolute_import, unicode_literals
 
-import argparse
 import logging
 
 from virtualenv.run.app_data import AppDataAction
@@ -19,11 +18,9 @@ def cli_run(args, options=None):
     """Create a virtual environment given some command line interface arguments
 
     :param args: the command line arguments
-    :param options: passing in a ``argparse.Namespace`` object allows return of the parsed options
+    :param options: passing in a ``VirtualEnvOptions`` object allows return of the parsed options
     :return: the session object of the creation (its structure for now is experimental and might change on short notice)
     """
-    if options is None:
-        options = argparse.Namespace()
     session = session_via_cli(args, options)
     with session:
         session.run()
@@ -31,11 +28,11 @@ def cli_run(args, options=None):
 
 
 # noinspection PyProtectedMember
-def session_via_cli(args, options):
+def session_via_cli(args, options=None):
     parser = build_parser(args, options)
-    parser.parse_args(args, namespace=parser._options)
-    creator, seeder, activators = tuple(e.create(parser._options) for e in parser._elements)  # create types
-    session = Session(parser._verbosity, options.app_data, parser._interpreter, creator, seeder, activators)
+    options = parser.parse_args(args)
+    creator, seeder, activators = tuple(e.create(options) for e in parser._elements)  # create types
+    session = Session(options.verbosity, options.app_data, parser._interpreter, creator, seeder, activators)
     return session
 
 
@@ -50,7 +47,7 @@ def build_parser(args=None, options=None):
         default=False,
         help="on failure also display the stacktrace internals of virtualenv",
     )
-    parser._options, parser._verbosity = _do_report_setup(parser, args)
+    _do_report_setup(parser, args)
     # here we need a write-able application data (e.g. the zipapp might need this for discovery cache)
     default_app_data = AppDataAction.default()
     parser.add_argument(
@@ -67,7 +64,7 @@ def build_parser(args=None, options=None):
         help="start with empty app data folder",
         default=False,
     )
-    discover = get_discover(parser, args, parser._options)
+    discover = get_discover(parser, args)
     parser._interpreter = interpreter = discover.interpreter
     if interpreter is None:
         raise RuntimeError("failed to find interpreter for {}".format(discover))
@@ -76,9 +73,9 @@ def build_parser(args=None, options=None):
         SeederSelector(interpreter, parser),
         ActivationSelector(interpreter, parser),
     ]
-    parser.parse_known_args(args, namespace=parser._options)
+    options, _ = parser.parse_known_args(args)
     for element in parser._elements:
-        element.handle_selected_arg_parse(parser._options)
+        element.handle_selected_arg_parse(options)
     parser.enable_help()
     return parser
 
@@ -103,6 +100,5 @@ def _do_report_setup(parser, args):
     verbosity = verbosity_group.add_mutually_exclusive_group()
     verbosity.add_argument("-v", "--verbose", action="count", dest="verbose", help="increase verbosity", default=2)
     verbosity.add_argument("-q", "--quiet", action="count", dest="quiet", help="decrease verbosity", default=0)
-    options, _ = parser.parse_known_args(args, namespace=parser._options)
-    verbosity_value = setup_report(options.verbose, options.quiet)
-    return options, verbosity_value
+    option, _ = parser.parse_known_args(args)
+    setup_report(option.verbosity)
diff --git a/src/virtualenv/run/plugin/discovery.py b/src/virtualenv/run/plugin/discovery.py
index 43d5eb2e..03f0590f 100644
--- a/src/virtualenv/run/plugin/discovery.py
+++ b/src/virtualenv/run/plugin/discovery.py
@@ -9,7 +9,7 @@ class Discovery(PluginLoader):
     """"""
 
 
-def get_discover(parser, args, options):
+def get_discover(parser, args):
     discover_types = Discovery.entry_points_for("virtualenv.discovery")
     discovery_parser = parser.add_argument_group(
         title="discovery", description="discover and provide a target interpreter"
@@ -21,7 +21,7 @@ def get_discover(parser, args, options):
         required=False,
         help="interpreter discovery method",
     )
-    options, _ = parser.parse_known_args(args, namespace=options)
+    options, _ = parser.parse_known_args(args)
     if options.app_data == "<temp folder>":
         options.app_data = TempAppData()
     if options.clear_app_data:


Test patch:
diff --git a/tests/unit/config/test_env_var.py b/tests/unit/config/test_env_var.py
index 95c40e30..8eb858d7 100644
--- a/tests/unit/config/test_env_var.py
+++ b/tests/unit/config/test_env_var.py
@@ -1,7 +1,6 @@
 from __future__ import absolute_import, unicode_literals
 
 import os
-from argparse import Namespace
 
 import pytest
 
@@ -10,11 +9,6 @@ from virtualenv.run import session_via_cli
 from virtualenv.util.path import Path
 
 
-def parse_cli(args):
-    options = Namespace()
-    return session_via_cli(args, options)
-
-
 @pytest.fixture()
 def empty_conf(tmp_path, monkeypatch):
     conf = tmp_path / "conf.ini"
@@ -24,13 +18,13 @@ def empty_conf(tmp_path, monkeypatch):
 
 def test_value_ok(monkeypatch, empty_conf):
     monkeypatch.setenv(str("VIRTUALENV_VERBOSE"), str("5"))
-    result = parse_cli(["venv"])
+    result = session_via_cli(["venv"])
     assert result.verbosity == 5
 
 
 def test_value_bad(monkeypatch, caplog, empty_conf):
     monkeypatch.setenv(str("VIRTUALENV_VERBOSE"), str("a"))
-    result = parse_cli(["venv"])
+    result = session_via_cli(["venv"])
     assert result.verbosity == 2
     assert len(caplog.messages) == 1
     assert "env var VIRTUALENV_VERBOSE failed to convert" in caplog.messages[0]
@@ -44,7 +38,7 @@ def test_extra_search_dir_via_env_var(tmp_path, monkeypatch):
     (tmp_path / "a").mkdir()
     (tmp_path / "b").mkdir()
     (tmp_path / "c").mkdir()
-    result = parse_cli(["venv"])
+    result = session_via_cli(["venv"])
     assert result.seeder.extra_search_dir == [Path("a").resolve(), Path("b").resolve(), Path("c").resolve()]
 
 
@@ -65,5 +59,5 @@ def test_value_alias(monkeypatch, mocker, empty_conf):
     monkeypatch.delenv(str("SYMLINKS"), raising=False)
     monkeypatch.delenv(str("VIRTUALENV_COPIES"), raising=False)
     monkeypatch.setenv(str("VIRTUALENV_ALWAYS_COPY"), str("1"))
-    result = parse_cli(["venv"])
+    result = session_via_cli(["venv"])
     assert result.creator.symlinks is False
diff --git a/tests/unit/config/test_ini.py b/tests/unit/config/test_ini.py
new file mode 100644
index 00000000..6b54808d
--- /dev/null
+++ b/tests/unit/config/test_ini.py
@@ -0,0 +1,28 @@
+from __future__ import unicode_literals
+
+from textwrap import dedent
+
+import pytest
+
+from virtualenv.info import fs_supports_symlink
+from virtualenv.run import session_via_cli
+from virtualenv.util.six import ensure_str
+
+
+@pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported")
+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 = session_via_cli(["venv", "--symlinks"])
+
+    symlinks = result.creator.symlinks
+    assert symlinks is True
diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py
index 60f5b866..39fcbbed 100644
--- a/tests/unit/create/test_creator.py
+++ b/tests/unit/create/test_creator.py
@@ -349,7 +349,9 @@ def test_cross_major(cross_python, coverage_env, tmp_path, session_app_data, cur
 
 def test_create_parallel(tmp_path, monkeypatch, temp_app_data):
     def create(count):
-        subprocess.check_call([sys.executable, "-m", "virtualenv", str(tmp_path / "venv{}".format(count))])
+        subprocess.check_call(
+            [sys.executable, "-m", "virtualenv", "-vvv", str(tmp_path / "venv{}".format(count)), "--without-pip"]
+        )
 
     threads = [Thread(target=create, args=(i,)) for i in range(1, 4)]
     for thread in threads:
@@ -386,6 +388,7 @@ def test_create_long_path(current_fastest, tmp_path):
     subprocess.check_call([str(result.creator.script("pip")), "--version"])
 
 
+@pytest.mark.timeout(timeout=60)
 @pytest.mark.parametrize("creator", set(PythonInfo.current_system().creators().key_to_class) - {"builtin"})
 def test_create_distutils_cfg(creator, tmp_path, monkeypatch):
     result = cli_run([ensure_text(str(tmp_path / "venv")), "--activators", "", "--creator", creator])