Repository: pypa/pip
Pull request #11288 (2022-07-22 06:13:50)

Update test suite to use virtualenv >= 20.0.0
Let’s restart #7718, we must do this before 3.11 is out.

This is mostly #8441 re-applied to main with some very minor changes.

pradyunsg: FWIW, I've been looking into this as well (https://github.com/pradyunsg/pip/tree/improve-test-isolation), although I'm trying there to move us off of virtualenv entirely and that involves getting the venv-based setup to be at-parity with the existing virtualenv-based setup (which it's not).

I've still not gotten all the tests to pass locally though, so I didn't file a PR for that yet. 😅 

uranusjr: My current impression is if we manage to get new virtualenv to work, we should be very close with replacing it altogether with venv. The main blocker to `virtualenv>=20` is how PEP 503 controls global site-packages access and sitecustomize differently, and if we manage to fix that, most of the venv incompatibilities automatically goes away.
pradyunsg: Yea, that's my understanding as well. I figured that it's not that big of a hop to try complete removal in the first shot -- the problems that need to be solved are roughly the same and it removed the conditionals for which backend we're using, which I was found were making it difficult for me to keep track of what's happening in the code.

The thing that I last struggled with is that the code was still picking up the `sitecustomize` from the base environment -- thanks to Homebrew having one -- which borked my Homebrew installation of Python and, I ended up stopping after fixing that mess for the third time and stopped at some point while trying to understand where it was being added in site.py and how to prevent that from happening again (i.e. not leaking global site-customise into the isolated test environment). 😅 

> [Don't enable user site in template venv](https://github.com/pypa/pip/pull/11288/commits/6f3500980c7679a7ee2cbcce555e6375842cc228)

IIRC, a bunch of tests rely on the virtualenv having user-site visible -- which isn't a problem since we have the isolate fixture which makes the user's home directory point to a temp directory, during the pip execution. I think I hit this as part of working on that branch as well. 😅 

pfmoore: > PEP 503

Typo? Sorry, I couldn't work out what the correct PEP number was.
pradyunsg: I'm gonna guess that was supposed to be https://peps.python.org/pep-0405/
sbidoul: @uranusjr @pradyunsg By reading the above and looking at the number of failing tests it seems you have a good sense of what needs to be done and have made good progress.

With the RM hat on, I'm trying to get a feeling of when I'll push 22.2.1 out and if it should wait for this in order to have more assurance that everything works fine on Python 3.11.

So no pressure at all, of course, but if you have hints to share about your plans regarding this topic that'll help me.

uranusjr: Don’t let this block the release. This doesn’t need to be tied to one since the change should be strictly in the test suite.
pradyunsg: I missed this mention, but yea, this doesn’t block a bug fix. It’s more a logistical item needed for testing 3.11 and for reducing long term pain in this space.
uranusjr: I think this is working! Only failure in CI now is the `git submodule` issue. The main changes are made to accomodate bad interactions between distutils and the new virtualenv hacks. Since old virtualenv only does not work in modern Python versions, I decided to _only use new virtualenv with Python 3.10 and later_ and leave the old test setups as-is. A few tests also have the same distutils issues due to relying on `setup.py install`, and are rewritten to use wheels instead.
uranusjr: I think this actually works now; the only failure is the submodule one.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7687b1b8c..c9cfb7b8e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -122,7 +122,7 @@ jobs:
         if: matrix.os == 'MacOS'
         run: brew install breezy
 
-      - run: pip install nox 'virtualenv<20' 'setuptools != 60.6.0'
+      - run: pip install nox
 
       # Main check
       - name: Run unit tests
@@ -179,7 +179,7 @@ jobs:
           $acl.AddAccessRule($rule)
           Set-Acl "R:\Temp" $acl
 
-      - run: pip install nox 'virtualenv<20'
+      - run: pip install nox
         env:
           TEMP: "R:\\Temp"
 
@@ -261,7 +261,7 @@ jobs:
       - name: Install Ubuntu dependencies
         run: sudo apt-get install bzr
 
-      - run: pip install nox 'virtualenv<20'
+      - run: pip install nox
 
       - name: Run unit tests
         run: >-
diff --git a/setup.cfg b/setup.cfg
index dae2f21b1..1502abfc8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -63,7 +63,6 @@ xfail_strict = True
 markers =
     network: tests that need network
     incompatible_with_sysconfig
-    incompatible_with_test_venv
     incompatible_with_venv
     no_auto_tempdir_manager
     unit: unit tests
diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py
index c926db4c3..882e36f5c 100644
--- a/src/pip/_internal/utils/virtualenv.py
+++ b/src/pip/_internal/utils/virtualenv.py
@@ -19,7 +19,7 @@ def _running_under_venv() -> bool:
     return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
 
 
-def _running_under_regular_virtualenv() -> bool:
+def _running_under_legacy_virtualenv() -> bool:
     """Checks if sys.real_prefix is set.
 
     This handles virtual environments created with pypa's virtualenv.
@@ -29,8 +29,8 @@ def _running_under_regular_virtualenv() -> bool:
 
 
 def running_under_virtualenv() -> bool:
-    """Return True if we're running inside a virtualenv, False otherwise."""
-    return _running_under_venv() or _running_under_regular_virtualenv()
+    """True if we're running inside a virtual environment, False otherwise."""
+    return _running_under_venv() or _running_under_legacy_virtualenv()
 
 
 def _get_pyvenv_cfg_lines() -> Optional[List[str]]:
@@ -77,7 +77,7 @@ def _no_global_under_venv() -> bool:
     return False
 
 
-def _no_global_under_regular_virtualenv() -> bool:
+def _no_global_under_legacy_virtualenv() -> bool:
     """Check if "no-global-site-packages.txt" exists beside site.py
 
     This mirrors logic in pypa/virtualenv for determining whether system
@@ -98,7 +98,7 @@ def virtualenv_no_global() -> bool:
     if _running_under_venv():
         return _no_global_under_venv()
 
-    if _running_under_regular_virtualenv():
-        return _no_global_under_regular_virtualenv()
+    if _running_under_legacy_virtualenv():
+        return _no_global_under_legacy_virtualenv()
 
     return False


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 44aa56026..46975b29b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -108,10 +108,6 @@ def pytest_collection_modifyitems(config: Config, items: List[pytest.Function])
             if item.get_closest_marker("network") is not None:
                 item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2))
 
-        if item.get_closest_marker("incompatible_with_test_venv") and config.getoption(
-            "--use-venv"
-        ):
-            item.add_marker(pytest.mark.skip("Incompatible with test venv"))
         if (
             item.get_closest_marker("incompatible_with_venv")
             and sys.prefix != sys.base_prefix
@@ -474,9 +470,6 @@ def virtualenv_template(
         ):
             (venv.bin / exe).unlink()
 
-    # Enable user site packages.
-    venv.user_site_packages = True
-
     # Rename original virtualenv directory to make sure
     # it's not reused by mistake from one of the copies.
     venv_template = tmpdir / "venv_template"
@@ -742,3 +735,8 @@ def mock_server() -> Iterator[MockServer]:
 @pytest.fixture
 def proxy(request: pytest.FixtureRequest) -> str:
     return request.config.getoption("proxy")
+
+
+@pytest.fixture
+def enable_user_site(virtualenv: VirtualEnvironment) -> None:
+    virtualenv.user_site_packages = True
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 869e8ad92..93a6b930f 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -204,7 +204,7 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
     assert result.stdout.strip() == "2.0", str(result)
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_build_env_isolation(script: PipTestEnvironment) -> None:
 
     # Create dummy `pkg` wheel.
diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py
index 535581121..49b362d7e 100644
--- a/tests/functional/test_freeze.py
+++ b/tests/functional/test_freeze.py
@@ -862,7 +862,7 @@ def test_freeze_with_requirement_option_package_repeated_multi_file(
 
 
 @pytest.mark.network
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_freeze_user(
     script: PipTestEnvironment, virtualenv: VirtualEnvironment, data: TestData
 ) -> None:
@@ -900,7 +900,7 @@ def test_freeze_path(tmpdir: Path, script: PipTestEnvironment, data: TestData) -
 
 
 @pytest.mark.network
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_freeze_path_exclude_user(
     tmpdir: Path, script: PipTestEnvironment, data: TestData
 ) -> None:
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 876f2e12a..f61137268 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -171,7 +171,7 @@ def test_pep518_allows_missing_requires(
     assert result.files_created
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_pep518_with_user_pip(
     script: PipTestEnvironment, pip_src: Path, data: TestData, common_wheels: Path
 ) -> None:
@@ -2106,7 +2106,7 @@ def test_target_install_ignores_distutils_config_install_prefix(
     result.did_not_create(relative_script_base)
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_user_config_accepted(script: PipTestEnvironment) -> None:
     # user set in the config file is parsed as 0/1 instead of True/False.
     # Check that this doesn't cause a problem.
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 19c526aab..14e1056ae 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -305,8 +305,7 @@ def test_install_local_with_subdirectory(script: PipTestEnvironment) -> None:
     result.assert_installed("version_subpkg.py", editable=False)
 
 
-@pytest.mark.incompatible_with_test_venv
-@pytest.mark.usefixtures("with_wheel")
+@pytest.mark.usefixtures("enable_user_site", "with_wheel")
 def test_wheel_user_with_prefix_in_pydistutils_cfg(
     script: PipTestEnvironment, data: TestData
 ) -> None:
diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py
index d0bdbc3a5..c960d0de4 100644
--- a/tests/functional/test_install_user.py
+++ b/tests/functional/test_install_user.py
@@ -1,6 +1,7 @@
 """
 tests specific to "pip install --user"
 """
+import os
 import textwrap
 from os.path import curdir, isdir, isfile
 from pathlib import Path
@@ -8,7 +9,12 @@ from pathlib import Path
 import pytest
 
 from tests.lib import pyversion  # noqa: F401
-from tests.lib import PipTestEnvironment, TestData, need_svn
+from tests.lib import (
+    PipTestEnvironment,
+    TestData,
+    create_basic_wheel_for_package,
+    need_svn,
+)
 from tests.lib.local_repos import local_checkout
 from tests.lib.venv import VirtualEnvironment
 
@@ -29,9 +35,9 @@ def _patch_dist_in_site_packages(virtualenv: VirtualEnvironment) -> None:
     )
 
 
+@pytest.mark.usefixtures("enable_user_site")
 class Tests_UserSite:
     @pytest.mark.network
-    @pytest.mark.incompatible_with_test_venv
     def test_reset_env_system_site_packages_usersite(
         self, script: PipTestEnvironment
     ) -> None:
@@ -51,7 +57,6 @@ class Tests_UserSite:
     @pytest.mark.xfail
     @pytest.mark.network
     @need_svn
-    @pytest.mark.incompatible_with_test_venv
     def test_install_subversion_usersite_editable_with_distribute(
         self, script: PipTestEnvironment, tmpdir: Path
     ) -> None:
@@ -71,7 +76,6 @@ class Tests_UserSite:
         )
         result.assert_installed("INITools", use_user_site=True)
 
-    @pytest.mark.incompatible_with_test_venv
     @pytest.mark.usefixtures("with_wheel")
     def test_install_from_current_directory_into_usersite(
         self, script: PipTestEnvironment, data: TestData
@@ -117,7 +121,6 @@ class Tests_UserSite:
         )
 
     @pytest.mark.network
-    @pytest.mark.incompatible_with_test_venv
     def test_install_user_conflict_in_usersite(
         self, script: PipTestEnvironment
     ) -> None:
@@ -142,8 +145,6 @@ class Tests_UserSite:
         result2.did_create(egg_info_folder)
         assert not isfile(initools_v3_file), initools_v3_file
 
-    @pytest.mark.network
-    @pytest.mark.incompatible_with_test_venv
     def test_install_user_conflict_in_globalsite(
         self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
     ) -> None:
@@ -151,31 +152,41 @@ class Tests_UserSite:
         Test user install with conflict in global site ignores site and
         installs to usersite
         """
-        _patch_dist_in_site_packages(virtualenv)
+        create_basic_wheel_for_package(script, "initools", "0.1")
+        create_basic_wheel_for_package(script, "initools", "0.2")
 
-        script.pip("install", "INITools==0.2", "--no-binary=:all:")
+        _patch_dist_in_site_packages(virtualenv)
 
-        result2 = script.pip("install", "--user", "INITools==0.1", "--no-binary=:all:")
+        script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "initools==0.2",
+        )
+        result2 = script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "--user",
+            "initools==0.1",
+        )
 
         # usersite has 0.1
