diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7687b1b8cd4..c9cfb7b8e98 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 dae2f21b10d..1502abfc86a 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 c926db4c332..882e36f5c1d 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 diff --git a/tests/conftest.py b/tests/conftest.py index 44aa56026b6..46975b29beb 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 869e8ad921d..93a6b930f66 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 535581121ff..49b362d7e96 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 876f2e12a7c..f611372685f 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 19c526aab09..14e1056ae7a 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 d0bdbc3a547..c960d0de4f9 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 @@ 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 dist_in_site_packages(dist): ) +@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 @@ def test_reset_env_system_site_packages_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 @@ def test_install_subversion_usersite_editable_with_distribute( ) 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 @@ def test_install_user_venv_nositepkgs_fails( ) @pytest.mark.network - @pytest.mark.incompatible_with_test_venv def test_install_user_conflict_in_usersite( self, script: PipTestEnvironment ) -> None: @@ -142,8 +145,6 @@ def test_install_user_conflict_in_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 @@ def test_install_user_conflict_in_globalsite( 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 @@ def test_upgrade_user_conflict_in_globalsite( 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 @@ def test_install_user_conflict_in_globalsite_and_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 e988e741962..49c2d1d6d7c 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 c7fdec2f2fc..d05fe9dcea5 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 2f9fb65ba5a..4578c311468 100644 --- a/tests/functional/test_new_resolver_user.py +++ b/tests/functional/test_new_resolver_user.py @@ -7,7 +7,7 @@ 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 dist_in_site_packages(dist): ) -@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 6d48fe1627a..0bf2e6d4180 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -6,12 +6,12 @@ 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 @@ def test_uninstall_from_usersite_with_dist_in_global_site( """ 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 @@ def test_uninstall_from_usersite_with_dist_in_global_site( 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 ab6644bc9ab..e65a3291230 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -3,10 +3,11 @@ 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 @@ def __init__( 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 @@ def __init__( 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 @@ def _update_paths(self) -> None: 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"" @@ -66,7 +88,11 @@ def _create(self, clear: bool = False) -> None: 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 @@ def _create(self, clear: bool = False) -> None: 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 @@ def _create(self, clear: bool = False) -> None: "--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 @@ def _fix_virtualenv_site_module(self) -> None: 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 @@ def _customize_site(self) -> None: 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 @@ def _customize_site(self) -> None: # 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 @@ def user_site_packages(self) -> bool: @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 9ce6d62078a..84b7c14d4b4 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 38d5383ce04..94461c6d89e 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