Repository: tox-dev/tox

Issue:
tox4: sitepackages support
👍🏻 

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=h1) Report
> Merging [#1862](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=desc) (baad3a8) into [rewrite](https://codecov.io/gh/tox-dev/tox/commit/5f172bc47b85dc24c706a2f33c3917089e0554f4?el=desc) (5f172bc) will **increase** coverage by `0.20%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tox-dev/tox/pull/1862/graphs/tree.svg?width=650&height=150&src=pr&token=DYodAwDCZ5)](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##           rewrite     #1862      +/-   ##
============================================
+ Coverage    99.79%   100.00%   +0.20%     
============================================
  Files          133       133              
  Lines         7228      7215      -13     
  Branches       719       701      -18     
============================================
+ Hits          7213      7215       +2     
+ Misses           3         0       -3     
+ Partials        12         0      -12     
```

| Flag | Coverage Δ | |
|---|---|---|
| tests | `100.00% <100.00%> (+0.20%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/tox/config/cli/parser.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-c3JjL3RveC9jb25maWcvY2xpL3BhcnNlci5weQ==) | `100.00% <100.00%> (+0.54%)` | :arrow_up: |
| [src/tox/tox\_env/api.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L2FwaS5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/tox/tox\_env/python/api.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi9hcGkucHk=) | `100.00% <100.00%> (+0.68%)` | :arrow_up: |
| [src/tox/tox\_env/python/virtual\_env/api.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi92aXJ0dWFsX2Vudi9hcGkucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/config/test\_set\_env.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-dGVzdHMvY29uZmlnL3Rlc3Rfc2V0X2Vudi5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/session/cmd/test\_show\_config.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-dGVzdHMvc2Vzc2lvbi9jbWQvdGVzdF9zaG93X2NvbmZpZy5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/test\_provision.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9wcm92aXNpb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/tox\_env/python/virtual\_env/test\_package.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-dGVzdHMvdG94X2Vudi9weXRob24vdmlydHVhbF9lbnYvdGVzdF9wYWNrYWdlLnB5) | `100.00% <100.00%> (ø)` | |
| [.../tox\_env/python/virtual\_env/test\_virtualenv\_api.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-dGVzdHMvdG94X2Vudi9weXRob24vdmlydHVhbF9lbnYvdGVzdF92aXJ0dWFsZW52X2FwaS5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/tox/pytest.py](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree#diff-c3JjL3RveC9weXRlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| ... and [15 more](https://codecov.io/gh/tox-dev/tox/pull/1862/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=footer). Last update [5f172bc...6d50aab](https://codecov.io/gh/tox-dev/tox/pull/1862?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/changelog/1847.feature.rst b/docs/changelog/1847.feature.rst
new file mode 100644
index 00000000..3be70211
--- /dev/null
+++ b/docs/changelog/1847.feature.rst
@@ -0,0 +1,2 @@
+Support the ``system_site_packages``/``sitepackages`` flag for virtual environment based tox environments -
+by :user:`gaborbernat`.
diff --git a/docs/changelog/1848.feature.rst b/docs/changelog/1848.feature.rst
new file mode 100644
index 00000000..31000a91
--- /dev/null
+++ b/docs/changelog/1848.feature.rst
@@ -0,0 +1,2 @@
+Support the ``always_copy``/``alwayscopy`` flag for virtual environment based tox environments -
+by :user:`gaborbernat`.
diff --git a/docs/changelog/1849.feature.rst b/docs/changelog/1849.feature.rst
new file mode 100644
index 00000000..add6fbfd
--- /dev/null
+++ b/docs/changelog/1849.feature.rst
@@ -0,0 +1,1 @@
+Support the ``download`` flag for virtual environment based tox environments - by :user:`gaborbernat`.
diff --git a/setup.cfg b/setup.cfg
index c5590f19..c56afc5e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -112,7 +112,6 @@ subtract_omit = */.tox/*
 
 [mypy]
 python_version = 3.6
-disallow_any_unimported = True
 disallow_any_generics = True
 disallow_subclassing_any = True
 disallow_untyped_calls = True
diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py
index b1804793..f47029ba 100644
--- a/src/tox/config/cli/parser.py
+++ b/src/tox/config/cli/parser.py
@@ -87,7 +87,7 @@ class HelpFormatter(ArgumentDefaultsHelpFormatter):
         text: str = super()._get_help_string(action) or ""  # noqa
         if hasattr(action, "default_source"):
             default = " (default: %(default)s)"
-            if text.endswith(default):
+            if text.endswith(default):  # pragma: no branch
                 text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)"
         return text
 
diff --git a/src/tox/execute/local_sub_process/read_via_thread_windows.py b/src/tox/execute/local_sub_process/read_via_thread_windows.py
index 9bec8962..1f4d29a1 100644
--- a/src/tox/execute/local_sub_process/read_via_thread_windows.py
+++ b/src/tox/execute/local_sub_process/read_via_thread_windows.py
@@ -15,7 +15,7 @@ class ReadViaThreadWindows(ReadViaThread):  # pragma: win32 cover
     def __init__(self, file_no: int, handler: Callable[[bytes], None], name: str, drain: bool) -> None:
         super().__init__(file_no, handler, name, drain)
         self.closed = False
-        self._ov: Optional[_overlapped.Overlapped] = None  # type: ignore[no-any-unimported]
+        self._ov: Optional[_overlapped.Overlapped] = None
         self._waiting_for_read = False
 
     def _read_stream(self) -> None:
diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py
index ee3e42f8..55edb57c 100644
--- a/src/tox/plugin/manager.py
+++ b/src/tox/plugin/manager.py
@@ -21,7 +21,7 @@ from . import NAME, spec
 
 class Plugin:
     def __init__(self) -> None:
-        self.manager: pluggy.PluginManager = pluggy.PluginManager(NAME)  # type: ignore[no-any-unimported]
+        self.manager: pluggy.PluginManager = pluggy.PluginManager(NAME)
         self.manager.add_hookspecs(spec)
 
         internal_plugins = (
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index fe8888a5..1bff9fec 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -210,10 +210,26 @@ class ToxEnv(ABC):
         set_env: SetEnv = self.conf["set_env"]
         for key in set_env:
             result[key] = set_env.load(key)
-        result["PATH"] = os.pathsep.join([str(i) for i in self._paths] + os.environ.get("PATH", "").split(os.pathsep))
+        result["PATH"] = self.paths_env()
         self._env_vars = result
         return result
 
+    @property
+    def paths(self) -> List[Path]:
+        return self._paths
+
+    @paths.setter
+    def paths(self, value: List[Path]) -> None:
+        self._paths = value
+        if self._env_vars is not None:  # pragma: no branch # also update the environment variables with the new value
+            self._env_vars["PATH"] = self.paths_env()
+
+    def paths_env(self) -> str:
+        # remove duplicates and prepend the tox env paths
+        values = dict.fromkeys(str(i) for i in self.paths)
+        values.update(dict.fromkeys(os.environ.get("PATH", "").split(os.pathsep)))
+        return os.pathsep.join(values)
+
     def execute(
         self,
         cmd: Sequence[Union[Path, str]],
diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py
index f2950dd7..cc2ffd68 100644
--- a/src/tox/tox_env/python/api.py
+++ b/src/tox/tox_env/python/api.py
@@ -158,9 +158,13 @@ class Python(ToxEnv, ABC):
         with self._cache.compare(conf, Python.__name__) as (eq, old):
             if eq is False:  # if changed create
                 self.create_python_env()
-            self._paths = self.paths()
+        self.paths = self.python_env_paths()  # now that the environment exist we can add them to the path
         super().setup()
 
+    @abstractmethod
+    def python_env_paths(self) -> List[Path]:
+        raise NotImplementedError
+
     def setup_has_been_done(self) -> None:
         """called when setup is done"""
         super().setup_has_been_done()
@@ -227,10 +231,6 @@ class Python(ToxEnv, ABC):
     def create_python_env(self) -> None:
         raise NotImplementedError
 
-    @abstractmethod
-    def paths(self) -> List[Path]:
-        raise NotImplementedError
-
     @abstractmethod
     def install_python_packages(self, packages: PythonDeps, of_type: str, no_deps: bool = False) -> None:
         raise NotImplementedError
diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py
index 5af0a504..fb28a5b7 100644
--- a/src/tox/tox_env/python/virtual_env/api.py
+++ b/src/tox/tox_env/python/virtual_env/api.py
@@ -10,6 +10,7 @@ from virtualenv.create.creator import Creator
 from virtualenv.run.session import Session
 
 from tox.config.cli.parser import DEFAULT_VERBOSITY, Parsed
+from tox.config.loader.str_convert import StrConvert
 from tox.config.sets import CoreConfigSet, EnvConfigSet
 from tox.execute.api import Execute, Outcome, StdinSource
 from tox.execute.local_sub_process import LocalSubProcessExecutor
@@ -25,9 +26,38 @@ class VirtualEnv(Python, ABC):
     def __init__(
         self, conf: EnvConfigSet, core: CoreConfigSet, options: Parsed, journal: EnvJournal, log_handler: ToxHandler
     ) -> None:
-        self._virtualenv_session: Optional[Session] = None  # type: ignore[no-any-unimported]
+        self._virtualenv_session: Optional[Session] = None
         super().__init__(conf, core, options, journal, log_handler)
 
+    def register_config(self) -> None:
+        super().register_config()
+        self.conf.add_config(
+            keys=["system_site_packages", "sitepackages"],
+            of_type=bool,
+            default=lambda conf, name: StrConvert().to_bool(
+                self.environment_variables.get("VIRTUALENV_SYSTEM_SITE_PACKAGES", "False")
+            ),
+            desc="create virtual environments that also have access to globally installed packages.",
+        )
+        self.conf.add_config(
+            keys=["always_copy", "alwayscopy"],
+            of_type=bool,
+            default=lambda conf, name: StrConvert().to_bool(
+                self.environment_variables.get(
+                    "VIRTUALENV_COPIES", self.environment_variables.get("VIRTUALENV_ALWAYS_COPY", "False")
+                )
+            ),
+            desc="force virtualenv to always copy rather than symlink",
+        )
+        self.conf.add_config(
+            keys=["download"],
+            of_type=bool,
+            default=lambda conf, name: StrConvert().to_bool(
+                self.environment_variables.get("VIRTUALENV_DOWNLOAD", "False")
+            ),
+            desc="true if you want virtualenv to upgrade pip/wheel/setuptools to the latest version",
+        )
+
     def default_pass_env(self) -> List[str]:
         env = super().default_pass_env()
         env.append("PIP_*")  # we use pip as installer
@@ -37,28 +67,37 @@ class VirtualEnv(Python, ABC):
     def default_set_env(self) -> Dict[str, str]:
         env = super().default_set_env()
         env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
-        env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "1"
         return env
 
     def build_executor(self) -> Execute:
         return LocalSubProcessExecutor(self.options.is_colored)
 
     @property
-    def session(self) -> Session:  # type: ignore[no-any-unimported]
+    def session(self) -> Session:
         if self._virtualenv_session is None:
-            args = [
-                "--clear",
-                "--no-periodic-update",
-                str(cast(Path, self.conf["env_dir"])),
-            ]
-            base_python: List[str] = self.conf["base_python"]
-            for base in base_python:
-                args.extend(["-p", base])
-            self._virtualenv_session = session_via_cli(args, setup_logging=False)
+            self._virtualenv_session = session_via_cli(
+                [str(cast(Path, self.conf["env_dir"]))],
+                options=None,
+                setup_logging=False,
+                env=self.virtualenv_env_vars(),
+            )
         return self._virtualenv_session
 
+    def virtualenv_env_vars(self) -> Dict[str, str]:
+        env = self.environment_variables.copy()
+        base_python: List[str] = self.conf["base_python"]
+        if "VIRTUALENV_CLEAR" not in env:
+            env["VIRTUALENV_CLEAR"] = "True"
+        if "VIRTUALENV_NO_PERIODIC_UPDATE" not in env:
+            env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "True"
+        env["VIRTUALENV_SYSTEM_SITE_PACKAGES"] = str(self.conf["system_site_packages"])
+        env["VIRTUALENV_COPIES"] = str(self.conf["always_copy"])
+        env["VIRTUALENV_DOWNLOAD"] = str(self.conf["download"])
+        env["VIRTUALENV_PYTHON"] = "\n".join(base_python)
+        return env
+
     @property
-    def creator(self) -> Creator:  # type: ignore[no-any-unimported]
+    def creator(self) -> Creator:
         return self.session.creator
 
     def create_python_env(self) -> None:
@@ -79,10 +118,10 @@ class VirtualEnv(Python, ABC):
             extra_version_info=None,
         )
 
-    def paths(self) -> List[Path]:
+    def python_env_paths(self) -> List[Path]:
         """Paths to add to the executable"""
         # we use the original executable as shims may be somewhere else
-        return list({self.creator.bin_dir, self.creator.script_dir})
+        return list(dict.fromkeys((self.creator.bin_dir, self.creator.script_dir)))
 
     def env_site_package_dir(self) -> Path:
         return cast(Path, self.creator.purelib)
diff --git a/src/tox/tox_env/python/virtual_env/package/api.py b/src/tox/tox_env/python/virtual_env/package/api.py
index 8bf28c5b..b3a0a11b 100644
--- a/src/tox/tox_env/python/virtual_env/package/api.py
+++ b/src/tox/tox_env/python/virtual_env/package/api.py
@@ -90,7 +90,7 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend):
     ) -> None:
         VirtualEnv.__init__(self, conf, core, options, journal, log_handler)
         Frontend.__init__(self, *Frontend.create_args_from_folder(core["tox_root"]))
-        self._distribution_meta: Optional[PathDistribution] = None  # type: ignore[no-any-unimported]
+        self._distribution_meta: Optional[PathDistribution] = None
         self._build_requires: Optional[Tuple[Requirement]] = None
         self._build_wheel_cache: Optional[WheelResult] = None
         self._backend_executor: Optional[LocalSubProcessPep517Executor] = None
@@ -217,9 +217,7 @@ class Pep517VirtualEnvPackage(VirtualEnv, PythonPackage, Frontend):
         return self._package_dependencies
 
     @staticmethod
-    def discover_package_dependencies(  # type: ignore[no-any-unimported]
-        meta: PathDistribution, extras: Set[str]
-    ) -> List[Requirement]:
+    def discover_package_dependencies(meta: PathDistribution, extras: Set[str]) -> List[Requirement]:
         result: List[Requirement] = []
         requires = meta.requires or []
         for req_str in requires:


Test patch:
diff --git a/tests/config/test_set_env.py b/tests/config/test_set_env.py
index df59830b..ef6ae8d3 100644
--- a/tests/config/test_set_env.py
+++ b/tests/config/test_set_env.py
@@ -44,9 +44,9 @@ def eval_set_env(tox_project: ToxProjectCreator) -> EvalSetEnv:
 def test_set_env_default(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) -> None:
     set_env = eval_set_env("")
     keys = list(set_env)
-    assert keys == ["PIP_DISABLE_PIP_VERSION_CHECK", "VIRTUALENV_NO_PERIODIC_UPDATE"]
+    assert keys == ["PIP_DISABLE_PIP_VERSION_CHECK"]
     values = [set_env.load(k) for k in keys]
-    assert values == ["1", "1"]
+    assert values == ["1"]
 
 
 def test_set_env_self_key(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) -> None:
diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py
index 7c196ee8..1cd3fcda 100644
--- a/tests/session/cmd/test_show_config.py
+++ b/tests/session/cmd/test_show_config.py
@@ -45,7 +45,7 @@ def test_show_config_commands(tox_project: ToxProjectCreator) -> None:
     )
     outcome = project.run("c")
     outcome.assert_success()
-    env_config = outcome.state.tox_env("py").conf
+    env_config = outcome.env_conf("py")
     assert env_config["commands_pre"] == [Command(args=["python", "-c", 'import sys; print("start", sys.executable)'])]
     assert env_config["commands"] == [
         Command(args=["pip", "config", "list"]),
@@ -90,7 +90,7 @@ def test_pass_env_config_default(tox_project: ToxProjectCreator, stdout_is_atty:
     mocker.patch("sys.stdout.isatty", return_value=stdout_is_atty)
     project = tox_project({"tox.ini": ""})
     outcome = project.run("c", "-e", "py", "-k", "pass_env")
-    pass_env = outcome.state.tox_env("py").conf["pass_env"]
+    pass_env = outcome.env_conf("py")["pass_env"]
     is_win = sys.platform == "win32"
     expected = (
         (["COMSPEC"] if is_win else [])
diff --git a/tests/test_provision.py b/tests/test_provision.py
index c08024ae..365ac7a5 100644
--- a/tests/test_provision.py
+++ b/tests/test_provision.py
@@ -124,7 +124,7 @@ def test_provision_requires_ok(
     assert "py" in log_report["testenvs"]
 
     # recreate without recreating the provisioned env
-    provision_env = result_first.state.tox_env(".tox").conf["env_dir"]
+    provision_env = result_first.env_conf(".tox")["env_dir"]
     result_recreate_no_pr = proj.run("r", "--recreate", "--no-recreate-provision")
     result_recreate_no_pr.assert_success()
     assert prov_msg in result_recreate_no_pr.out
diff --git a/tests/tox_env/python/virtual_env/test_package.py b/tests/tox_env/python/virtual_env/test_package.py
index 378a6fb7..9ba1f14e 100644
--- a/tests/tox_env/python/virtual_env/test_package.py
+++ b/tests/tox_env/python/virtual_env/test_package.py
@@ -23,9 +23,9 @@ def test_tox_ini_package_type_valid(tox_project: ToxProjectCreator, pkg_type: st
     proj = tox_project({"tox.ini": f"[testenv]\npackage={pkg_type}"})
     result = proj.run("c", "-k", "package_tox_env_type")
     result.assert_success()
-    res = result.state.tox_env("py").conf["package"]
+    res = result.env_conf("py")["package"]
     assert res is getattr(PackageType, pkg_type)
-    got_type = result.state.tox_env("py").conf["package_tox_env_type"]
+    got_type = result.env_conf("py")["package_tox_env_type"]
     assert got_type == "virtualenv-pep-517"
 
 
diff --git a/tests/tox_env/python/virtual_env/test_virtualenv_api.py b/tests/tox_env/python/virtual_env/test_virtualenv_api.py
new file mode 100644
index 00000000..8320b9dc
--- /dev/null
+++ b/tests/tox_env/python/virtual_env/test_virtualenv_api.py
@@ -0,0 +1,107 @@
+import os
+
+import pytest
+from pytest_mock import MockerFixture
+from virtualenv import session_via_cli
+from virtualenv.config.cli.parser import VirtualEnvOptions
+
+from tox.pytest import MonkeyPatch, ToxProject, ToxProjectCreator
+
+
+@pytest.fixture()
+def virtualenv_opt(monkeypatch: MonkeyPatch, mocker: MockerFixture) -> VirtualEnvOptions:
+    for key in os.environ:
+        if key.startswith("VIRTUALENV_"):  # pragma: no cover
+            monkeypatch.delenv(key)  # pragma: no cover
+    opts = VirtualEnvOptions()
+    mocker.patch(
+        "tox.tox_env.python.virtual_env.api.session_via_cli",
+        side_effect=lambda args, options, setup_logging, env: session_via_cli(args, opts, setup_logging, env),
+    )
+    return opts
+
+
+def test_virtualenv_default_settings(tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions) -> None:
+    proj = tox_project({"tox.ini": "[testenv]\npackage=skip"})
+    result = proj.run("r", "-e", "py")
+    result.assert_success()
+
+    conf = result.env_conf("py")
+    assert conf["system_site_packages"] is False
+    assert conf["always_copy"] is False
+    assert conf["download"] is False
+
+    assert virtualenv_opt.clear is True
+    assert virtualenv_opt.system_site is False
+    assert virtualenv_opt.download is False
+    assert virtualenv_opt.copies is False
+    assert virtualenv_opt.no_periodic_update is True
+    assert virtualenv_opt.python == ["py"]
+
+
+def test_virtualenv_flipped_settings(
+    tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch
+) -> None:
+    proj = tox_project(
+        {"tox.ini": "[testenv]\npackage=skip\nsystem_site_packages=True\nalways_copy=True\ndownload=True"}
+    )
+    monkeypatch.setenv("VIRTUALENV_CLEAR", "0")
+
+    result = proj.run("r", "-e", "py")
+    result.assert_success()
+
+    conf = result.env_conf("py")
+    assert conf["system_site_packages"] is True
+    assert conf["always_copy"] is True
+    assert conf["download"] is True
+
+    assert virtualenv_opt.clear is False
+    assert virtualenv_opt.system_site is True
+    assert virtualenv_opt.download is True
+    assert virtualenv_opt.copies is True
+    assert virtualenv_opt.python == ["py"]
+
+
+def test_virtualenv_env_ignored_if_set(
+    tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch
+) -> None:
+    ini = "[testenv]\npackage=skip\nsystem_site_packages=True\nalways_copy=True\ndownload=True"
+    proj = tox_project({"tox.ini": ini})
+    monkeypatch.setenv("VIRTUALENV_COPIES", "0")
+    monkeypatch.setenv("VIRTUALENV_DOWNLOAD", "0")
+    monkeypatch.setenv("VIRTUALENV_SYSTEM_SITE_PACKAGES", "0")
+    run_and_check_set(proj, virtualenv_opt)
+
+
+def test_virtualenv_env_used_if_not_set(
+    tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch
+) -> None:
+    proj = tox_project({"tox.ini": "[testenv]\npackage=skip"})
+    monkeypatch.setenv("VIRTUALENV_COPIES", "1")
+    monkeypatch.setenv("VIRTUALENV_DOWNLOAD", "1")
+    monkeypatch.setenv("VIRTUALENV_SYSTEM_SITE_PACKAGES", "1")
+    run_and_check_set(proj, virtualenv_opt)
+
+
+def run_and_check_set(proj: ToxProject, virtualenv_opt: VirtualEnvOptions) -> None:
+    result = proj.run("r", "-e", "py")
+    result.assert_success()
+    conf = result.env_conf("py")
+    assert conf["system_site_packages"] is True
+    assert conf["always_copy"] is True
+    assert conf["download"] is True
+    assert virtualenv_opt.system_site is True
+    assert virtualenv_opt.download is True
+    assert virtualenv_opt.copies is True
+
+
+def test_honor_set_env_for_clear_periodic_update(
+    tox_project: ToxProjectCreator, virtualenv_opt: VirtualEnvOptions, monkeypatch: MonkeyPatch
+) -> None:
+    ini = "[testenv]\npackage=skip\nset_env=\n  VIRTUALENV_CLEAR=0\n  VIRTUALENV_NO_PERIODIC_UPDATE=0"
+    proj = tox_project({"tox.ini": ini})
+    result = proj.run("r", "-e", "py")
+    result.assert_success()
+
+    assert virtualenv_opt.clear is False
+    assert virtualenv_opt.no_periodic_update is False