-        # we still test for egg-info because no-binary implies setup.py install
-        egg_info_folder = script.user_site / f"INITools-0.1-py{pyversion}.egg-info"
+        dist_info_folder = script.user_site / "initools-0.1.dist-info"
         initools_folder = script.user_site / "initools"
-        result2.did_create(egg_info_folder)
+        result2.did_create(dist_info_folder)
         result2.did_create(initools_folder)
 
         # site still has 0.2 (can't look in result1; have to check)
-        egg_info_folder = (
-            script.base_path
-            / script.site_packages
-            / f"INITools-0.2-py{pyversion}.egg-info"
+        dist_info_folder = (
+            script.base_path / script.site_packages / "initools-0.2.dist-info"
         )
         initools_folder = script.base_path / script.site_packages / "initools"
-        assert isdir(egg_info_folder)
+        assert isdir(dist_info_folder)
         assert isdir(initools_folder)
 
-    @pytest.mark.network
-    @pytest.mark.incompatible_with_test_venv
     def test_upgrade_user_conflict_in_globalsite(
         self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
     ) -> None:
@@ -183,32 +194,42 @@ class Tests_UserSite:
         Test user install/upgrade with conflict in global site ignores site and
         installs to usersite
         """
+        create_basic_wheel_for_package(script, "initools", "0.2")
+        create_basic_wheel_for_package(script, "initools", "0.3.1")
+
         _patch_dist_in_site_packages(virtualenv)
 
-        script.pip("install", "INITools==0.2", "--no-binary=:all:")
+        script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "initools==0.2",
+        )
         result2 = script.pip(
-            "install", "--user", "--upgrade", "INITools", "--no-binary=:all:"
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "--user",
+            "--upgrade",
+            "initools",
         )
 
         # usersite has 0.3.1
-        # we still test for egg-info because no-binary implies setup.py install
-        egg_info_folder = script.user_site / f"INITools-0.3.1-py{pyversion}.egg-info"
+        dist_info_folder = script.user_site / "initools-0.3.1.dist-info"
         initools_folder = script.user_site / "initools"
-        result2.did_create(egg_info_folder)
+        result2.did_create(dist_info_folder)
         result2.did_create(initools_folder)
 
         # site still has 0.2 (can't look in result1; have to check)
-        egg_info_folder = (
-            script.base_path
-            / script.site_packages
-            / f"INITools-0.2-py{pyversion}.egg-info"
+        dist_info_folder = (
+            script.base_path / script.site_packages / "initools-0.2.dist-info"
         )
         initools_folder = script.base_path / script.site_packages / "initools"
-        assert isdir(egg_info_folder), result2.stdout
+        assert isdir(dist_info_folder), result2.stdout
         assert isdir(initools_folder)
 
-    @pytest.mark.network
-    @pytest.mark.incompatible_with_test_venv
     def test_install_user_conflict_in_globalsite_and_usersite(
         self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
     ) -> None:
@@ -216,38 +237,57 @@ class Tests_UserSite:
         Test user install with conflict in globalsite and usersite ignores
         global site and updates usersite.
         """
-        _patch_dist_in_site_packages(virtualenv)
+        initools_v3_file_name = os.path.join("initools", "configparser.py")
+        create_basic_wheel_for_package(script, "initools", "0.1")
+        create_basic_wheel_for_package(script, "initools", "0.2")
+        create_basic_wheel_for_package(
+            script,
+            "initools",
+            "0.3",
+            extra_files={initools_v3_file_name: "# Hi!"},
+        )
 
-        script.pip("install", "INITools==0.2", "--no-binary=:all:")
-        script.pip("install", "--user", "INITools==0.3", "--no-binary=:all:")
+        _patch_dist_in_site_packages(virtualenv)
 
-        result3 = script.pip("install", "--user", "INITools==0.1", "--no-binary=:all:")
+        script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "initools==0.2",
+        )
+        script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "--user",
+            "initools==0.3",
+        )
+        result3 = script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "--user",
+            "initools==0.1",
+        )
 
         # usersite has 0.1
-        # we still test for egg-info because no-binary implies setup.py install
-        egg_info_folder = script.user_site / f"INITools-0.1-py{pyversion}.egg-info"
-        initools_v3_file = (
-            # file only in 0.3
-            script.base_path
-            / script.user_site
-            / "initools"
-            / "configparser.py"
-        )
-        result3.did_create(egg_info_folder)
+        dist_info_folder = script.user_site / "initools-0.1.dist-info"
+        result3.did_create(dist_info_folder)
+        initools_v3_file = script.base_path / script.user_site / initools_v3_file_name
         assert not isfile(initools_v3_file), initools_v3_file
 
         # site still has 0.2 (can't just look in result1; have to check)
-        egg_info_folder = (
-            script.base_path
-            / script.site_packages
-            / f"INITools-0.2-py{pyversion}.egg-info"
+        dist_info_folder = (
+            script.base_path / script.site_packages / "initools-0.2.dist-info"
         )
         initools_folder = script.base_path / script.site_packages / "initools"
-        assert isdir(egg_info_folder)
+        assert isdir(dist_info_folder)
         assert isdir(initools_folder)
 
     @pytest.mark.network
-    @pytest.mark.incompatible_with_test_venv
     def test_install_user_in_global_virtualenv_with_conflict_fails(
         self, script: PipTestEnvironment
     ) -> None:
diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py
index e988e7419..49c2d1d6d 100644
--- a/tests/functional/test_install_wheel.py
+++ b/tests/functional/test_install_wheel.py
@@ -406,8 +406,7 @@ def test_wheel_record_lines_have_updated_hash_for_scripts(
     ]
 
 
-@pytest.mark.incompatible_with_test_venv
-@pytest.mark.usefixtures("with_wheel")
+@pytest.mark.usefixtures("enable_user_site", "with_wheel")
 def test_install_user_wheel(
     script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
 ) -> None:
diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py
index c7fdec2f2..d05fe9dce 100644
--- a/tests/functional/test_list.py
+++ b/tests/functional/test_list.py
@@ -129,7 +129,7 @@ def test_multiple_exclude_and_normalization(
 
 
 @pytest.mark.network
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_user_flag(script: PipTestEnvironment, data: TestData) -> None:
     """
     Test the behavior of --user flag in the list command
@@ -144,7 +144,7 @@ def test_user_flag(script: PipTestEnvironment, data: TestData) -> None:
 
 
 @pytest.mark.network
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_user_columns_flag(script: PipTestEnvironment, data: TestData) -> None:
     """
     Test the behavior of --user --format=columns flags in the list command
@@ -656,7 +656,7 @@ def test_list_path(tmpdir: Path, script: PipTestEnvironment, data: TestData) ->
     assert {"name": "simple", "version": "2.0"} in json_result
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_list_path_exclude_user(
     tmpdir: Path, script: PipTestEnvironment, data: TestData
 ) -> None:
diff --git a/tests/functional/test_new_resolver_user.py b/tests/functional/test_new_resolver_user.py
index 2f9fb65ba..4578c3114 100644
--- a/tests/functional/test_new_resolver_user.py
+++ b/tests/functional/test_new_resolver_user.py
@@ -7,7 +7,7 @@ from tests.lib import PipTestEnvironment, create_basic_wheel_for_package
 from tests.lib.venv import VirtualEnvironment
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_new_resolver_install_user(script: PipTestEnvironment) -> None:
     create_basic_wheel_for_package(script, "base", "0.1.0")
     result = script.pip(
@@ -22,7 +22,7 @@ def test_new_resolver_install_user(script: PipTestEnvironment) -> None:
     result.did_create(script.user_site / "base")
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_new_resolver_install_user_satisfied_by_global_site(
     script: PipTestEnvironment,
 ) -> None:
@@ -53,7 +53,7 @@ def test_new_resolver_install_user_satisfied_by_global_site(
     result.did_not_create(script.user_site / "base")
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_new_resolver_install_user_conflict_in_user_site(
     script: PipTestEnvironment,
 ) -> None:
@@ -91,7 +91,7 @@ def test_new_resolver_install_user_conflict_in_user_site(
     result.did_not_create(base_2_dist_info)
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(
     script: PipTestEnvironment,
 ) -> None:
@@ -141,8 +141,7 @@ def patch_dist_in_site_packages(virtualenv: VirtualEnvironment) -> None:
     )
 
 
-@pytest.mark.incompatible_with_test_venv
-@pytest.mark.usefixtures("patch_dist_in_site_packages")
+@pytest.mark.usefixtures("enable_user_site", "patch_dist_in_site_packages")
 def test_new_resolver_install_user_reinstall_global_site(
     script: PipTestEnvironment,
 ) -> None:
@@ -177,8 +176,7 @@ def test_new_resolver_install_user_reinstall_global_site(
     assert "base" in site_packages_content
 
 
-@pytest.mark.incompatible_with_test_venv
-@pytest.mark.usefixtures("patch_dist_in_site_packages")
+@pytest.mark.usefixtures("enable_user_site", "patch_dist_in_site_packages")
 def test_new_resolver_install_user_conflict_in_global_site(
     script: PipTestEnvironment,
 ) -> None:
@@ -215,8 +213,7 @@ def test_new_resolver_install_user_conflict_in_global_site(
     assert "base-1.0.0.dist-info" in site_packages_content
 
 
-@pytest.mark.incompatible_with_test_venv
-@pytest.mark.usefixtures("patch_dist_in_site_packages")
+@pytest.mark.usefixtures("enable_user_site", "patch_dist_in_site_packages")
 def test_new_resolver_install_user_conflict_in_global_and_user_sites(
     script: PipTestEnvironment,
 ) -> None:
diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py
index 6d48fe162..0bf2e6d41 100644
--- a/tests/functional/test_uninstall_user.py
+++ b/tests/functional/test_uninstall_user.py
@@ -6,12 +6,12 @@ from os.path import isdir, isfile, normcase
 import pytest
 
 from tests.functional.test_install_user import _patch_dist_in_site_packages
-from tests.lib import pyversion  # noqa: F401
 from tests.lib import PipTestEnvironment, TestData, assert_all_changes
 from tests.lib.venv import VirtualEnvironment
+from tests.lib.wheel import make_wheel
 
 
-@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("enable_user_site")
 class Tests_UninstallUserSite:
     @pytest.mark.network
     def test_uninstall_from_usersite(self, script: PipTestEnvironment) -> None:
@@ -28,14 +28,39 @@ class Tests_UninstallUserSite:
         """
         Test uninstall from usersite (with same dist in global site)
         """
+        entry_points_txt = "[console_scripts]\nscript = pkg:func"
+        make_wheel(
+            "pkg",
+            "0.1",
+            extra_metadata_files={"entry_points.txt": entry_points_txt},
+        ).save_to_dir(script.scratch_path)
+        make_wheel(
+            "pkg",
+            "0.1.1",
+            extra_metadata_files={"entry_points.txt": entry_points_txt},
+        ).save_to_dir(script.scratch_path)
+
         _patch_dist_in_site_packages(virtualenv)
 
-        script.pip_install_local("pip-test-package==0.1", "--no-binary=:all:")
+        script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "--no-warn-script-location",
+            "pkg==0.1",
+        )
 
-        result2 = script.pip_install_local(
-            "--user", "pip-test-package==0.1.1", "--no-binary=:all:"
+        result2 = script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            script.scratch_path,
+            "--no-warn-script-location",
+            "--user",
+            "pkg==0.1.1",
         )
-        result3 = script.pip("uninstall", "-vy", "pip-test-package")
+        result3 = script.pip("uninstall", "-vy", "pkg")
 
         # uninstall console is mentioning user scripts, but not global scripts
         assert normcase(script.user_bin_path) in result3.stdout, str(result3)
@@ -45,13 +70,8 @@ class Tests_UninstallUserSite:
         assert_all_changes(result2, result3, [script.venv / "build", "cache"])
 
         # site still has 0.2 (can't look in result1; have to check)
-        # keep checking for egg-info because no-binary implies setup.py install
-        egg_info_folder = (
-            script.base_path
-            / script.site_packages
-            / f"pip_test_package-0.1-py{pyversion}.egg-info"
-        )
-        assert isdir(egg_info_folder)
+        dist_info_folder = script.base_path / script.site_packages / "pkg-0.1.dist-info"
+        assert isdir(dist_info_folder)
 
     def test_uninstall_editable_from_usersite(
         self, script: PipTestEnvironment, data: TestData
diff --git a/tests/lib/venv.py b/tests/lib/venv.py
index ab6644bc9..e65a32912 100644
--- a/tests/lib/venv.py
+++ b/tests/lib/venv.py
@@ -3,10 +3,11 @@ import os
 import shutil
 import subprocess
 import sys
+import sysconfig
 import textwrap
 import venv as _venv
 from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING, Dict, Optional, Union
 
 import virtualenv as _virtualenv
 
@@ -30,7 +31,7 @@ class VirtualEnvironment:
         location: Path,
         template: Optional["VirtualEnvironment"] = None,
         venv_type: Optional[VirtualEnvironmentType] = None,
-    ):
+    ) -> None:
         self.location = location
         assert template is None or venv_type is None
         self._venv_type: VirtualEnvironmentType
@@ -46,7 +47,13 @@ class VirtualEnvironment:
         self._update_paths()
         self._create()
 
-    def _update_paths(self) -> None:
+    @property
+    def _legacy_virtualenv(self) -> bool:
+        if self._venv_type != "virtualenv":
+            return False
+        return int(_virtualenv.__version__.split(".", 1)[0]) < 20
+
+    def __update_paths_legacy(self) -> None:
         home, lib, inc, bin = _virtualenv.path_locations(self.location)
         self.bin = Path(bin)
         self.site = Path(lib) / "site-packages"
@@ -57,6 +64,21 @@ class VirtualEnvironment:
         else:
             self.lib = Path(lib)
 
+    def _update_paths(self) -> None:
+        if self._legacy_virtualenv:
+            self.__update_paths_legacy()
+            return
+        bases = {
+            "installed_base": self.location,
+            "installed_platbase": self.location,
+            "base": self.location,
+            "platbase": self.location,
+        }
+        paths = sysconfig.get_paths(vars=bases)
+        self.bin = Path(paths["scripts"])
+        self.site = Path(paths["purelib"])
+        self.lib = Path(paths["stdlib"])
+
     def __repr__(self) -> str:
         return f"<VirtualEnvironment {self.location}>"
 
@@ -66,7 +88,11 @@ class VirtualEnvironment:
         if self._template:
             # On Windows, calling `_virtualenv.path_locations(target)`
             # will have created the `target` directory...
-            if sys.platform == "win32" and self.location.exists():
+            if (
+                self._legacy_virtualenv
+                and sys.platform == "win32"
+                and self.location.exists()
+            ):
                 self.location.rmdir()
             # Clone virtual environment from template.
             shutil.copytree(self._template.location, self.location, symlinks=True)
@@ -74,7 +100,7 @@ class VirtualEnvironment:
             self._user_site_packages = self._template.user_site_packages
         else:
             # Create a new virtual environment.
-            if self._venv_type == "virtualenv":
+            if self._legacy_virtualenv:
                 subprocess.check_call(
                     [
                         sys.executable,
@@ -83,20 +109,31 @@ class VirtualEnvironment:
                         "--no-pip",
                         "--no-wheel",
                         "--no-setuptools",
-                        str(self.location),
+                        os.fspath(self.location),
                     ]
                 )
-                self._fix_virtualenv_site_module()
+                self._fix_legacy_virtualenv_site_module()
+            elif self._venv_type == "virtualenv":
+                _virtualenv.cli_run(
+                    [
+                        "--no-pip",
+                        "--no-wheel",
+                        "--no-setuptools",
+                        os.fspath(self.location),
+                    ],
+                )
             elif self._venv_type == "venv":
                 builder = _venv.EnvBuilder()
                 context = builder.ensure_directories(self.location)
                 builder.create_configuration(context)
                 builder.setup_python(context)
                 self.site.mkdir(parents=True, exist_ok=True)
+            else:
+                raise RuntimeError(f"Unsupported venv type {self._venv_type!r}")
             self.sitecustomize = self._sitecustomize
             self.user_site_packages = self._user_site_packages
 
-    def _fix_virtualenv_site_module(self) -> None:
+    def _fix_legacy_virtualenv_site_module(self) -> None:
         # Patch `site.py` so user site work as expected.
         site_py = self.lib / "site.py"
         with open(site_py) as fp:
@@ -131,17 +168,15 @@ class VirtualEnvironment:
         assert compileall.compile_file(str(site_py), quiet=1, force=True)
 
     def _customize_site(self) -> None:
-        contents = ""
-        if self._venv_type == "venv":
+        if self._legacy_virtualenv:
+            contents = ""
+        else:
             # Enable user site (before system).
-            contents += textwrap.dedent(
-                """
+            contents = textwrap.dedent(
+                f"""
                 import os, site, sys
-
                 if not os.environ.get('PYTHONNOUSERSITE', False):
-
-                    site.ENABLE_USER_SITE = True
-
+                    site.ENABLE_USER_SITE = {self._user_site_packages}
                     # First, drop system-sites related paths.
                     original_sys_path = sys.path[:]
                     known_paths = set()
@@ -152,10 +187,9 @@ class VirtualEnvironment:
                         if path in original_sys_path:
                             original_sys_path.remove(path)
                     sys.path = original_sys_path
-
                     # Second, add user-site.
-                    site.addsitedir(site.getusersitepackages())
-
+                    if {self._user_site_packages}:
+                        site.addsitedir(site.getusersitepackages())
                     # Third, add back system-sites related paths.
                     for path in site.getsitepackages():
                         site.addsitedir(path)
@@ -168,6 +202,21 @@ class VirtualEnvironment:
         # Make sure bytecode is up-to-date too.
         assert compileall.compile_file(str(sitecustomize), quiet=1, force=True)
 
+    def _rewrite_pyvenv_cfg(self, replacements: Dict[str, str]) -> None:
+        pyvenv_cfg = self.location.joinpath("pyvenv.cfg")
+        lines = pyvenv_cfg.read_text(encoding="utf-8").splitlines()
+
+        def maybe_replace_line(line: str) -> str:
+            key = line.split("=", 1)[0].strip()
+            try:
+                value = replacements[key]
+            except KeyError:  # No need to replace.
+                return line
+            return f"{key} = {value}"
+
+        lines = [maybe_replace_line(line) for line in lines]
+        pyvenv_cfg.write_text("\n".join(lines), encoding="utf-8")
+
     def clear(self) -> None:
         self._create(clear=True)
 
@@ -192,11 +241,14 @@ class VirtualEnvironment:
     @user_site_packages.setter
     def user_site_packages(self, value: bool) -> None:
         self._user_site_packages = value
-        if self._venv_type == "virtualenv":
+        if self._legacy_virtualenv:
             marker = self.lib / "no-global-site-packages.txt"
             if self._user_site_packages:
                 marker.unlink()
             else:
                 marker.touch()
-        elif self._venv_type == "venv":
+        else:
+            self._rewrite_pyvenv_cfg(
+                {"include-system-site-packages": str(bool(value)).lower()}
+            )
             self._customize_site()
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 9ce6d6207..84b7c14d4 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -7,7 +7,8 @@ pytest-rerunfailures
 pytest-xdist
 scripttest
 setuptools
-virtualenv < 20.0
+virtualenv < 20.0 ; python_version < '3.10'
+virtualenv >= 20.0 ; python_version >= '3.10'
 werkzeug
 wheel
 tomli-w
diff --git a/tests/unit/test_utils_virtualenv.py b/tests/unit/test_utils_virtualenv.py
index 38d5383ce..94461c6d8 100644
--- a/tests/unit/test_utils_virtualenv.py
+++ b/tests/unit/test_utils_virtualenv.py
@@ -63,7 +63,7 @@ def test_virtualenv_no_global_with_regular_virtualenv(
     monkeypatch.setattr(site, "__file__", os.fspath(tmpdir / "site.py"))
     monkeypatch.setattr(
         virtualenv,
-        "_running_under_regular_virtualenv",
+        "_running_under_legacy_virtualenv",
         lambda: under_virtualenv,
     )
     if no_global_file:
@@ -73,7 +73,7 @@ def test_virtualenv_no_global_with_regular_virtualenv(
 
 
 @pytest.mark.parametrize(
-    "pyvenv_cfg_lines, under_venv, expected, expect_warning",
+    "pyvenv_cfg_lines, under_venv, expect_no_global, expect_warning",
     [
         (None, False, False, False),
         (None, True, True, True),  # this has a warning.
@@ -104,15 +104,15 @@ def test_virtualenv_no_global_with_pep_405_virtual_environment(
     caplog: pytest.LogCaptureFixture,
     pyvenv_cfg_lines: Optional[List[str]],
     under_venv: bool,
-    expected: bool,
+    expect_no_global: bool,
     expect_warning: bool,
 ) -> None:
-    monkeypatch.setattr(virtualenv, "_running_under_regular_virtualenv", lambda: False)
+    monkeypatch.setattr(virtualenv, "_running_under_legacy_virtualenv", lambda: False)
     monkeypatch.setattr(virtualenv, "_get_pyvenv_cfg_lines", lambda: pyvenv_cfg_lines)
     monkeypatch.setattr(virtualenv, "_running_under_venv", lambda: under_venv)
 
     with caplog.at_level(logging.WARNING):
-        assert virtualenv.virtualenv_no_global() == expected
+        assert virtualenv.virtualenv_no_global() == expect_no_global
 
     if expect_warning:
         assert caplog.records